##### Day 1, imperatively: `transformers`

and monads

The first puzzle asks us to increment and decrement a floor number every time an opening or closing parenthesis is encountered. In an imperative language, we'd probably begin by initializing a counter, then we'd iterate over the characters of the input string, incrementing or decrementing the counter depending on which character we encounted. Implementing such a solution in Haskell is possible, but is a bit verbose:

```
import Control.Monad
import Control.Monad.Trans.State
day1 :: String -> Int
day1 input = execState go 0 -- counter starts at 0
where
go :: State Int ()
go = do
forM_ [0 .. length input - 1] $ \i -> do
when (input !! i == '(') $ do -- when we encounter '('...
modify (+ 1) -- ...increment the counter
when (input !! i == ')') $ do -- when we encounter ')'...
modify (subtract 1) -- ...decrement the counter
```

Here I have used the `transformers`

library to define a stateful computation. That's right, Haskell is such a pure functional language that it doesn't even have builtin syntax for declaring and incrementing a counter, you need to use a library in order to do that! I'll explain why the library is called "transformers" in the next section.

Haskell has tons of libraries like this defining specialized embeded domain specific languages (EDSLs). In fact, the word "monad" which is always brought up when discussing Haskell is the name of an interface implemented by many such EDSLs. This shared interface allows generic constructs like the above `forM_`

and `when`

to work with any EDSL implementing the interface.

##### Day 1, functionally: pattern-matching and composition

A functional solution would instead transform the input using a series of mathematical functions, in such a way that the final transformation gives the desired result. Here, I'm using pattern-matching to map the two parenthesis characters to the numbers one and minus one, and the final result is the sum of all those numbers. Much more succinct!

```
delta '(' = 1
delta ')' = -1
day1 = sum . map delta
```

The mathematical transformations are applied from right to left: first, `map delta`

transforms the input string (a list of characters) into a list of numbers, and then `sum`

adds those numbers together.

It is customary to give type signatures to top-level definitions. This makes the code a bit less succinct, but it makes it much easier to understand functions in isolation.

```
delta :: Char -> Int
delta '(' = 1
delta ')' = -1
day1 :: String -> Int
day1 = sum . map delta
```

With those type signature, it is clearer that `day1`

is transforming a `String`

into an `Int`

, using two intermediate transformations `map delta`

and `sum`

. I happen to know that `map`

takes a transformation from `a`

to `b`

and produces a transformation from a list of `a`

to a list of `b`

, so `map delta`

transforms the `String`

, which is also a `[Char]`

, into an `[Int]`

, which `sum`

then transforms into an `Int`

.

It is also customary to write *total* functions, meaning that if the signature of `delta`

claims that it transforms `Char`

s into `Int`

s, it better be able to handle any `Char`

. The above implementation of `delta`

is only handling two possible characters, and so even though Advent of Code's input only uses those two characters, I feel compelled to complete my implementation so that it ignores non-parenthesis characters.

```
delta :: Char -> Int
delta '(' = 1
delta ')' = -1
delta _ = 0
```

Finally, while mathematicians like to write composition from right to left (`f (g x)`

first applies `g`

to `x`

, and then applies `f`

to the result, and so the composition is written `f . g`

), in this case I find it more convenient to write down my transformations in the order in which they apply, so I'd rather have a left-to-right composition operator. Such an operator exists in the `Control.Arrow`

module, along with a few other operators for combining and manipulating various kinds of transformations (Haskell supports more kinds of transformations than just mathematical functions).

```
import Control.Arrow
day1 = map delta
>>> sum
```

Notice that I've dropped the type signature for `day1`

. Its type is still `String -> Int`

, but leaving the type out makes it easier for me to visualize intermediate results, using the following technique. I first write the first half of the implementation, `day1 = map delta`

. The type of this transformation is `String -> [Int]`

, and so my `ghcid`

setup will display the resulting list of numbers. After visually inspecting that the result seems correct so far, I add the second line, and `ghcid`

updates its output to display the final result. Keeping the type signature would have discouraged this kind of incremental development, because `ghcid`

would have displayed a type error instead of the intermediate result.

##### Day 1.5, imperatively: `either`

and monad transformers

The next part of the puzzle asks us to count the number of steps until the counter reaches `-1`

. An imperative solution would build on the previous one by using a second counter to track the number of steps, and by aborting the loop as soon as the floor number reaches `-1`

.

Perhaps surprisingly, the EDSL we used earlier for expressing a stateful computation is intentionally limited to a single stateful variable, and so we cannot use it to express our new two-counters algorithm. Instead, we need to combine two instances of the EDSL, each tracking one of the two counters, with a third EDSL called `either`

implementing the ability to abort the computation early. The fact that we can do that is why the library is named `transformers`

: the EDSLs it provides are not only independent EDSLs each providing their own feature such as stateful computation, they also have the ability to transform an input EDSL into a slightly more complex EDSL by adding stateful operations to the list of operations supported by the input EDSL. This idea of building more complex systems out of simpler systems is pervasive in Haskell, and if you're interested in learning more, I made a video about combinator libraries which introduces the subject to non-Haskellers.

Now, using three EDSLs for such a simple problem might seem a bit overkill, and it is. As before, the imperative solution ends up being much more verbose in Haskell than the functional implementation. But let's take a look anyway, as building custom EDSLs is an important Haskell skill which becomes invaluable when structuring larger programs.

```
import Control.Arrow
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.Either as EitherT
import Control.Monad.Trans.State
import Data.Functor.Identity
type MyEDSL = StateT Int (StateT Int (EitherT Int Identity))
runMyEDSL :: MyEDSL () -> Int
runMyEDSL = flip evalStateT 0 -- step counter starts at 0
>>> flip evalStateT 0 -- floor number starts at 0
>>> eitherT return (\() -> error "floor -1 never reached")
>>> runIdentity
```

I first define my custom EDSL, which I name `MyEDSL`

. To build it, I start (from right to left) with the `Identity`

EDSL, which has no special operations. I combine it with `EitherT Int`

, which adds the ability to abort the computation early. Finally, I add two layers of `StateT Int`

, for the two counters. I also define a way to run `MyEDSL`

computations, by running the operations of all three underlying layers (four if we count `Identity`

).

```
getSteps :: MyEDSL Int
getSteps = get
incrSteps :: MyEDSL ()
incrSteps = modify (+1)
getFloor :: MyEDSL Int
getFloor = lift $ get
incrFloor :: MyEDSL ()
incrFloor = lift $ modify (+ 1)
decrFloor :: MyEDSL ()
decrFloor = lift $ modify (subtract 1)
returnEarly :: Int -> MyEDSL ()
returnEarly x = lift $ lift $ EitherT.left x
```

Next, I define operations which are specific to `MyEDSL`

, in terms of the simpler EDSLs it is made of: `get`

and `modify`

are the stateful operations we've already seen in Day 1, and `EitherT.left`

is an operator from `either`

which aborts the computation early. The function `lift`

is used to specify which layer the operation should run in: `op`

runs in the outermost `StateT Int`

layer, `lift $ op`

runs in the second `StateT Int`

layer, and `lift $ lift $ op`

runs in the third layer, the `EitherT Int`

one.

```
day1_5 :: String -> Int
day1_5 input = runMyEDSL go
where
go :: MyEDSL ()
go = do
forM_ [0 .. length input - 1] $ \i -> do
incrSteps
when (input !! i == '(') $ incrFloor
when (input !! i == ')') $ decrFloor
floorNumber <- getFloor
when (floorNumber == -1) $ do
steps <- getSteps
returnEarly steps
```

Once all the operations are defined, I can describe my algorithm in terms of my custom operations: iterate through the input string, count the steps, increment and decrement the floor counter, and return early when floor `-1`

is reached.

##### Day 1.5, functionally: laziness and higher-order functions

As promised, the functional solution is much more succinct:

```
-- |
-- >>> day1_5 ")"
-- 1
-- >>> day1_5 "()())"
-- 5
day1_5 = map delta
>>> scanl (+) 0
>>> takeWhile (/= -1)
>>> length
```

As before, I begin by converting the list of characters into a list of `+1`

s and `-1`

s. This time, however, instead of taking the sum of all the numbers, I use `scanl`

to take a running sum, meaning I get a list of all the intermediate sums as each number is added to the total. I then cut that list short just before the first `-1`

, and I count how many items are in that trimmed list. Oh, and thanks to laziness (Haskell only evaluates the part of the code which needs to be evaluated to obtain the answer), this implementation is just as efficient as the imperative version: even though it would seem like the running sum would need to produce its entire output list before `takeWhile`

could begin to trim it, `map`

and `scanl`

actually stop consuming their input lists after the sum reaches `-1`

, as if by magic.

`map`

, `scanl`

and `takeWhile`

are three of many higher-order functions provided by Haskell's standard library. A "higher-order" function is simply one which takes another function as one of its inputs, in this case in order to produce a modified function. We've already seen how `map`

converts a function on elements into a function on lists. The higher-order function `scanl`

takes a binary function, in this case `(+)`

, and an initial value, in this case `0`

, and produces a function from a list of input elements to a list of outputs obtained by repeatedly applying the input function to the value so far and the next input element. So in this case, since the input function is addition, the result is a running sum. Our last higher-order function, `takeWhile`

, takes a predicate (a function returning a boolean) and produces a function which trims its input list by keeping all the elements as long as they satisfy the predicate, and then dropping the rest of the elements as soon as it encounters one which doens't satisfy the predicate. In this case, we're keeping all the elements which are not `-1`

, and then we're droping `-1`

and the rest of the elements.

High-order functions are another example of the principle of constructing more complex systems, in this case functions, by combining and transforming simpler systems, in this case simpler functions. The function `(+)`

is quite simple, it simply adds two numbers. The number `0`

is even simpler. By applying the higher-order function `scanl`

to `(+)`

and `0`

, we obtain a more sophisticated function which keeps a running sum. And by composing the four functions `map delta`

, `scanl (+) 0`

, `takeWhile (/= 1)`

and `length`

, we obtain a very specialized function which solves Day 1.5's puzzle.

##### Navigation

- About this series
- Next day: type inference and type classes

## No comments:

Post a comment