Write more understandable Haskell with Flow

by Taylor Fausak on

Flow's logo

Last week, I announced Blunt, a tool for converting Haskell expressions between the pointfree and pointful styles. I have a secret, though: I don’t like the pointfree style.

I don’t like it because I think it’s hard to read. Pointfree expressions don’t mention all of their arguments, which forces you to know the arity of the functions they call. And the . operator reads from right to left, meaning your eyes have to jump back and forth when reading code that uses it.

I think Haskell can do better. That’s why I created Flow, a library for writing more understandable Haskell. Flow provides alternatives to common operators in the base package, like function application with $ and function composition with .. Here’s an overview of the functions and operators it defines:

Flow Base
apply x f f x
x |> f x & f
f <| x f $ x
compose f g x g (f x)
f .> g f >>> g
g <. f g . f
apply' x f seq x (f x)
x !> f -
f <! x f $! x

Flow borrows from F#, which defines pipe forward (|>) and pipe backward (<|) operators. It also borrows the compose forward (>>) and compose backward (<<) operators, although I renamed them since Haskell already defines >>.

In addition to operators, Flow provides functions for application and composition. I did this for two reasons: One, every operator should have a name. And two, regular functions are clearer when used with higher-order functions. Compare map (apply 2) to map ($ 2) for example.

Given all that, how can Flow be used to write more understandable Haskell? Let’s look at problem 8 from Project Euler as an example.

Find the thirteen adjacent digits in the 1000-digit number that have the greatest product. What is the value of this product?

I wrote a solution years ago when I was learning Haskell. Recently I re-wrote that solution in a more idiomatic, pointfree way. I’ll present both solutions here and convert them to using Flow.

The first part of the solution is the takes function. It slides a window of the given size over a list. Let’s see an example.

>>> takes 2 [1 .. 3]
[[1,2],[2,3],[3],[]]

When I first wrote this function, I used explicit recursion. There’s nothing wrong with this per se, but most experienced Haskellers wouldn’t write it this way.

takes :: Int -> [a] -> [[a]]
takes _ [] = [[]]
takes n xs@(_ : ys) = take n xs : takes n ys

Since then, I learned about the pointfree style and the tails function. Using both leads to the most idiomatic definition of this function.

takes n = map (take n) . tails

How can you rewrite this function with Flow, and what does it get you? You have two options for converting. If you want to keep the pointfree style, you can use the compose forward operator.

takes n = tails .> map (take n)

If, like me, you don’t want to keep the pointfree style, you can use the pipe forward operator.

takes n xs = xs |> tails |> map (take n)

Either way, what you get is a function that reads naturally from left to right. And if you use the pipe forward operator, your function looks like something you might have written in GHCi. Consider the following interpreter session.

>>> let xs = [1 .. 3]
>>> xs
[1,2,3]
>>> xs |> tails
<interactive>:3:7: Not in scope: ‘tails’
>>> import Data.List
>>> xs |> tails
[[1,2,3],[2,3],[3],[]]
>>> xs |> tails |> map (take n)
<interactive>:6:26: Not in scope: ‘n’
>>> let n = 2
>>> n
2
>>> xs |> tails |> map (take n)
[[1,2],[2,3],[3],[]]

This shows how Flow helps you build up complex expressions one piece at a time. So let’s move on to the next piece of this problem, the euler8 function. Given a window of size n and a string, it returns the largest product of n consecutive numbers in the string. Let’s take a look at a simple example.

>>> euler8 2 "123"
6

The first time I wrote this function, I didn’t know about function composition. I wrote it with a bunch of parentheses. I quickly learned about the $ operator to remove them.

euler8 :: Int -> String -> Int
euler8 n x
    = maximum
    $ map product
    $ takes n
    $ map digitToInt
    $ filter isDigit x

Now that I know about the . operator, I would write this function differently. Like the pointfree definition of takes, this is the most idiomatic way to write this function.

euler8 n
    = maximum
    . map product
    . takes n
    . map digitToInt
    . filter isDigit

In spite of that, this looks backwards to me. You have to read it starting from the bottom in order to understand it. With Flow, you can read it naturally from top to bottom.

euler8 n x = x
    |> filter isDigit
    |> map digitToInt
    |> takes n
    |> map product
    |> maximum

So that’s a quick overview of what Flow does and why you might want it. If you’re interested, please check out Flow’s project page.