In preparation for the monad post (and also for some life goals), I’ve been working on learning Haskell. What a weird language.
First thing’s first. I learned using Haskell for Mac and two books. First, I tried using Real World Haskell, but didn’t find it terribly effective. Learn You a Haskell, on the other hand, I thought was extremely well-written and effective at teaching the priniciples. However, I’ve been trying to read Haskell tutorials (and monad explanations) for a few years, and it’s hard to say how much that stuff primed me for being able to understand the language.
I finally feel like I can read Haskell (even if it’s with a little trouble), but I’m not writing it super well yet. Nevertheless, this feels like a victory.
I want to skip all the stuff about functional purity, static compilation, strong types, currying, and lazy evaulation. I’ve read a lot of blog posts about that kind of thing, and I don’t think there’s anything to be gained from repeating the work of others.
I’m going to talk about some stuff which struck me while learning Haskell. This post is meant for people who write code in languages that are not Haskell, but would like to learn more about the experience of learning it. Like many of my posts, it’s for past versions of myself.
Whitespace
Haskell is whitespace significant. This was the most unexpected thing I came across while learning Haskell. I thought we were all in agreement that Python would be the only programming language that maintains structure through whitespace so that we could all ceaselessly make fun of it. Apparently Haskell is also in this hilarious crowd of languages.
It seems like a goal of Haskell is to minimize the amount of punctuation that’s used to write code, and being whitespace significant helps with that.
Elegance
A lot of elegance is borne out of the way that functions are defined in Haskell (and in part I think to its aversion to punctuation). For example, if you wanted a function that filtered out all of the odd numbers in a list and left only the even ones, you could define it “strictly”, with xs
as an explicit parameter:
filterEvens xs = filter even xs
or more “loosely”:
filterEvens = filter even
Because of partial application, we can define filterEvens
as a partially-applied version of filter
. Since that just returns another function, we can just assign it to the name filterEven
and be done.
Because braces or other separating punctuation are optional, creating a function feels a lot more like definition, rather than implementation. These types of functions really lends themselves to succinct, if not one-line, definitions. I think creating functions in this style is a big part of the reason why Haskell’s champions describe it as declarative rather than imperative.
Haskell seems to “fit together” really nicely in a lot of ways. Instead of separating the idea of “blocks” from the idea of “functions”, they’re all the same thing. Anywhere you need a “block” (like the first parameter of filter
), you can just pass a function that has the correct type signature.
Another awesome example of this elegance is the quicksort implementation:
quicksort [] = []
quicksort (head:tail) =
let
smallerNumbers = quicksort (filter (<=head) tail)
biggerNumbers = quicksort (filter (>head) tail)
in
smallerNumbers ++ [head] ++ biggerNumbers
It’s embarrassing to admit, but I’ve never understood how quicksort works. Looking at the Haskell implementation, however, makes it painfully obvious.
- Quicksort for an empty array is an empty array.
- Quicksort for an array with a head and a tail is all the numbers that are smaller than the head, then the head, and then all the numbers that are larger than the head.
(Strictly speaking, this implementation isn’t quicksort exactly. A true quicksort implementation requires mutation, something that Haskell steers you away from. Much like the Sieve of Eratosthenes, a real implementation of quicksort in Haskell is much messier. This is a pedantic point.)
Another great example of elegance in Haskell is foldl
and foldr
. These are the Haskell equivalents of reduce
, which start from either the left of a list or the right.
foldl f initial [] = initial
foldl f initial (head:tail) = foldl f (f initial head) tail
foldr f initial [] = initial
foldr f initial (head:tail) = f head (foldr f initial tail)
They’re defined recursively, and that lends itself to great simplicity. They’re also both defined using the same components, just in a different order. I find myself having to stare at them for a while trying to parse exactly how they work, but they do work. I like the way they parallel each other.
Inelegance
There are also a few things about Haskell that strike me as extremely inelegant. I’ve gotten past the weird operators and the dense mathematical jargon. Again, those have been covered endlessly elsewhere so I’d rather not repeat other people’s work.
I think my expectation was to find no flaws in Haskell. That was obviously a doomed expectation, but I had hoped that since it was designed in the academy to be as weird and experimental as possible, that the creators wouldn’t make any of the terrible compromises that are so apparent in the rest of our programming languages. Haskell is an old language (about as old as Objective-C), and so it will have warts and cruft in it, like any older language.
On of the big things I found that seemed unfinished was the distinction between the identical map
and fmap
. map
is used exclusively for arrays, and fmap
is used for the more general Functor
types. (There’s also an identical function called liftM
for monads, lending almost PHP-esque levels of inconsistency.) These functions all do the exact same thing, but they have different names.
There are a lot of excuses given to explain this insconsistency, including path-dependence and better error messages, but it’s still the class of thing I would expect a language like Haskell to get right.
Another example of inelegance in Haskell is infix notation. The lanugage primarily operates with prefix functions. That means that, for example, the function div
, which divides two integers, is called like so:
div 92 10
For a lot of functions, this is hard to read. English is primarily a subject-verb-object language, so English speakers find “infix notation” easier to read in some cases. If you want to convert the div
function to infix style, you can surround it with backticks.
92 `div` 10
I think this is pretty ugly, and it feels like it was glommed on to the language in some kind of compromise. For div
specifically, you can use the operator /
to achieve the same thing, but this won’t work for all other functions.
There are also other operators like .
(compose), <$>
(apply), and $
(low-precedence apply), which allow you to change the order of functions and parenthesis usage. I’m not sure if it becomes easier to read this kind of Haskell over time, but for now, it’s very confusing to figure out how all the functions compose and apply to each other to build bigger functions.
The final thing that stands out about Haskell was the positional arguments. I’m more convinced now than ever that positional (as opposed to named) arguments are a huge flaw in the way we program. For example, the Haskell function elem
returns whether an element exists in a list. Do you call it with the list first or thing to search? There’s no way to know. (The thing to search comes first.)
This is even more important for partial application (or currying). If I want to make a function that searches many lists for the existence of the element 3
, I can curry the elem
function, like so:
existenceOfThree = elem 3
existenceOfThree [1,2,3] // => True
But if I want a function that searches the same list for many things, I have to either define my own function, or I have to use the function flip
, which flips the order of the inputs:
existsInMyList = flip elem [1,2,3]
And if there’s more than two arguments, it gets even worse.
Writing code like this is when I feel like I’m implementing rather than defining. This code is heading in the direction of imperative, where I have to worry about implementation details.
While I was researching this example, I found the source for both elem
and flip
:
elem = any . (==)
flip f x y = f y x
Look how nice those are! I want all the code I write to be that nice.
Positional arguments are part of how Haskell maintains its terseness, but they’re hard to read, impenetrable for beginners, and cause messiness when we want to apply parameters in a different order.
Haskell and You
The creators of Swift say they picked the best features from all the languages they could find, and a lot those features and functionalism come from Haskell. It’s definitely worth a weekend to take a stab at Learn You A Haskell and see where those ideas come from.