Why I prefer Scheme to Haskell
Before you sharpen you keyboards and spit acid at me as punishment for the title, let me point out that I haven't written this post to bash Haskell (no pun intended), in fact I love it. Haskell introduced me to algebraic data types, domain specific languages, pattern matching, and many other programming constructs that feel like lost limbs in other languages, but Haskell willfully resists any attempt I make to write useful code with it. Scheme lacks almost all of Haskell's innovations in favour of a minimalistic flexibility, but it mixes a practicality with the functional beauty that makes it Haskell for human beings.
Last weekend I began writing a prototype for a data mining application. I started in Haskell; it is fast, elegant, and once you've used the Parsec module it is tough to imagine writing a parser without it. My first step was to extract data from a straightforward XML file. This is a simple task with ruby: download an xml parser, read a tutorial, and maybe ten minutes later you have the data in memory ready to work with. After a frustrutating hour with my Haskell version I was still struggling to understand the monadic API required to work with the XML parser and I gave up. This has always been my problem with Haskell; it is beautiful, but it is useless for hacking. Scheme doesn't have Haskell's vast standard library or an efficient compiler, but it does have Haskell's terseness, and more importantly it makes coding a joy.
As elegant and appealing as Haskell's purely functional foundation is, it prohibits simple, but crucial, impure tasks such as writing to files and communicating over networks. Monads - Haskell's answer to this deficiency - are little more than a wobbly crutch; they enable coders to write useful software in Haskell, but they make it neither intuitive nor easy.
If you wish to debug a function in Haskell you can't insert a
printf to inspect its inner workings unless that function happens to be on the IO monad. This is a serious barrier to Haskell's usability that is rarely discussed, because you can only fix what you see, and if you can't inspect the inside of a function, you can't fix it. Scheme on the other hand sacrifices a little purity so that you can slap a
(display ) in your code and find out what is going on. I'm sure a Haskell programmer right now has his or her head in his or her hands whilst questioning how I could miss the inappropriateness of this behaviour in a lazy, well optimized language with a debugger. They would be right, but the inability to debug like this is a price I am not willing to pay for Haskell's academic purity.
The second problem with Monads is related to their great strength - they are synonymous with Domain Specific Languages. The mission statement for domain specific languages is stupendous - don't write a complex program to solve your problem, write a simple program in a programming language you have designed solely for that task. I've already mentioned the best use of DSL/Monads - Haskell's parsec module. With Parsec the Haskell function to parse a file is identical to the Backus Naur Form description of the parse grammar - how much clearer could it be? They say that imitation is the highest form of flattery, and every parser function I have written outside of Haskell since meeting Parsec has been a shoddy facsimile of Parsec in the chosen language. The success of Parsec and its ilk has filled Hackage (the Haskell module repository) with hundreds of DSLs covering any task you care to mention.
Yes, literally hundreds of them. Hundreds of little programming languages, one for BNF parsing, one for parsing xml, one for creating PDFs, all perfectly suited to their task. Each is different, and each has its own learning curve. Consider a common task such as parsing an XML file, mutating it according to some JSON you pulled out of a web API and then writing to a PDF. In Ruby or a similar object oriented language you expect to find three APIs/gems, all with a similar object oriented syntax, but for three Haskell DSLs designed for three different tasks to share syntax implies that their authors failed to optimise them for those tasks, hence instead of five minutes with API documentation you have hours of DSL tutorials ahead of you before you can begin work. It is this subtle difference that plants Haskell on the academic side of the town and gown divide. With Haskell the focus is on the language, not the problems you are solving with the language.
Scheme is intentionally simple, in fact I don't imagine it would take a competent programmer more than an hour to become comfortable with the syntax, but it is simple in a manner that makes it flexible, not inhibiting. Take for example template metaprogramming. Although templating is less useful in dynamic languages, Lisp hackers have had access to templates for years through the macro system, but Haskell, like C++, required a language extension to make it available.
Scheme's historical role as the language of innovation is a big part of why I chose it for my iPad development environment Lisping. When reinventing coding on touchscreens by allowing the user to edit the syntax tree, it seemed fitting to choose a language associated with so many new technologies over its history.
It would be dishonest not to point out that despite it's utility to hackers, Scheme shares Haskell's unsuitability for production code. The difference is that Scheme is limited by its minimalistic standard library, not by a flaw in the language, and you can "upgrade" to a syntactically similar, but heavyweight, cousin such as Common Lisp, or Clojure, to get work done.
I'm not saying Scheme is better than Haskell. Programming languages are tools, and one should always choose the best tool for a job, but I do think that Haskell's obtuseness makes it a poor choice for hackers. Learn all you can from its functional beauty, and campaign for its best features to be added to your language of choice, but that is all Haskell will ever be to me, a platonic ideal of a programming language, lighting the way to the future, but unusable in itself.
EDIT: thanks to those who pointed out the existence of Debug.Trace, but I was trying to express my frustration at not being able to insert a little impure code temporarily to tweak behaviour and help with debugging.
Posted on 9 April 2012
Based on a work at http://slidetocode.com/blog
Slide to code blog is licensed under a Creative Commons Attribution 3.0 Unported License