Day 2 is another parse-and-compute problem. I opted for a simple lazy list processing parser, with a couple of neat tricks thanks to Arrow combinators.

On a hunch, I wrote the first part in the form of a higher order function. It payed up: the second part was trivial to write.

The Problem

We are given a dataset of unknown length, with the following structure. The explicit newlines (\n) are there to make the explanation easier to follow.

    A Y\n
    B X\n
    C Z\n
    ...

Each line correspons to a round of Rock-Paper-Scissors.

The first column will always contain A, B, or C, which correspond to Paper, Rock, and Scissors, as played by your opponent. The second column will always contain X, Y, or Z. For the first part, they represent a «cheat code» of what we should play, meaning Rock, Paper, and Scissors, respectively.

Each round is scored depending on its outcome, and what you play. The scoring should be obvious from the code below.

Part A wants the total score for all the games.

Main Program

The main program is pretty much the same as Day 1.

Playing the game

The solution for Part A has the signature

    partA :: String -> Int

and we can break its implementation in three parts:

  1. Parse the input String into a list of «turns»

     turns :: String -> [Turn]
    
  2. Have a way to «score» each turn, and map it over the list.

     score :: Turn -> Int
    
  3. Add the list.

This leads to a high level implemenation like so

    partA = sum . map score . turns

You'll notice I'm using Int instead of Integer. This has to do with my wanting to illustrate the use of the Enum typeclass for scoring.

Strong static typing go brrr

Let's create a simple model for Rock-Paper-Scissors using Haskell types. I've already mentioned the notion of Turn to represent each line to process. And every Turn is actually comprised of what the opponent and we will Play.

This suggest the following sum type to model each player's action

    data Play = Rock | Paper | Scissors
              deriving (Show,Eq,Enum)

with the natural relation

    beats :: Play -> Play -> Bool
    Rock     `beats` Scissors = True
    Scissors `beats` Paper    = True
    Paper    `beats` Rock     = True
    _        `beats` _        = False

As mentioned before, the scoring rules given in the problem's description assign specific values to each Play. When a Haskell sum type has an Enum instance, a bijection with the Int datatype is created, counting from 0. So, for scoring as per the problems description we write

    valueOf :: Play -> Int
    valueOf = succ . fromEnum

A Turn is nothing more than a pair with Plays from the opponent and ourselves. Instead of using a plain tuple, I chose a product type with record notation for simplicity

    data Turn = Turn { they :: Play
                     , we   :: Play
                     }
              deriving (Show)

I was able to complete the scoring rules by writing

    score :: Turn -> Int
    score turn | they turn == we turn        = valueOf (we turn) + 3
    score turn | they turn `beats` we turn   = valueOf (we turn)
    score turn | we turn   `beats` they turn = valueOf (we turn) + 6

This combines the valueOf our Play for the Turn, with the offset depending on the outcome of the turn, thus solving (2)

Parsing the input

I built my parser using pure list manipulation functions, step by step using the REPL. It went like this

    ghci> f <- readFile "input.txt"
    f <- String
    ghci> take 3 $ lines f 
    ["A Y","B Y","B Z"]

Using lines gives us a list with each line; I always use take so I can look at the beginning instead of the whole thing. Now I need to get the first and third letters. Here's how knowing about Arrows makes it easy to write.

Granted operator

    (&&&) :: Arrow a => a b c -> a b c' -> a b (c,c')

has a strange signature when you look at it. Regular functions happen to be arrows, so the above operator, in the context of plain functions can be thought of as

    (&&&) :: (b -> c) -> (b -> c') -> b -> (c,c')

That is, if you have two functions that can work on the same domain type, this operator gives you a new function that operates on a single argument producing a tuple with both results, e.g.

    ghci> ((*2) &&& even) 21
    (42,False)

Back to our parsing problem, I need the first and third character from each line. So I write

    ghci> take 3 $ map ((!! 0) &&& (!! 2)) $ lines f
    [('A','Y'),('B','Y'),('B','Z')]

Neat!

The last step is to actually use our Turn type. At this point I was about to write a simple helper function with signature

    (Char,Char) -> (Play,Play)

or even

    (Char,Char) -> Turn

but my laziness kicked in.

Recall that you don't know about Part B until you've successfully completed Part A. While reading the latter, I had a hunch that the «meaning» of the second column was going to change for Part B. Instead of «hardcoding» transformations inside the parsing code, I decided to abstract it in the form of a higher order function:

    playWith :: ((Char,Char) -> (Play,Play))
             -> String
             -> [Turn]
    playWith style = map (tupleToTurn . style . (!! 0) &&& (!! 2)) . lines
      where
        tupleToTurn (t,m) = Turn t m

so I could pass any function style able to «change the meaning» of the second column, before turning it into an actual Turn.

Back to solving Part A

For Part A, we're told to interpret A, B, and C, as Rock, Paper, and Scissors, respectively, and to interpret X, Y, or Z, as Rock, Paper, and Scissors, respectively.

This results in

    charToPlay :: Char -> Play
    charToPlay c | c == 'A' || c == 'X' = Rock
    charToPlay c | c == 'B' || c == 'Y' = Paper
    charToPlay c | c == 'C' || c == 'Z' = Scissors

Recall the function

    playWith :: ((Char,Char) -> (Play,Play))
             -> String
             -> [Turn]

where the first argument is the transformation we want from our source tuple to the actual Play interpretation. That is, we need to write a function with type

    (Char,Char) -> (Play,Play)

But we already have charToPlay! We would need to apply it to each argument of the tuple, and produce the result as a tuple. That is something that you can definitely do via pattern-matching, and that's left as an exercise to the reader. Arrow makes it very easy, thanks to operator

    (***) :: Arrow a => a b c -> a b' c' -> a (b, b') (c, c')

which, in the context of functions, can be thought of as

    (***) :: (b -> c) -> (b' -> c') -> (b, b') -> (c, c')

This allows us to write

    turns :: String -> [Turn]
    turns = playWith (charToPlay *** charToPlay)

and now partA is complete!

How does that help with Part B?

Once Part A was solved using the preceding implementation, the specification for Part B became available. We're told to keep interpreting A, B, and C as they were in Part A, but the meanings for X, Y, and Z have now changed. Instead of being interpreted as what to play, they are to be interpreted as the desired outcome for each round: an X means «play to lose», Y means «play to draw», and Z means «play to win».

So I wrote a couple of helper functions for figuring out how to win or lose to a particular Play,

    toBeat :: Play -> Play
    toBeat Rock     = Paper
    toBeat Paper    = Scissors
    toBeat Scissors = Rock

    toLose :: Play -> Play
    toLose Rock     = Scissors
    toLose Paper    = Rock
    toLose Scissors = Paper

Now, the solution for partB becomes

    partB = sum . map score . predict

    predict :: String -> [Turn]
    predict = playWith fix 
      where
        fix :: (Char,Char) -> (Play,Play)
        fix (t,c) = let they = charToPlay t
                    in case c of
                      'X' -> (they, toLose they)
                      'Y' -> (they, they)
                      'Z' -> (they, toBeat they)

passing to playWith a helper function that «fixes» the Turn to have the desired outcome.

Conclusion

There are many Haskell standard type-classes that have somewhat obscure names. Instead of focusing on their names, or trying to fathom their arcane origins in Category Theory, it's way better to know their practical use cases. fmap over Functor, bimap over Bifunctor, mconcat on Monoid, and Arrow combinators in function contexts are some of the more useful ones. You find them everywhere and anywhere...

The use of higher-order functions, such as playWith, is pervasive in functional programming (think of map, foldr, etc.). It's one of the basic techniques that help build reusable components. Instead of hard-coding functionality to an unrelated process, try abstracting it out and creating higher-order functions.

Granted, I did this «on a hunch» before knowing what Part B was about, but then again, this is not my first rodeo.