This post summarizes a few tips that increase the readability of Haskell code in my anecdotal experience. Each guideline will have a corresponding rationale.
Do not take this post to mean that all Haskell code should be written this way. These are guidelines for code that you wish to use as expository examples to introduce people to the language without scaring them with unfamiliar syntax.
Rule #1: Don't use ($)
This is probably the most controversial guideline but I believe this is the recommendation which has the highest impact on readability.
A typical example of this issue is something like the following code:
print $ even $ 4 * 2
... which is equivalent to this code:
print (even (4 * 2))
The biggest issue with the dollar sign is that most people will not recognize it as an operator! There is no precedent for using the dollar sign as an operator in any other languages. Indeed, the vast majority of developers program in languages that do not support adding new operators at all, such as Javascript, Java, C++, or Python, so you cannot reasonably expect them to immediately recognize that the dollar symbol is an operator.
This then leads people to believe that the dollar sign is some sort of built-in language syntax, which in turn convinces them that Haskell's syntax is needlessly complex and optimized for being terse over readable. This perception is compounded by the fact that the most significant use of the dollar symbol outside of Haskell is in Perl (a language notorious for being write-only).
Suppose that they do recognize that the symbol represents an operator. They still cannot guess at what the operator means. There is no obvious mental connection between a symbol used for currency and function application. There is also no prior art for this connection outside of the Haskell language.
Even if a newcomer is lucky enough to guess that the symbol represents function application, it's still ambiguous because they cannot tell if the symbol is left- or right-associative. Even people who do actually take the time to learn Haskell in more depth have difficulty understanding how ($)
behaves and frequently confuse it with the composition operator, (.)
. If people earnestly learning the language have difficulty understanding ($)
, what chance do skeptics have?
By this point you've already lost many people who might have been potentially interested in the language, and for what? The dollar sign does not even shorten the expression.
Rule #2: Use operators sparingly
Rule #1 is a special case of Rule #2.
My rough guideline for which operators to use is that assocative operators are okay, and all other operators are not okay.
Okay:
(.)
(+)
/(*)
(&&)
/(||)
(++)
Not okay:
(<$>)
/(<*>)
- UseliftA{n}
orApplicativeDo
in the future(^.)
/(^..)
/%~
/.~
- Useview
/toListOf
/over
/set
instead
You don't have to agree with me on the specific operators to keep or reject. The important part is just using them more sparingly when teaching Haskell.
The issues with operators are very similar in principle to the issue with the dollar sign:
- They are not recognizable as operators to some people, especially if they have no equivalent in other languages
- Their meaning is not immediately obvious
- Their precedence and fixity are not obvious, particular for Haskell-specific operators
The main reason I slightly prefer associative operators is that their fixity does not matter and they usually have prior art outside the language as commonly used mathematical operators.
Rule #3: Use do
notation generously
Prefer do
notation over (>>=)
or fmap
when available, even if it makes your code a few lines longer. People won't reject a language on the basis of verbosity (Java and Go are living proof of that), but they will reject languages on the basis of unfamiliar operators or functions.
This means that instead of writing this:
example = getLine >>= putStrLn . (++ "!")
You instead write something like this:
example = do
str <- getLine
putStrLn (str ++ "!")
If you really want a one-liner you can still use do
notation, just by adding a semicolon:
example = do str <- getLine; putStrLn (str ++ "!")
do
notation and semicolons are immediately recognizable to outsiders because they resemble subroutine syntax and in the most common case (IO
) it is in fact subroutine syntax.
A corollary of this is to use the newly added ApplicativeDo
extension, which was recently merged into the GHC mainline and will be available in the next GHC release. I believe that ApplicativeDo
will be more readable to outsiders than the (<$>)
and (<*>)
operators.
Rule #4: Don't use lenses
Don't get me wrong: I'm one of the biggest advocates for lenses and I think they firmly belong as a mainstream Haskell idiom. However, I don't feel they are appropriate for beginners.
The biggest issues are that:
- It's difficult to explain to beginners how lenses work
- They require Template Haskell or boilerplate lens definitions
- They require separate names for function accessors and lenses, and one or the other is bound to look ugly as a result
- They lead to poor inferred types and error message, even when using the more monomorphic versions in
lens-family-core
Lenses are wonderful, but there's no hurry to teach them. There are already plenty of uniquely amazing things about the Haskell language worth learning before even mentioning lenses.
Rule #5: Use where
and let
generously
Resist the temptation to write one giant expression spanning multiple lines. Break it up into smaller sub-expressions each defined on their own line using either where
or let
.
This rule exists primarily to ease imperative programmers into functional programming. These programmers are accustomed to frequent visual "punctuation" in the form of statement boundaries when reading code. let
and where
visually simulate decomposing a larger program into smaller "statements" even if they are really sub-expressions and not statements.
Rule #6: Use point-free style sparingly
Every Haskell programmer goes through a phase where they try to see if they can eliminate all variable names. Spoiler alert: you always can, but this just makes the code terse and unreadable.
For example, I'll be damned if I know what this means without some careful thought and some pen and paper:
((==) <*>)
... but I can tell at a glance what this equivalent expression does:
\f x -> x == f x
This is a real example, by the way.
There's no hard and fast rule for where to draw the line, but when in doubt err on the side of being less point-free.
Conclusion
That's it! Those six simple rules go a very long way towards improving the readability of Haskell code to outsiders.
Haskell is actually a supremely readable language once you familiarize yourself with the prevailing idioms, thanks to:
- purity
- the opinionated functional paradigm
- minimization of needless side effects and state
However, we should make an extra effort to make our code readable even to complete outsiders with absolutely no familiarity or experience with the language. The entry barrier is one of the most widely cited criticisms of the language and I believe that a simple and clean coding style can lower that barrier.