April 16, 2018

Real World Haskell - Chapter 10

These are my notes on chapter 10 of the book "Real World Haskell".

Chapter 10 is legendary, where the book entirely runs off the rails with what should be a simple topic, namely writing a parser for the header line of a PGM file. The chapter plunges headlong into a variety of topics the relevance of which only becomes clear in later chapters. Chapter 14 in the book should really follow chapter 10 (probably the book should be rearranged so chapter 10 becomes chapter 13). What is going on in chapter 10 is that the author is inventing Monads, without telling you so and frightening you up front. You don't learn this until you read chapter 14 (which is pretty good honestly).

I don't think any newcomer to Haskell stands much of a chance learning anything from this chapter. The writer sets out to run a marathon and simply shouts over his shoulder "try to keep up".

The chapter plunges headlong into a variety of topics, entirely without explanation. The relevance of the bizarre things the author does in chapter 10 only become clear in later chapters, particularly chapter 14. Chapter 14 in the book should really follow chapter 10. The book should be rearranged so chapter 10 becomes chapter 13.

What is going on in chapter 10 is that the author is inventing Monads. But he isn't telling you this, for fear of frightening you. What he does though is far worse. My advice is this: skim both chapter 10 and chapter 14 first. Get a grip on a basic skeleton of what Monads are about, then tackle chapter 10, with the help of this page and the following excellent set of notes by Ulas Tuerkmen.

The PBM file

This is trivial, and the parser is really just picking apart what could be a single line of text that looks like this:
P5 3072 2048 255
We just have a two byte "magic number" to start and identify the file, followed by 3 numbers, then a big blob of binary data follows. The numbers can be separated by any amount of whitespace (inluding newlines). However only one whitespace character is allowed between the last number and the binary blob, so a bit of care is required here. Here is a quickly thrown together ruby script to parse a PGM file:
#!/bin/ruby

# Let's see how much trouble it is in ruby to
# parse a PGM file and compare to the ch10 Haskell code.

def error ( msg )
    puts msg
    exit
end

pgm_file = "test.pgm"

file = File.open(pgm_file, "rb")
contents = file.read
file.close

stuff = contents.split " ", 4
error "Not a PGM file 1" if stuff.size != 4
(magic,w,h,rem) = stuff
error "Not a PGM file 2" if magic != "P5"

# After the "max" value, we get only a single whitespace
# character, so we can't just include the max value in the split above.
md = /(\d+)\s/.match rem
max = md[1]
off = max.length + 1
bits = rem[off,rem.length-off]

print magic + " #{w} by #{h}, max = #{max}, size = #{bits.length} bytes\n"

# THE END
It is worth bearing in mind this simple imperative program as you plow through the complexities of chapter 10. It makes a person wonder if Haskell really is suited for doing real world programming.

The complexity in chapter 10 comes in by the use of "Maybe" to perform a basic sort of error handling. This is improved later in the code by changing the use of "Maybe" to "Either" to allow an error message to be propogated rather than a simple notion of failure. The trick then is to propogate the possible "Nothing" that can arise at various states of the parse and return it as the result.

As it turns out, both Maybe and Either are instances of the much feared Monads that you hear so much about. I find it much easier to understand chapter 10, or at least to fathom why the writer does some of the things he does by understanding some basic things about Monads up front. You can get all this by reading parts of chapter 14 before diving into chapter 10, but here is a basic orientation

Monad 101

Without freaking out over this scary new term, lets look at monads in a general "black box" sort of way. Here is a condensed description of a monad from page 328 of the book. It is pertinent to use Maybe given that chapter 10 begins with this monad. And if you didn't already know, "Maybe" is a monad, and probably the simplest non-trivial one.
  1. We have a monad constructor. Or perhaps we should just call it the name of the monad. "Maybe" would be our example here.
  2. We have a monad "injector" that wraps some value and makes a monad out of it. The example here in the case of the Maybe monad would be "Just a".
  3. We have a monad chaining function that passes monads from one function to the other.
The whole purpose of the first two examples in chapter 10 is to invent and motivate the idea of a monad chaining function. The book uses the function ">>?" in the revised parser as shown on pages 238-239. The final example in the book uses "==>&".

Notice that nothing has been said here about what monads are in any general or precise way. The goal in chapter 10 would seem to be to get some experience working with monads without realizing you are doing so. Sort of like tricking a child who has a mental block against eating vegatables into eating his beans.

We really should stop here and resume discussion of chapter 10, but it seems pertinent at this point to talk about some standard monad jargon that you are bound to run into.

  1. There is a Monad typeclass that is part of the standard Haskell prelude.
  2. The standard name for the injector function is "return". Return has the type a -> ma
  3. The standard name for the chaining function is ">>=" of all things. It is called "bind" (though I agree that "chain" would be a better name).
  4. The Haskell "do" statement is a built in sublanguage just for monad chaining. You can chain monads with "do" without typing ">>=" all over the place, but we are getting ahead of ourselves here for sure.

The PGM parser, version 1

Here is a test PGM image along with my implementation of the code from page 237 in the book, with various supporting routines. My code is a complete runnable program. I like to use runhgc and treat Haskell like I do perl or ruby. I strongly dislike using an interactive thing like ghci, but your mileage may vary on this.

This is their example of bad code that needs to be improved. One mystery here that becomes an interest of mine is the absence of a skipSpace routine which appears in their next example. If you look closely at the matchHeader routine, you will see that it embeds skipSpace and each call to getNat has the skipSpace code on the line that calls the function. Curiously after reading "max", we do not call skipSpace, but use getBytes to skip a single character. No explanation of this.

Also I find the call to "fromIntegral" curious, perplexing, and needless, but perhaps they have their reasons. An explanation would be nice.

The PGM parser, version 2

Here is my rendition with some tweaking of my own. I ripped the embedded skipSpace out of matchHeader and add an explicit skipSpace to the chained list. I also changed the return type of matchHeader to include an integer value, so it matches types with the other routines and doesn't require the "(getNat . snd )" construction used in the book. I have to wonder if all the lambdas could be eliminated if the chained functions all had compatible type signatures.

Ulas points out that there is some dishonest trickery going on in this code involving the lambdas. The book makes a lame excuse for them, but actually they are vital to pull the height, width, and other parsed items out of the code chain so they will available for the final call to the Greymap constructor. Ulas also points out that these lambdas are not complete on one line, but all are essentially dangling. A more honest rendition of the code with truthful indentation would be:

parseP5 s =
    matchHeader (L8.pack "P5") s >>?
    skipSpace                    >>?
    \(_,s) -> getNat s           >>?
      skipSpace                    >>?
      \(w,s) -> getNat s           >>?
        skipSpace                    >>?
        \(h,s) -> getNat s           >>?
          \(max,s) -> getBytes 1 s     >>?
            (getBytes (w*h) . snd)       >>?
            \(map,s) -> Just (Greymap w h max map, s)
This is a subtle but very significant aspect of this code. If this isn't clear to you, ponder how the value "w" makes it from the pattern in the lambda that matches for it to the Greymap call. Read what Ulas has to say -- and try his experiment of wrapping one of these lambdas in parenthesis. The business of how Haskell operators bind and precedence can be a confusing and tricky subject, as it is here.

Here each lambda runs all the way to the end of the parseP5 function, and it must for the variables like w, h, and so on to be available to the Greymap call.

I have another nit to pick as well. In a well written parser, the skipSpace routine ought to return Nothing if it fails to find whitespace. This would be a fairly trivial code change.

The following is a yet more simple parser that plucks apart a line with two numbers. It fixes the skipSpace routine so it ensures that there is whitespace. It still uses the same brittle lambda chaining as does example 2 in the book. It uses the standard Monad bind operator >>=. My attempts to rewrite this using do syntax have failed thus far.

Monad reflux

In chapter 14 (page 336) the writer claims to refute a number of Monad misconceptions. His statements seem almost humorous, here is the list.
  1. Monads can be hard to understand.
  2. Monads are only useful for IO and imperative coding.
  3. Monads are unique to Haskell. He claims otherwise.
I will grant him number 2 as a misconception, Denying the rest is outright lies and deception, perhaps even self-deception on the part of the author. Monads are hard to understand. If chapter 10 is supposed to convince us that they "fall out naturally" I have a bridge for sale that I want you to look at. I have been programming for nearly 40 years in every language known to man, and never ran into Monads or anything even vaguely like them until I began learning Haskell.

I would even go so far as to say that Monads are the very heart of Haskell, for anyone who wants to go beyond the basic level. You can't even write "hello world" without tripping over the IO monad. Monads are quite unique to the Haskell language. Getting comfortable with Monads separates the men from the boys; the neophytes from the veterans; the dogs from the cats; the bulls from the frogs.

The PGM parser, version 3

After example 2, with its various warts, the book runs off the rails and into the weeds followed by a large cloud of dust. The information on functors is nice, but while reading it I find myself wondering what it is doing in the midst of this chapter on PGM parsing. There is a reason of course, but it would be nice to have a hint of it up front.

Chapter 10 concludes with a third version of the parser, the heart of which is shown on page 252. I have to agree with (and laugh about) what Ulas says about this code in his commentary on chapter 10:

To be perfectly honest, this is some of the ugliest and most incomprehensible code I have ever seen, and I don't understand how it could be an improvement over anything.
My approach was to type in code, beginning with the parseRawPGM function on page 252 and then keep hunting down missing pieces and adding them, studying them as I went. I used code Ulas wrote as the inspiration for my main and wrapper and kept fixing errors and typos until I had something that worked. There are a huge number of new and poorly explained concepts here and they are worth the time to study.

State

The trouble begins pretty much with the following "newtype" statement. The only explanation in the book is that this is introduced to encapsulate access to the Parse state. There is actually a lot more going on here, and the whole business merits several chapters on its own.
newtype Parse a = Parse {
        runParse :: ParseState -> Either String (a, ParseState)
}

Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org