Sometimes, terse operators can make code more readable. An article for all, even people who don't read Haskell code.

The Haskell programming language has a reputation for being terse to the point of being unreadable. That reputation isn't undeserved, but to counter, other languages exist that are verbose to the point of being unreadable.

Particularly, idiomatic Haskell code involves abstruse operators like ., $, <$>, >>=, <*>, <>, and so on. Not only do such operators look scary, but when I started writing Haskell code, it also bothered me that I didn't know how to pronounce these operators. I don't know how you read code, but my brain often tries to 'talk' about the code, silently, inside my head, and when it encounters something like =<<, it tends to stumble.

At least, it used to. These days, my brain has accepted that in many cases, Haskell operators are a little like punctuation marks. When I read a piece of prose, my brain doesn't insist on 'saying' comma, semicolon, question mark, period, etcetera. Such symbols assist reading, and often adjust the meaning of a text, but aren't to be read explicitly as themselves.

I'm beginning to realise that Haskell operators work like that; sometimes, they fade into the background and assist reading.

As a word of caution, don't take this analogy literally. Each Haskell operator means something specific, and they aren't interchangeable. Additionally, Haskell enables you to add your own custom operators, and I'm not sure that e.g. lens operators like .~ or %~ make code more readable.

A simple business code example #

Forgetting about the lens operators, though, consider a piece of business code like this:

tryAccept :: Int -> Reservation -> MaybeT ReservationsProgram Int
tryAccept capacity reservation = do
  guard =<< isReservationInFuture reservation
 
  reservations <- readReservations $ reservationDate reservation
  let reservedSeats = sum $ reservationQuantity <$> reservations
  guard $ reservedSeats + reservationQuantity reservation <= capacity
 
  create $ reservation { reservationIsAccepted = True }

Please read on, even if you don't read Haskell code. I'm not going to walk you through the details of how the operators work. That's not the point of this article. The point is how the operators enable you to focus on the overall picture of what's going on.

To establish a bit of context, this function determines whether or not to accept a restaurant reservation. Even if you've never read Haskell code before, see if you can get a sense of what's going on.

First, there's a guard which seems to involve whether or not the reservation is in the future. Second, there seems to be some calculations involving reservations, reserved seats, culminating in another guard. Third, the function seems to create a reservation by setting reservationIsAccepted to True.

Granted, it probably helps if you know that both = and <- bind the left-hand symbol to the expression on the right side. Additionally, after all this talk about special Haskell operators, it may not be immediately apparent that + is the perfectly normal addition operator, and <= is the well-known less-than-or-equal relational operator. What if we keep those operators, and mask the rest with a white rectangle symbol (▯)?

tryAccept :: Int -> Reservation -> MaybeT ReservationsProgram Int
tryAccept capacity reservation = do
  guard ▯ isReservationInFuture reservation
 
  reservations <- readReservations ▯ reservationDate reservation
  let reservedSeats = sum ▯ reservationQuantity ▯ reservations
  guard ▯ reservedSeats + reservationQuantity reservation <= capacity
 
  create ▯ reservation { reservationIsAccepted = True }

Finally, you also ought to know that while Haskell code is read from top to bottom, you tend to read each expression from right to left. Armed with this knowledge, and by masking the operators, the business logic begins to stand out.

First, it examines whether the reservation is in the future, and it does a guard of that. Again, I don't wish to make any claims that the code is magically self-documenting, because if you don't know what guard does, you don't know if this expression guards against the reservation being in the future, or if, conversely, it ensures that the reservation is in the future. It does the latter.

Second, it conjures up some reservations from somewhere, by first getting the reservationDate from reservation, and then passing that value to readReservations (expressions are read from right to left).

Moving on, it then calculates reservedSeats by starting with reservations, somehow extracting the reservationQuantity from those, and taking the sum. Since we've masked the operators, you can't tell exactly what's going on, but the gist is, hopefully, clear.

The middle block of code concludes with another guard, this time ensuring that the reservedSeats plus the reservationQuantity is less than or equal to the capacity.

Finally, the function sets reservationIsAccepted to True and calls create.

What I find compelling about this is that the terseness of the Haskell operators enables you, a code reader, to scan the code to first understand the big picture.

Guards #

Additionally, some common motifs begin to stand out. For example, there are two guard expressions. Because the operators are terse, the similarities stand out better. Let's juxtapose them:

  guard ▯ isReservationInFuture reservation

  guard ▯ reservedSeats + reservationQuantity reservation <= capacity

It seems clear that the same sort of thing is going on in both cases. There's a guard ensuring that a Boolean condition is satisfied. If you, however, reconsider the actual code, you'll see that the white rectangle hides two different operators:

  guard =<< isReservationInFuture reservation

  guard $ reservedSeats + reservationQuantity reservation <= capacity

The reason for this is that it has to, because otherwise it wouldn't compile. isReservationInFuture reservation has the type MaybeT ReservationsProgram Bool. There's a Boolean value hidden in there, but it's buried inside a container. Using =<< enables you to pull out the Boolean value and pass it to guard.

In the second guard expression, reservedSeats + reservationQuantity reservation <= capacity is a 'naked' Boolean expression, so in this case you can use the $ operator to pass it to guard.

Haskellers may wonder why I chose =<< instead of the more common >>= operator in the first of the two guard expressions. I could have, but then the expression would have been this:

  isReservationInFuture reservation >>= guard

The resulting behaviour is the same, but I think this obscures how the two guard expressions are variations on the same motif.

The use of operators enables you to express code in such a way that motifs stand out. In contrast, I tried writing the same business functionality in F#, but it didn't come out as readable (in my opinion):

// int -> Reservation -> ReservationsProgram<int option>
let tryAccept capacity reservation = reservationsOption {
    do! ReservationsOption.bind guard <| isReservationInFuture reservation
    
    let! reservations = readReservations reservation.Date
    let reservedSeats = List.sumBy (fun r -> r.Quantity) reservations
    do! guard (reservedSeats + reservation.Quantity <= capacity)
 
    return! create { reservation with IsAccepted = true } }

While you can also define custom operators in F#, it's rarely a good idea, for various reasons that, at its core, are related to how F# isn't Haskell. The lack of 'glue' operators in F#, though, obliged me to instead use the more verbose ReservationsOption.bind. This adds noise to the degree that the guard function disappears in the middle of the expression. The motif is fainter.

Piping #

Another motif in Haskell code is piping. This is known from F# as well, where piping is normally done from left to right using the |> operator. You can, as the above example shows, also use the right-to-left pipe operator <|. In Haskell, expressions are idiomatically composed from right to left, often with the $ operator, or, when using point-free style, with the . operator.

Once you realise that expressions compose from right to left, a masked expression like sum ▯ reservationQuantity ▯ reservations begins to look like a pipeline: start with reservations, somehow pipe them to reservationQuantity, and finally pipe the result of doing that to sum. That's actually not quite what happens, but I think that this compellingly communicates the overall idea: start with some reservations, consider their quantities, and calculate the sum of those.

Another way to write that expression would be:

  let reservedSeats = sum (fmap reservationQuantity reservations)

This implements the same behaviour as sum $ reservationQuantity <$> reservations, but once you get used to it, I like the operator-based alternative better. The operators fade into the background, enabling the flow of data to stand out better.

Conclusion #

Haskell operators constitute the glue that enables you to compose expressions together. Often, you need to vary how expressions are composed together, because the types are slightly different. Picking an appropriate operator that composes particular expressions enables you to perform the composition with a high signal-to-noise ratio.

Once you get used to reading Haskell code, the operators can fade into the background in well-factored code, just like punctuation marks assist you when you read prose. As always, this is no silver bullet. I've seen plenty of examples of obscure Haskell code as well, and copious use of operators is a fast way to obfuscate code.

Use; punctuation? marks with. taste!



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 02 July 2018 12:00:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!