In Praise of Haskell
drdobbs.com
drdobbs.com
In actuality the author is trying to sell Haskell because he sells Haskell IDEs.
I'm not knocking Haskell, but let's not pretend that this is a useful piece of journalism.
The shallowness of the article combined with the background of the author can be reconciled if you understand the article as an advertisement.
The difference is that Haskell is a very principled language. All of the claims and excitement are actually backed up by mathematical proofs. Pure functions really do make your code easier to maintain.
And no, it's not the case that Haskell is the only language which allows you to write pure functions. People who try to claim that are being silly and dishonest. Haskell's advantage is that its type system allows you to know at a glance whether or not a function is pure.
I disagree. You can make objective measurements of maintainability. You can look at cyclomatic complexity and reusability (both wins for pure functions). You can test how often changes lead to bugs and how often the errors are caught. There are many instances of Haskell code where there is only one sensible implementation of a function, given its type. Hell, some Haskell libraries have even been formally verified though admittedly that is done with external packages.
I will agree on one thing, though: you do run into trouble when you throw around the word all (or indeed any absolute) without actually meaning it.
Yes, those are both wins for pure functions. But even there you need an asterisk, because side effects do have their place - the fact of the matter is, the primary task of most real-world software can be summed up as "manipulating mutable state". In light of that I don't think you can necessarily jump from the observation that there are many situations where pure functions are easier to work with than impure ones to the conclusion that pure functions are inherently more maintainable.
But even if that were granted for the sake of argument, it's not much of a win for Haskell. The ability to create pure functions is a feature of every language that has been invented during the lifetime of probably every person reading this post, and therefore not really a differentiating feature of Haskell.
Nor is lack of ability to create impure functions a feature that can be claimed of Haskell, because that simply isn't true. Haskell's real differentiator in that department is just that you've got to jump through hoops to do it. And if your business rules are inherently impure, then it's not clear to me how a language that tries to ghettoize impurity makes them easier to implement.
What you can't do in Haskell is write impure code that claims to be pure (up to the customarily and idiomatically avoided `unsafePerformIO`), or write code whose degree of statefulness is ambiguous. Reliable, explicit, and statically enforced purity holds more than just theoretical benefits. I don't have to read through library code to see if it is thread-safe. I don't have to worry about whether passing a data structure to a library function will result in that structure being mutated behind my back. I don't have to trust code comments that may not be in sync with the current state of the code. Simply by virtue of the fact that a function does not mention `IO` in its type, I can have total confidence that it won't violate my assumptions about its behavior. And I don't even have to take it on faith that the author wrote the correct type for his code; if he hadn't, the compiler would have rejected it. This is the difference between "pure by convention" and "provably pure".
Every language must necessarily have an "IO monad" and interact with the inherently stateful world, or else it is useless. Haskell is different not because you can choose to avoid I/O, but because when you do so, the type system will back you up with perfect accuracy.
I already dealt with this three posts above:
And no, it's not the case that Haskell is the only language which allows you to write pure functions. People who try to claim that are being silly and dishonest. Haskell's advantage is that its type system allows you to know at a glance whether or not a function is pure.
Nor is lack of ability to create impure functions a feature that can be claimed of Haskell, because that simply isn't true. Haskell's real differentiator in that department is just that you've got to jump through hoops to do it. And if your business rules are inherently impure, then it's not clear to me how a language that tries to ghettoize impurity makes them easier to implement.
That's only the case for beginners who don't understand monads and their associated libraries. For an experienced programmer, Haskell offers much more powerful and expressive (not to mention safe) means of combining effectful computations than typical imperative languages.
Allowing the type of value to to depend on the value itself. A common example being a list that can expresses whether it is empty or not in its type. These types allow one to write functions with signatures like: `safeHead :: SafeList a NonEmpty -> a`. Uses of this function will fail to compile if I can not prove that my list will always be non-empty.
Combine this with the well known Curry-Howard Correspondence [3] which shows that types have a direct correspondence to proofs. The logical extension of this is computer theorem proving languages like Coq, Agda, Epigram, Idris.
For example Coq[4] uses a dependently typed lambda calculus as its proof term language, and sees wide spread use in the formal verification of software. It is not a stretch to say that less powerful types systems like Haskell's allow us to encode some forms of proof. If you view the type checker as an proof checker, one provides propositions to it (in the form of types) and then supplies proof (in the form of terms). These can be trivial propositions like `a :: Int`, which in Haskell there are 2^31 correct answers.
[1] (http://ttic.uchicago.edu/~dreyer/course/papers/wadler.pdf)
[2] (http://en.wikipedia.org/wiki/Dependent_type)
[3] (http://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspond...)
[4] (http://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspond...)
My disagreement was specifically with the implied claim that it can be mathematically proven that Haskell code is easier to maintain than code written in other languages. That has indeed been my experience with Haskell, but I do not think it is directly provable. What seems easy for me may be stupefyingly difficult or confusing for someone else.
"How easy something is" is not really a quantifiable measurement; it is a subjective experience. As chongli points out, however, there are related measurements that can be taken, such as cyclomatic complexity, and those at least seem to be correlated with ease of maintenance. So perhaps I'm just being overly pedantic!
I found working in a big Ruby code base that this required spending a non trivial amount of time unifying the two. In that regard by minimizing the amount of test code one has to write and maintain I would argue that maintainability goes up. I've also seen a lot of FP practitioners boasting about relatively low test to code ratios, while in the Ruby world often times you have at least 2x test code, since often times you even have to validate assumptions like `object.responds_to? :foo`.
I found it really interesting to see how a large team can deal with a real haskell project.
[1]: http://www.haskellcast.com/episode/002-don-stewart-on-real-w...
In a pure functional environment, you don't have those dependency issues. Your problems boil down to types -- do you have the correct fields and definitions in your inputs? -- and transforms -- are your symbolic transformations what is desired?
In practice, pure FP is the closest I've seen to stuff that "just works". While FP can be a huge pain in the ass mentally, it beats the heck out of the spaghetti dependency issues in OOP. [insert long discussion here about whether pure FP can actually scale in a large organization]
It scales for us at Standard Chartered.
http://prog21.dadgum.com/138.html "If you want to be on the cutting edge of functional programming research, it's easy. Pick something that looks like a poor match for state-free code, like a video game, and start working on it."
Will I have to write a FP "white paper" to make a game in FP?
http://functionaltalks.org/2013/08/26/john-carmack-thoughts-...
The perceived difficulty of mastering pure functional programming for practical work might be a result of bias in the typical CS education experiences. That might shift as the value of pure functional programming gets more universally recognized.
I've thought about this a lot and honestly, I think this is a good thing. Languages which are much less strict about types and mutation require huge mental gymnastics too, they just don't tell you about it. They let you happily continue on your merry way until some time at a later date you realize your program is a giant ball of mud and you have no idea how to change things without breaking it. Haskell forces you to deal with the hard problem of state up front in exchange for having a clear, maintainable program down the road.
Not to mention adding a new piece to a highly type based solution could involve modifying a lot of the existing types to make room for the modification. Whereas with many dynamic solutions, this is not the case.
This does not mean it will not compile. In fact often the fix is simply for the author or developer to relax the version bounds and everything will be fine.
Lastly, many of these issues arise not because of a direct dependency but an indirect one. The problem is that GHC will not allow you to depend on two packages which themselves depend on different versions of the same package. Once this is fixed, along with sandboxing, will eliminate the vast majority of the cabal hell problem.
The analogy I have in my mind is a digital computer for a bike. The general "contract" is simply that you will somehow get a pulse to the computer for each rotation of a tire with a known size. The details of how the bike is constructed are ultimately irrelevant.
Further, because this is such a general contract, it doesn't actually matter if you hook it to the wheel or not. You can, in fact, use such a computer in a non bike device with success.
That said, dynamic languages allow this kind of contractual module imputation by the virtue of allowing absolutely anything to be considered a module and hashed out at runtime. At the least this means carrying around a lot of tagging/runtime typing information so you can call up runtime errors. At the most, it means that your whole system is held together by an increasingly wide set of unchecked and uncheckable assumptions.
Cabal hell is annoying but at least it's checking.
Although I've read some blogs that complained that ML gets this right, and Haskell makes it harder with it's typeclasses
The typical solution to this problem in production Haskell systems, by the way, is to run the application with profiling turned on.
What's not to like?
Cabal, which unsurprisingly is not mentioned. Dependency hell makes it stupidly difficult to rely on other people's code on a large scale. I'd rather write uglier code in JavaScript if it means having access to other people's packages immediately and without complications.[^1]: http://hackage.haskell.org/package/cabal-dev [^2]: http://hackage.haskell.org/package/hsenv (Disclosure: I am the current maintainer of this project, though I am happy to see it becoming obsolete)
It may be that I am simply too dumb, or that I didn't find the right resources, but 'easy to learn' this language is not.
What makes Haskell a little challenging in the beginning is its currying, which means you don't need to call a function with all of its stated arguments, and the fact that function application associates to the left; i.e. that `f x y` is the same as `(f x) y`, NOT `f (x y)`, which is closer to how it would be in, say, Ruby or CoffeeScript. There are very good reasons for this, but it does take some getting used to. Currying and associativity rules are responsible for a lot of seemingly incomprehensible type errors. You get better at avoiding them as time goes on. I feel that the weirdness of reading Haskell DSLs, like Parsec, which often make heavy use of customized operators, or do-notation, is an extension of these issues, since most of the difficulty comes from trying to figure out how the types propagate through what amounts to be long, complicated chains of function applications.
$a = range(2,100,2); $b = array_slice($a, 5);
int[] arr = new int[50]; for(int x =2; x <= 100; x = x+2){arr[(x/2)-1] = x;} int[] arr2 = Arrays.copyOfRange(arr,5,arr.length);
There's probably a more fluent method to achieve the same result (in Scala, F# and C#, certainly there are), but I'd like to see a considerably less-contrived example from a more-disinterested party.
Is it me, or was it a bad example for where Haskell "really shines"? I don't like the "use Haskell cause it has these really cool functions" approach. Feels so superficial.
Haskell is worth the effort, but I don't think the article's author did a great job of articulating why.
Some things I've noticed:
Haskell does not automatically admit an extra "null" value to every type, so you can ordinarily ignore NullPointerExceptions and the like. When you do need a nullable bit of data, you have to do some extra work: You must rename the type from "Foo" to "Maybe Foo" and change all the callsites. These callsites must test for null and specify what happens. (there exists a non-terminating explode() function if you really want it) The build will fail until every last case is covered. This is great for reliability.
Most data in Haskell is immutable. This encourages good programming practice, sure, but it really pays off when you start writing parallel code. Parallel Haskell is by far the easiest way I know to write fast concurrent stuff. On that note, Haskell's concurrency primitives are very well-designed.
Haskell's solution to callback hell is more general and powerful than anything I know. The "do" block is really just super elegant syntax sugar for a CPS chain.
Our unit test suite does not intermittently fail. We restrict side effects such that our build will fail if a test tries to do non-faked I/O. It took a bit of effort to build, but normal use is effortless.
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
Even still, it's difficult to get the feel of actually using the language from one function taken from a freshman year CS class. powerset :: [a] -> [[a]]
powerset xs = filterM (\x -> [True, False]) xs
ghci> powerset [1,2,3]
[[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]]
Edit: I read LYaH some time ago but this example stuck with me because it's both wonderful and mind-blowing. powerset :: [a] -> [[a]]
powerset [] = [[]]
powerset (x:xs) = yss ++ map (x:) yss
where yss = powerset xs
> powerset [1,2,3]
[[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]Having strong and concise type guarantees is great. Haskell's a great example of that. So is Ocaml.
val a = 4 to 100 by 2
val b = a drop 5