In CppCon 2022, Herb Sutter gave a talk titled Can C++ be 10x Simpler & Safer? where he described an alterntive syntax that compiles to C++ (which he calls CppFront or Cpp2) that could remove much of the existing issues with C++ - reduce complexity, increase safety, and more. If you haven't seen it yet, it's worth a watch!

With recent events and discussions in the Haskell community, my mind went back to that talk, thinking whether we can we do the same with Haskell.

Honestly, it does not sound like a terrible idea.

In this post I'll explore 11 ideas I would pursue if I were to build an alternative syntax for Haskell (a new compiler frontend for a modified language that compiles to GHC Haskell source code) to solve some of the most repeated complaints about Haskell.

Not everything here is fully fleshed out, but I hope you'll get the general idea.

Edit to clarify: I'm not attempting anything more here than writing down some thoughts!

1. Interoperability with Haskell

Since our new syntax will be compiled to GHC Haskell source code, interoperability should be seemless, and everything that is expressible in the new syntax should be expressible in Haskell. This way our language can tap into the existing Haskell ecosystem including all of the facilities, and the compilation scheme should be straightforward. Building a new syntax won't mean starting from scratch.

2. Language extensions and stability

GHC Haskell is a big language with many configurable knobs in the form of language extensions. There are pros to that approach: some people like to experiment with shiny new features.

However, others don't like the idea of Haskell being a moving target, don't like inconsistency between codebases, and don't like the idea that they need to enable so many language extensions to do common things.

The solution: no more customization, no more extensions. In the new syntax, the following features (and those only) are always on: GHC2021, LambdaCase, DerivingStrategies/Via, BlockArguments, TypeApplications, ForeignFunctionInterface (and Maybe even StrictData?), and each module will be compiled with these extensions at the top.

Sorry fancier type extensions and fancy types enthusiasts, if you really want them, you probably want to stick to Haskell.

This will give users of the new syntax the stability they are craving, and will let researchers continue evolve Haskell and try new ideas without butting heads (as much) with the first group.

3. Standard library re-energized

The new syntax can provide a clean cut from Haskell and base and make many desired changes to the standard library. Such as:

  1. Clean up prelude and remove footguns such as partial functions like head, slow functions like foldl, excessive polymorphism, etc. And export some useful but missing functions such as for and for_.
  2. Rename common operators such as $, &, ., to <|, |>, <., and .>, (you'll see why soon).
  3. Include containers in the standard library.
  4. Remove type String = [Char] and promote strict Text to be the one true string type in its stead.
  5. Provide a better API around IO than lazy IO.

4. Modules

A few things that I sorely miss from the Haskell module system is the ability to export modules qualified, have easy access to mutually dependent modules, and defining multiple modules in one source file.

We can define a compilation scheme that allows us to have all that using a new syntax.

  1. We can make all module imports qualified by default, and use an expose keyword instead to import unqualified.
  2. We can compile qualified module exports to multiple module imports.
  3. We can split and merge source files to single or multiple source files to achieve what we want.

5. Records

  1. We ignore existing record syntax in Haskell and instead formulate a new syntax that is translated to raw product types.
  2. We revamp record update syntax so nested updates are easy, we use dot syntax for access (this is why . for compose has been renamed), and we base everything on lenses so we get row polymorphism for free.
  3. Records are anonymous and are generated as data types with a canonical representation. Writing

    data Person = Person { name : String, age : Int }

    is translated to something like:

    newtype Person = Person PersonRecord
    data PersonRecord = PersonRecord { _personRecordAge : Int, _personRecordName : String }
    makeFields ''PersonRecord
  4. We can no longer mix records with sum types and get partial accessors or updaters.

6. Syntax changes

We can change or get rid of a bunch of syntax that is rarely in use or just isn't great:

  1. Remove custom list syntax and instead use List, Cons and Nil.
  2. Use : for types.
  3. No more overloaded literals (strings or numbers). Additional suffix can be provided for literals or explicit conversions. Next time you ask newcomers to do :t 17 it will say Int64.
  4. Operators can either only be defined in Haskell land, or must be aliases to existing named functions (I'd vote for the former).
  5. Add multiline strings syntax
  6. Remove arrow syntax
  7. Remove multiple value declarations and patterns outside of case or \cases.
  8. Remove tuples - use records instead.
  9. Provide an official autoformatter out of the box and end formatting arguments.

(I'll go even further and suggest some ideas that might be a bit controversial):

We can even remove the data and type keywords and reclaim them as value names. I would suggest this as the new syntax for data types:

Tree a =
  | Nil
  | Node { value : a, left : Tree a, right : Tree a }

where:

  1. Each constructor gets 0 or 1 argument, for multiple arguments, use a record, its better to have named arguments anyway.
  2. Data types with a single constructor and value are automatically promoted to a newtype.
  3. type aliases are no longer possible.

But again, we don't have to do majorly controversial things if we don't want to.

7. Polymorphic variants

There are encoding that lets you implement polymorphic variants using row polymorphism. Now we can implement a Result type with errors that can compose. You welcome.

8. Debugging

Create a new Debug typeclass that is automatically derived for all data types. This typeclass could provide the ability to pretty print values anywhere in the program.

Additional step debugging tooling can be created by injecting haskell function calls that will halt execution and provide runtime information.

9. Docstrings

Create a new docstring tooling with standard markdown format that compiles to haddock comments.

10. Warnings and Errors

Enable some warnings like -Wall, and even make some of them, especially those around introducing partial functions and non-exhaustive pattern matching, errors by default.

11. A different macro system?

Since we are generating Haskell source code, we could potentially introduce a different kind of macro system.

This seems the most difficult thing to do on this list by-far, and I don't have a lot of experience with this topic, but it seems like a good opportunity to think about it.

Conclusion

While I believe many of these changes are things that many (most?) Haskellers would want, making them in Haskell would significantly alter the language to the point that all existing code would break. That's a tough sell for a well-established language. But a new frontend - essentially a new language, could make it work.

I think there could be a lot of merit to splitting the Haskell language by providing an alternative front-end that will allow many people to have a Haskell that better aligns to their needs - both for those who want stability, and those who want to experiment, in a way that benefits both. And this way does not require forking GHC, or starting from scratch.

Of course, the most difficult part of such ideas is to get concensus from a wide group of people, and to be careful not to step on too many toes. I will probably not attempt to implement such an idea, mostly because I really dislike writing parsers.