First steps with Haskell

caipre · 4 min read

A few days ago I started working through the Write Yourself a Scheme in 48 Hours wikibook. The book uses Haskell to build a working Scheme parser and REPL from the ground up. As a means of learning Haskell, one should be aware that the book doesn’t describe language features well (read: at all), and the code is a bit dated, to the point where ghci complains about use of deprecated functionality (in this case, Control.Monad.Error). For my purposes — I just want to see the language before really diving in — it gets the job done.

I originally had it in mind to create a pet programming language compiling to LLVM. When talking to friends about that project though, I was referred to the Haskell wikibook. I decided that it was probably worthwhile to see how a real language is parsed and evaluated before spending too much time designing my own language and parser. Haskell has long been on my to-do list, and my friend V’s enthusiasm convinced me it’s worth taking the time to learn.

Well, I was mistaken in understanding the “48 hours” part of the tutorial as meaning “two days”. I guess it really means closer to a week’s time.

On the bright side, I now have a functional REPL for a useful subset of Scheme. Moreover, I have a better understanding of Haskell. There’s still a lot I need to learn, but I thought I’d share some insights.

Making analogies to Rust

Rust is an excellent language, and makes quite a few functional programming concepts available in the context of systems programming. As my language of choice right now, I’ve found that it’s been helpful to understand new Haskell concepts by making an analogy to Rust.

There are some obvious ones:

  • Haskell’s Maybe a is Rust’s Option<T>
  • Haskell’s case ... of is Rust’s match ...
  • Both languages support pattern matching and destructuring

There are plenty of Haskell concepts that I’m still learning.

As far as I understand them, typeclasses in Haskell are analogous to Rust’s traits. For example, the following Haskell:

class Eq a where
   (==) :: a -> a -> Bool

data Foo = String

eqFoo a b = a == b

instance Eq Foo where (==) = eqFoo

…translates roughly to Rust as:

trait Eq {
   fn eq(a, b) -> Bool;
}

struct Foo(String);

impl Eq for Foo {
   fn eq(&self, &other: Self) -> bool {
      self.0 == other.0
   }
}

I don’t know how/if Haskell has a concept of trait objects. I also don’t know much about Haskell’s support for generics, at least in the Rust sense (ie, <T>).

Haskell seems to be able to constrain types, but I don’t grok the syntax yet. In Rust, I can write the following:

fn myfunc<T: Display>(str: T) {
   println!("{}", str)
}

For Haskell, I think it’s something like this:

myfunc :: Show str => str -> String
myfunc str = show str

I might revisit the comparison between these languages as I get more comfortable with each.

Understanding Monads and do notation

Monads might be more subtle than I’m aware of, but for the moment I’m just thinking of them as a container for value that does some additional background work through the >>= and >> operators. I like to think of this as a more functional Unix pipe — the book even calls it a “monad pipeline” at one point.

Consider the following command pipeline: cat /usr/share/dict/words | tee file

I claim that | tee is a monad, where the “additional background work” is to repeat stdin both into a file and into stdout. The key idea is that there’s a value coming into the monad, which is then free to do whatever it wants before passing the value to the next action in the chain.

This “action, extra work, forward” cycle is hidden by the do notation, so it’s useful to know how to desugar the syntax. Consider the following Haskell code, which implements the evaluation of an if statement in Scheme:

eval env (List [Atom "if", pred, conseq, alt]) =
   do result <- eval env pred
      case result of
         Bool False -> eval env alt
         otherwise -> eval env conseq

We can desugar the do to:

eval env (List [Atom "if", pred, conseq, alt]) =
   eval env pred >>= \result -> case result of ...

Essentially, >> can be pronounced as “and then …”, while >>= can be pronounced as “and forward the result to …”.