In a post from top of the year I mentioned that I was looking into Haskell programming language, and promised to give some insight on how it fares compared to others. Well, I think it’s time to deliver on that promise, for the topic of this particular language – and functional programming in general – is indeed very insightful.
I do not claim to have achieved any kind of proficiency in Haskell, of course; I might very well be just scratching the surface. However, this is exactly the sort of perspective I wanted to employ when evaluating language usefulness: a practical standpoint, held by programmer who is looking to use it for actual tasks, without having mastered it in great detail – at least initially. This is, by the way, a pretty common setting when tackling all new things related to coding, be it frameworks, software platforms or languages.
Still, I knew it was almost supposed to be a rough ride. A language whose tutorials purposefully shy away from classic Hello World example (typically inserting a factorial function instead) looks like something designed specifically to melt brains of poor programmers who dare to venture forth to investigate it. Those few who make their way back are told about in folk tales, portrayed as mildly crazy types who profess this weird idea of “purity”, and utter the word ‘monad’ with great contempt.
Okay, I may be exaggerating… slightly. But there is no denying that Haskell attained a certain kind of reputation, something like a quirky cousin in the big and diverse family of languages. Unlike Lisp, he isn’t picked on due to any (particular (and (rather) obvious)) property. There is just this general aura of weirdness and impracticality that he supposedly radiates, repelling all but the most adventurous of coders.
If it really exists, then pity: it certainly failed to repel me. As a result, I may have a story (or two) to share with those who want to learn what’s this functional business is really about, and why should they care.
Grab a chair and something to drink – it’s going to take a while. But in the end, you shouldn’t regret staying.
Regardless of what prior knowledge you may already have, it is certainly useful to begin with the basics.
Haskell is a general purpose, compiled, statically (and strongly) typed, purely functional programming language with lazy evaluation, expansive and modular standard library, a rich repository of external packages, support for all major operating systems via the GHC compiler, and peculiar ability of making blogposts sound like Wikipedia articles. It doesn’t seem to have a single creator (in a way that, for instance, Python has Guido von Rossum), but its namesake is Haskell Curry, a 20th century mathematician and logician.
As for popularity, it currently sports a not-very-impressive 42th place on the Tiobe Index – notably above Go, Clojure, Dart and Scala, and unsurprisingly below most languages you would think about. GitHub currently hosts around 6700 repositories whose projects use Haskell in any way – compared to, say, about 80,000 for Python, 40,000 for C and 160,000 for Ruby (yikes!). A little below 6000 questions tagged haskell has been asked on StackOverflow, putting this tag on the same page as boost, jquery-plugins, version-control and soap (eww!).
So it goes without saying that Haskell seems to be rather hip…, ahem, niche language. In the programming world, this should be considered a liability, since larger community around a language naturally results in more third-party libraries, open source projects and yes, job offers. On the other hand, we know the old saying about quantity triumphing quality, which we are inclined to believe at least in some cases (*cough* Java).
If not popularity, then what are the Haskell’s immediately recognizable treats that could appeal to newcomers?… For one, it’s the GHC package, which stands for Glasgow Haskell Compiler. Easily available on any desktop platform, it’s not only a compiler but also an interpreter which can run scripts (via runghc command), and act as a REPL (Read-Eval-Print Loop) for quick experiments (via ghci). That last thing is especially important, as I’ve grown to perceive languages that refuse to be poked at with their own shell as just plain barbaric. Haskell proves it’s entirely possible for a compiled language to offer sensible, efficient and useful REPL.
What about writing real code? As for actual programs, they are organized into modules (.hs files) which form a nice hierarchy of namespaces, just like you would expect when coming from Java, .NET, etc. One neat thing here is how external packages fit into the whole picture. Rather than rolling out an independent root namespace for every custom package, convention orders to place third-party modules within existing tree. As a result, stuff related to networking goes always into
Network namespace, text utilities (e.g. parsers) into
Text, data structures into
Data, OS stuff into
System, and so on.
On more local level, the most basic unit of code structure is of course a function. Haskell functions are slightly different than usual, because they are required to be referentially transparent: they must always return the same value for the same arguments. Also, they cannot have the so-called side effects, which excludes pretty much anything outside of actual computation – most notably I/O.
How this can be reconciled with being able to, well, do anything practical shall be left for now. (Some may know that it’s about the infamous M-word). What I’d like to point out instead is that a function in Haskell is just a name for (parametrized) expression. If you’re a Python programmer, you can imagine functions being always defined with
lambda rather than typical
def. Because there really isn’t much you can cram into a single expression, you are basically required to aggressively split things into smaller and smaller bits, using
where syntax. Here’s a simple example of such splitting, done for a function that is already pretty trivial – it just formats an URL:
Hopefully I don’t have to elaborate overmuch on how this is a good thing, to have your code organized into many small, manageable parts that clearly relate to each other. In Haskell, this is almost the only way you can actually produce anything, which is both marvelous and scary at the same time. There are simply no excuses for quick & dirty hacks: refactoring happens while you are coding, not afterwards.
Besides algorithms (functions), we know that programs are composed of data structures, and in case of Haskell they translate into data types. Quite amusingly, if we don’t count aliases, there is just one sort of types that can be defined here. It’s called algebraic data type (ADT) and it’s entirely sufficient, because it simply has everything. And by that I mean literally everything, because it allows to define stuff like enums:
as well as structures (records):
Not only that – it also permits types to have parameters (with constraints):
which easily satisfies the definition of ‘template’, ‘generic type’ and ‘concept’ from other languages. And if you haven’t noticed, we just combined half of semantics of C and the most powerful feature of C++ into one syntactical construct – and rather simple one, too. I don’t know about you, but for me it just oozes awesomeness.
The perks of Haskell type system are not limited to ADTs, though. As I have mentioned earlier, Haskell is a compiled and statically typed language. For many programmers this may bring immediate associations with cryptic error messages and pointless things littering the code just to please the compiler. In reality, static typing was always meant as a tool to reinforce code correctness; but often, it just didn’t seem to bring enough benefits to justify the headaches involved.
Haskell appears to be offering a proper balance. Because it’s very good at figuring out the types by itself (via type inference), it seldom requires explicit type declarations. (They are still recommended for top-level functions, though). And since the type system is extremely powerful, it can often serve as a sufficient proof of program’s correctness. Basically: if it compiles, it has pretty high chance of working – much higher than in other statically typed languages, at least.
To be honest, this happens not only due to elaborate mixing and matching of types, but also because of referential transparency. Constraining the class of possible actions that a particular piece of code can undertake goes a long way towards preventing errors. Furthermore, it greatly simplifies the testing process, up to the point of rendering many typical kinds of unit tests obsolete. Indeed, Haskell’s testing facility known as QuickCheck can generate test cases automatically, based on a set of properties that we expect our functions to hold.
Going through all this features and learning about those wonders of modern technology, you might be left thinking: where’s the trick here?… It’s wise to not let yourself be caught off-guard, for indeed there are some downsides. Several of them, actually.
An obvious one seems to be the functional paradigm itself, especially if coming from a language that wasn’t significantly influenced by FP concepts. However, the pool of languages that still lack at least basic functional features is steadily shrinking. A relatively recent example could be C++, which has just gained a superior support for closures and functions as first-order values, supplementing its already existing capabilities. Others – like popular scripting languages – are usually going even further in their support for functional style.
So if you have done some serious coding in any of those languages, you had good chance of seeing the elements of FP in action. Heck, there is even a non-zero probability that you have used them, which should definitely help with cushioning the Haskell’s blow. And if structuring your code using maps, folds, pattern matching and guards turns out to be insufficient, there is always a place for plain ol’ loops. Sure, they are called “tail recursion” and may not have an obvious 1:1 correspondence to iterative constructs from imperative languages, but they are close enough to work in every reasonable case – if only somewhat inelegantly.
Unfortunately, getting past the initial obstacle and learning to frame your thoughts into purely functional code is just a start. See, on some level Haskell can be viewed as conglomerate of several sublanguages, defined by functions and operators applicable to specific typeclasses. A typeclass is rather simple concept here; it can be explained as slightly more powerful version of an interface from traditional OOP. Concrete types can be instances of one or more typeclass, just like “normal” classes are said to be implementing interfaces.
In Haskell, however, some typeclasses are so influential that their operators and functions essentially dictate the overall shape of a piece of code. Therefore, you can often hear about few different “styles” of Haskell code, and the most prominent ones seem to be: applicative, arrow-based and monadic style.
When writing any more complicated piece of logic, you are generally required to decide early on which style you are going to use. The choice is often non-obvious, as the foundational bases behind different styles are closely related, often through inclusion (like applicative functors and monads). Each style has a distinct “feel” to it, though, and correspondingly, it looks noticeably different in actual code. And while it’s entirely possible to mix several styles together, it requires great care if result is to be in any way comprehensible later on.
At this point it’s quite natural to ask: what are those things anyway, those functors, arrows and *deep breath* monads?… Actually, I could add a few more to the mix. Meet categories, foldables, traversables, monad transformers and monoids; I’m sure it’s a beginning of a long-lasting friendship!
Jokes aside: the answer is both simple and complicated. These are abstract concepts, originating mostly from category theory and pertaining to either computational processes, or entities that those processes are operating on. Note that by ‘abstract’, I mean real, mathematical abstraction – not the flamboyant, object-oriented stuff that’s making wimpy coders shudder because they’ve seen too many service provider factories. That’s just flexibility at best, and complexity at worst – but nothing there requires cracking big, mental roadblocks in order to understand.
With terms I’ve listed above, it’s entirely different situation. Sure, they are all built from quite simple, elementary building blocks: functions, their combinations and parametrized data types. But somewhere along the way up, there is always an initial mental gap which has to be filled with our own, individual analogies that allow us to grasp these abstract concepts. Those analogies are likely specific to given individual and might very well be impossible to put into words. Because of that, there is no royal road to enlightenment: one must simply bash their head against those ideas long enough for those helpful analogies to finally emerge.
Oh, and don’t think you’ll be able to communicate them to others. It’s been tried enough times as to attain a dedicated term: the monad tutorial fallacy.
You may argue – and be absolutely correct – that this situation isn’t really specific to Haskell or functional programming. Indeed, it happens when we start to tackle any new programming paradigm, and especially when just learning to code for the first time. But many of us simply aren’t used to be at that level anymore, especially if their last time was few years ago or even further in the past. Thus, in a sense, taking up Haskell often begins with a lesson in humility. Hardly a pleasant experience, but apparently a necessary one.
I would be totally fine with intricate semantics, though, if things that are obviously fixable weren’t getting in the way so very often. One of those things is the miserable excuse for “documentation” that many Haskell packages are being shipped with. And I mean packages with absolutely crucial functionality, such as regular expressions’ library or – my personal nemesis – JSON parser package. In the latter case, the documentation is so overwhelmingly useless that I had to spend better part of an hour just to figure out any kind of starting point to work my way from. Then, fleshing out the details for a final parsing logic of a a very simple project took more than one evening.
To emphasize: I’m talking about handling JSON here, a bread and butter of most contemporary applications that have anything to do with the Internet. This is something that should be as trivial as putting a string together, or sorting a collection via custom comparator. It’s not like there aren’t any precedents here, showing that it is entirely doable, and thus a reasonable thing to request from a language or its standard library. Yet it is apparently not the case in Haskell, which is just shameful and sad.
Naturally, this isn’t something that cannot be amended, and I hope for Haskell packages’ docs to steadily improve over time. Besides, hiccups like that are not nearly enough to dispel the irrefutable “magic” that Haskell emanates – although admittedly, not every shade of it is something to be satisfied with either.
Surely, there’s plenty of magic of the Arthur C. Clarke type. Some of this good stuff I briefly touched upon in the beginning, but there’s a lot more things worthy of attention here. One of those may be an out-of-the-box concurrency, which allows to automatically parallelize computing of pure code. Combined with support for software transactional memory (STM), it can turn hard problems of concurrent programming into something much more manageable.
But unfortunately, it seems that the other kind of coding magic – of bad type – is also abundant in Haskell. It’s the magic of doing a whole lot of stuff behind the scenes, out of programmer’s sight. It is present in the very idea of lazy evaluation, resulting in detachment between the place where expression is put in code and the actual moment where it is computed. This may sometimes result in unwanted performance implications, forcing us to use
seq or bangs (e.g.
$! for function application) in order to regain some control over evaluation.
Hidden magic can also lie in syntax, obscuring the details of what particular piece of code really does. Sometimes it might even allow for one snippet to do several totally different things, as if you had overloaded the semicolon “operator” in C-like language. ‘Programmable semicolons’ is actually one of the many epithets associated with monads, and the reason for that can be easily seen in the following function
If we execute it within context of
IO monad, we will get rather predictable result. (And, as a bonus, we’ll finally see that I/O in Haskell is very possible):
But if we use
f on two lists, we will get something entirely different:
By some inexplicable sleight of hand, the function has been executed six times, resulting in a Cartesian product of its arguments. For an untrained eye, this has to be really bizarre sort of polymorphism, with no obvious explanation (as I imagine that “list is also a monad” isn’t nearly enough).
You may be blinking now, and wondering: who could have used that in real code, anyway? After all, it’s an incredibly effective technique to rapidly decrease the quality of code, as measured by the one and only, true metric. Surely, real haskellers must have much more sanity than that, right?…
By the way, did you notice the names I’ve used in the preceding samples? Not counting library functions and mandatory
main, they were all one-letter names:
y, etc. I’m sure you have thought it’s only because this is just an example, unlikely to be replicated in actual code. I must therefore inform you right away: you’re greatly mistaken.
Using one-letter identifiers looks like a long-standing Haskell tradition, up to the point of utter confusion. When
f can stand for both function or functor (not nearly the same thing);
m could be a monoid or monad;
t may be traversable, monad transformer or just an arbitrary type – it’s hardly surprising to find yourself staring at piece of code in dazing bewilderment.
It won’t be only because of cryptic identifiers, though. I noticed that haskellers exhibit a strange fondness to very terse code, dubbing this property “expressiveness”. For instance, the following function could be considered relatively typical:
Notice how this function must take an argument (directory’s path):
yet there is no identifier that would stand for it in function’s declaration or body. It is defined entirely in terms of applying some combinators to functions treated as first-order values, which is often referred to as point-free style, and greatly encouraged. Since there exists plethora of different combinators, this technique can be – and often is – taken really far, exceeding the readability threshold of all but the most seasoned hackers.
This is called “code golf” – a term I’ve heard for the first time only when diving into Haskell. Frankly, other languages are much more honest when talking about similar practices.
So, is Haskell beyond salvation?… Hardly. In terms of applications, few areas are already explored very well, with concurrent programs, distributed computing and web programming (yes!) being probably the most important ones. Some others are still relatively pristine, and some can be placed in between: where serious effort is being made, but there aren’t yet any universally acclaimed solutions and practices (e.g. programming graphical UIs).
But despite obvious capabilities of performing very practical tasks, Haskell ecosystem as a whole still appears to suffer from the ivory tower syndrome. There is little doubt that learning Haskell is an overall mind-expanding experience, ultimately leading to better code in any language you choose to write in. However, using Haskell for everyday programming tasks is often striding through unknown territory, exploring strange new paradigms, seeking out new techniques and methodologies, to boldly go where no monad has gone before.
What’s up, I read your blogs like every week. Your writing style is awesome, keep doing what you’re doing!