Custom Search

Sunday, February 26, 2012

An evaluation of Haskell

I've now been writing Haskell code on and off in my spare time for most of a year. I've done small calculations in the REPL, and written a few small utilities. I still don't think I'm an expert in the language, but do think I've gotten enough of it under my belt to evaluate it compared to the other languages I've used.

First comment: I don't think this will ever be an immensely popular language. The syntax is weird. While Perl shows that that alone can't keep a language from becoming popular, it is an obstacle. Second, it's got the worst learning curve of any language I've ever run into. I'll expand on these points where it's appropriate.

The syntax

The syntax is weird, but it's also one of the things that keeps the language terse. Consider this fragment from my analysis of fbs controversies blogs:


getYear :: String -> (Bool, Bool, Bool)
getYear = controversy . sort . (\ (t, r) -> map (getData t) r) . getRows . getTable . parseTags

Note that the first line is a type declaration, and optional here. This is basically a one-line function definition, though a relatively long line. Let's try it in Python:


def getYear(year):
    t, r = getRows(getTable(parseTags(year)))
    return controversy(sorted([getData(t, x) for x in r]))

That uses functional idioms for Python, and is probably how I'd write it if I were writing Python and had that set of tools (chances are I'd have approached the problem differently in Python, though).

Three lines. Shorter lines, yes, but much denser lines as well. And a couple of extra variables. One is required. One I could get rid of by using map(functools.partial(getData, t), r) instead of a list comprehension, but that makes the code longer.

Maybe a more functional language would help. Say Clojure:


(defn getYear
  [year]
  (let [(t r) (getRows (getTable (parsetags year)))]
    (controversy (sort (for [x r] (getData t x))))))

A pretty direct translation of the Python code. Again, how I'd probably write the function in Clojure. Again, with the caveat that I might not use this approach in Clojure. This time it's four lines long, but not quite as dense.

I think the Haskell code is better than either. The weird syntax - which allows for automatic currying and functional composition as an operator - is part of what makes the language powerful. Haskell without those features would be like LISP without macros.

Functional

As hinted at above, Haskell has built-in operators that work on functions. I'm not familiar with any mainstream language that has such builtins. I can see how some might let you add such a feature, as they let you define your own operators, or at least define how the built-in operators work on your objects. But most languages that have functional features just let you pass them around and assign them, not create new ones with operators!

Functional programming in general makes for a steep learning curve, at least if all you're familiar with is object-oriented programming. That Haskell's functional nature is so deep makes it worse, because what looks like operators are function applications, and some function applications do control flow, and you have to learn how to use all of that appropriately.

A shade to much type checking

The debate about dynamic vs. static typing is all about development time. Dynamic typing advocates say that having to actually type in type information - in extreme cases, as many as three times - slows you down. Static typing advocates say that having the compiler check your types saves you time. Both claim that their time savings outweigh the loss from not having the other.

Haskell has both. The compiler infers types from the code, so you don't have to type them in. Well, not very often, anyway. In some cases, the compiler can't narrow the type down enough to generate code, so you have to provide some. The convention is that you write out the type information for top-level functions, which usually meets any such requirements. If that seems onerous, you can load your code into the REPL and ask for the types.

The only place I've found where typing causes problems is with the mixed mode arithmetic. This requires explicit conversions unless everything is either of the same type or a constant. Constants will be inferred to have the appropriate type. So you can say 1 / 2 and get .5, because the types assigned to 1 and 2 can be converted to float, and then divided. You can't say (length [1]) / 2 because length [1] is an Int, and that has to be explicitly converted to float. Once you're used to this, it's not a problem. But the first time you run into it, it's nasty.

Libraries

It has been observed by a number of people that programming is now more about connecting libraries than writing elegant solutions to problems. Haskell has a fine library, with lots of interesting tools in it, including most of the common ones.

In some ways, this makes the learning curve worse. Since control flow is handled by functions, the library has lots of interesting control flow tools in it, as well as the usual collection of data processing tools. Learning those extra control flow tools extends the learning curve.

In fact, there are classes for doing control flow that are so important that the language has syntax to make them easier to use. Learning how to use all of those is another learning curve extension, and one I'm still working on.

On the plus side, the libraries are in some ways easier to explore than those for dynamic languages. There are datebases of the standard library that you can search by type. So if you need a way to apply a function to every element of a list, you can ask for it (for the curious, the query is (a -> b) -> [a] -> [b]) and get a list of available functions from the library that match that. Of course, it does require being able to write Haskell type expressions, but that's a requirement for many things.

My status

Personally, I already had years of experience with a number of functional languages. The functional nature of Haskell didn't bother me. Getting used to operators that are just functions - and a syntax to treat them that way - took a bit of time.

Haskell can be run as a script or compiled into a binary. It fits well into the Unix environment. There are even back ends to generate JavaScript, if that's what you want to do.

On the other hand, the Haskell infrastructure seems to be missing some of the parts required to develop enterprise-size applications. The Hakellwiki page about it provides a list.

I think it's good for a lot of things I want to do. I think that learning more about it will make me a better programmer, and exercise the mathematics degree I haven't otherwise used much. I'm going to keep plugging away at the learning curve, while still keeping an eye open for languages with good concurrency support that might be a bit more acceptable to mainstream programmers.