So I’ve been programming in Haskell a lot recently, and thought I’d just share some of the random observations I’ve had about the language. The book Real World Haskell was what really pushed Haskell over the top for me, from “a language I should probably learn someday” to “a language I am learning and writing real code in”. Seriously, if you’re interested in Haskell, get this book.
There’s no over-arching theme or structure here, just a collection of random asides, none of them really worthy of blog posts by themselves. So if you’re looking at the end of the post and going “is that it?”, yup, that’s it.
Haskell is a big language. Java is also a big language, but it’s a big language in a different way. Java is big like west Texas is big. There’s a lot of it, but there is something of a dreary sameness to it. Haskell is big in the way that Manhattan is big- which is to say that it’s actually smaller than it looks, but there is just so much going on, all right on top of each other, that it feels bigger than it is in an absolute, west Texas sort of way. In west Texas, you can travel hundreds of miles and still not really have gone much of anywhere- in Manhattan, cross a street and you’ve gone from (Little) Italy to China (town). Walk a couple of blocks that-a-way and suddenly you’re in SoHo. It’s an information-density thing.
Haskell is about code combinatorics, making lots of little things that can be combined together into bigger interesting things. Monads, and variations on monads and extensions to monads and operations on monads are the primary way Haskell combines code- but the core idea is combining code. Which is why Haskell has a Manhattan-like super-dense nature- just a few lines of code can create, not just a whole new combination, but a whole new way to combine code. Which then heterodynes with all the other ways of combining code. And which changes, not only what exists, but what can exist (or at least, your perception of what can exist).
Haskell and Lisp are really opposite ends of a spectrum. In Lisp, all code is data. A function is nothing more than a list of operations to perform. This leads naturally to the concept of macros- code that inspects and modifies a list of operations just like you would for any other list. In Haskell, it’s the other way around- all data is code. Part of this is lazy evaluation, you don’t have an int, for example, what you actually have is a closure, a hunk of code, that when executed, will produce an int. But this philosophy goes deeper than that- Haskell is constantly blurring the distinction between data and code.
An example of what I mean here is difference lists, which may be common in Haskell but I didn’t know of them before (thanks, RWH). Appending to the end of a list is expensive, even with lazy evaluation- you end up having to reallocate the whole list, just later in time than in Ocaml. A difference list allows us to append cheaply. The public API might look something like:
type Difflist a empty :: Difflist a append :: Difflist a -> a -> Difflist a toList :: Difflist a -> [ a ]
It’s a Difflist holding some type a, I can start with an empty Difflist, append elements to my hearts content, and when I’m done, convert the whole thing to a list. Looks like a data structure to me. Acts like one too. But what it actually is, you might have guessed from context, is a function- specifically, it’s a function that, given the tail of the list, prepends all the earlier elements onto the tail. When we say “append this element”, we really just update the function to prepend the new element. The code makes this (somewhat) more clear:
type Difflist a = [a] -> [a] empty :: Difflist a empty = id append :: Difflist a -> a -> Difflist a append lst x = \tail -> lst (x : tail) toList :: Difflist a -> [ a ] toList lst = lst []
The key trick is in append- we are creating a new function which first prepends the given element onto the tail of the list before calling the original function to prepend all the other elements.
Yeah, there are probably better ways to do it- for example:
type Difflist a = [ a ] empty = [] append lst x = x : lst toList lst = rev lst
But the important point is the blurry line in Haskell between data and code, in that even data structures can really be code under the hood. This leads inevitably to monads, the ultimate (or at least ur-) data structure and/or code block. This, of course, leads to a lot of confusion around monads, as people keep trying to figure out is code or is it data, when it’s both.
One thing that does annoy me about Haskell- naming. Say you’ve noticed a common pattern, a lot of data structures are similar to the difference list I described above, in that they have an empty state and the ability to append things onto the end. Now, for various reasons, you want to give this pattern a name using on Haskell’s tools for expressing common idioms as general patterns (type classes, in this case). What name do you give it? I’d be inclined to call it something like “Appendable”. But no, Haskell calls this pattern a “Monoid”. Yep, that’s all a monoid is- something with an empty state and the ability to append things to the end. Well, it’s a little more general than that, but not much. Simon Peyton Jones once commented that the biggest mistake Haskell made was to call them “monads” instead of “warm, fluffy things”. Well, Haskell is exacerbating that mistake. Haskell developers, stop letting the category theorists name things. Please. I beg of you.
If you’re not a category theorists, and you’re learning (or thinking of learning) Haskell, don’t get scared off by names like “monoid” or “functor”. And ignore anyone who starts their explanation with references to category theory- you don’t need to know category theory, and I don’t think it helps.
I’m becoming very, very fond of lazy evaluation. Lazy evaluation is actually very common in the computer world, even outside of Haskell, people just don’t recognize it as such. Any time you’re writing code to produce some complicated structure on an as-needed basis, or short-circuiting a calculation or stopping a calculation early when some condition is met, you’re doing lazy evaluation. You’re just reimplementing what Haskell gives you for free.
Yes, lazy evaluation does lead to space leaks, but most every other language has problems just as bad, so if you’re using that as an excuse to write off Haskell, then also write off Java, C++, Ruby, etc. You disagree that, say, Java doesn’t have a problem that bad? Try null pointer exceptions. The difference is that Haskell is collecting a large tool set to address the issue of memory leaks, to help the programmer find and fix them. Real World Haskell has a whole chapter on the issue, how to profile your code and get at least some hints of where things are going wrong.
A while back I published a short screed saying that we needed both unit testing and static typing, and probably documentation as well, to produce correct, changeable (aka maintainable) code. Haskell is going a long way to proving me correct. First of all, Haskell has introduced (at least as far as I know) an interesting idea into the world of unit testing, which is quickcheck (although since then, the idea has been copied elsewhere). The idea is simple: define a generic way (using monads, natch) to generate random instances of some data structure, which you can then test various properties of. For example, you might test that, for all instances of a data structure, printing the data structure out (converting it to a string) and reading it back in again results in the same data structure. Automated code can then go through and generate a whole bunch of random structures and test to make sure the property holds. Static typing gets you a long way on the cheap, but it doesn’t do everything. The quickcheck infrastructure also documents the invariants that should hold, and thus are an aid for maintainers down the road.
Documentation is another interesting area in Haskell. In the above post, I talk about the importance of documentation to maintainability, and how I often even use documenting as a debugging technique. Well, Haskell is the first language I’ve used where literate programming makes sense. In normal programming, comments are marked out as special, maybe by a /* or — or some other character combination that says “the following isn’t code, it’s comment”. Everything not marked as comment is assumed to be code. With literate programming, it’s the other way around- it’s code that is marked out special (generally by putting a > sign at the beginning of lines of code), and anything not marked as code is assumed to comments. Generally, not just ordinary plain-text comments either, generally literate code comments are assumed to be latex.
Ignoring whether you like to dislike latex (and I happen to like it), the problem I’ve had with literate programming in other languages is that the comment to code ratio wasn’t high enough to make it worth it. Marking parts of the file as special, however you do it, is overhead. You should be marking the exception parts, not the majority parts. If the file is 70% one thing and 30% the other, you should be marking the 30%, not the 70%. In the other languages I’ve used, 70% or more the program files were code, not comment. In Haskell, it’s the other way around- 60/40 comments/code, if not more. At which point you’ve crossed a great divide, and downhill is the other way- it’s cheaper and easier to mark the code rather than the comments.
Toss in your standard xunit style unit testing, and you’ve got the trifecta of solid code- static type gaurantees, unit testing, plus strong documentation.
I’m not sure I’m on board yet with the whitespace being significant yet. It’s not as annoying as I thought it would be, however, having only really bitten me in one way (for some reason, if I have an if statement in a do block, the else needs to be indented- annoying). But I suspect this is mainly because I’m only one developer, and I’ve turned tabs off in vim (set expandtab). In a larger development group, this could be more of an issue.
Other than allowing the category theorists to name things, the main problem with Haskell being a “research language” that I see is the weak module system. Everything keeps wanting to get dumped into the same name space, so names tend to include the module. So it’s not TMVar.read, it’s readTMVar. Or, if you’re like me, and you tend to qualify your includes, it’s TMVar.readTMVar. Which is annoyingly repetitious, and repeatedly annoying.
A language being able to parse yourself is a huge advantage. It reclaims some of the ground lost to Lisp, and makes it easy to write tools to do various things with code.
Related posts:
Pingback: On keeping category theory and abstract algebra alive in Haskell « Integer Overflow
Pingback: Enfranchised Mind » How Should I Burn My Free Time?