Haskell is a language that took the ideas of functional programming very seriously. Instead of being halfhearted like OCaml or various Lisps, Haskell took the most principled functional choice every single time.
Before I get to the code, there are a few serious issues with Haskell I want to address up front.
Haskell demands full commitment from you and has brutal learning curve. You can’t slowly ease into functional style, like you could with a functional-flavored language Ruby, OCaml, Racket, or Clojure – starting with a style you’re already familiar with, and gradually going “more functional” as you get comfortable with it. Nope, you start functional, remain functional, and there are pretty much no escape hatches.
Among all the languages that see any nontrivial use, Haskell likely has the most sophisticated type system, and they are no opt-outs. You will need to learn how to make the type checker happy, and a lot of perfectly reasonable code will require some serious hoops to jump through to make it type check, if it is at all possible. Haskell also pretty much expects that if you want to code properly, you’ll need to use various language extensions, most of which are type system extensions. Type checker error messages often literally ask you if you want to enable some language extension or another.
Maybe Scala and Crystal get even close to that level of type system sophistication, and both are orders of magnitude less demanding, and with far more escape hatches available.
And the worst thing about Haskell is the kind of people it attracts. For some reason, when any other kinds programmers are given a choice of a “clever” more concise code, and a simpler more readable code, even Perl programmers tend to choose the simpler one. For whichever reason Haskell programmers rarely show any such restraint. You can go through some Haskell tutorials to learn the basics, then try to read some real Haskell code, and you’ll feel like you accidentally opened obfuscated code golf website. Here’s a joke version of this problem, but it’s very real.
Normally we’d start with a Hello, World!, but that’s not a function. Functions take arguments, and return results, and printing stuff to the terminal is not a result.
Haskell supports IO through “monads”, which are convoluted enough that explaining them through analogies was a meme a while back, including the controversial big argument if jQuery is a monad or not (to spoil it – technically it isn’t, but there are some similarities).
The code below might looks fairly conventional but it’s just a fake syntax hiding some alien concepts:
main = do putStrLn "Hello, World!"
$ ghc hello.hs $ ./hello Hello, World!
Let’s do this a functional way, constructing the output in one go, and just printing it whole once it’s done.
fib :: Int -> Int fib 1 = 1 fib 2 = 1 fib n = fib (n-1) + fib (n-2) fibStr :: Int -> String fibStr n = "fib(" ++ (show n) ++ ")=" ++ show (fib n) ++ "n" fibLoop :: Int -> String fibLoop 0 = "" fibLoop n = fibLoop (n-1) ++ fibStr n main = do putStr (fibLoop 20)
$ ghc fib.hs $ ./fib fib(1)=1 fib(2)=1 fib(3)=2 fib(4)=3 fib(5)=5 fib(6)=8 fib(7)=13 fib(8)=21 fib(9)=34 fib(10)=55 fib(11)=89 fib(12)=144 fib(13)=233 fib(14)=377 fib(15)=610 fib(16)=987 fib(17)=1597 fib(18)=2584 fib(19)=4181 fib(20)=6765
Let’s go through it step by step:
- we define
fibrecursively as expected. Haskell supports pattern matching including on numbers, so we can write separate definitions for base cases, then for recursive case (all definitions need to be all in one place)
- we define
fibStrto return a string like
"fib(8)=21n"or such – Haskell doesn’t support string interpolation, so we need to
++a bunch of strings together.
showmight look like a function, but it’s not, I’ll get to it in a moment
fibLoopis another recursive function for building up the whole answer string
- and finally a little IO monad code to do the printing
Haskell has a really strict type system, so you might be worried that each type would get its own function like
intToString, the way it works in OCaml.
Fortunately this is a problem Haskell managed to solve, using “type classes”. The price is a lot of additional type system complexity, but that’s the price Haskell is always happy to pay. Let’s see how they work.
At first we might want to write code like this:
add :: t -> t -> String add x y = (show x) ++ " + " ++ (show y) ++ " = " ++ (show (x + y)) main = do putStrLn (add 40 380) putStrLn (add 6 0.9)
That should work right? Both
show are defined for integers and for floats, and that’s the arguments we’re passing. Unfortunately Haskell type checker will not allow this.
add :: t -> t -> String would mean we can pass anything to
add – including strings, functions, and who knows what else, and these types might not have the right functionality available. It doesn’t matter to Haskell that we never actually pass such arguments. Type signature says that that we might, and that’s enough to reject this code.
Let’s add some additional annotations, to specify exactly which types are allowed for
add :: (Show t, Num t) => t -> t -> String add x y = (show x) ++ " + " ++ (show y) ++ " = " ++ (show (x + y)) main = do putStrLn (add 40 380) putStrLn (add 6 0.9)
$ ./add 40 + 380 = 420 6.0 + 0.9 = 6.9
add has type
t -> t -> String, but not for any
t, only for
t which is an instance of type classes
Show (types with
Num (types with
+ and a few other things).
By the way Haskell figured out that the
6 was actually
6.0, something OCaml would not do.
Well, let’s follow along and define our own type, a 2-element vector, with a pair of arbitrary types for elements.
data Vec t = Vec t t instance (Show t) => Show (Vec t) where show (Vec a b) = "<" ++ show a ++ "," ++ show b ++ ">" instance (Num t) => Num (Vec t) where (+) (Vec ax ay) (Vec bx by) = Vec (ax + ay) (by + by) (-) (Vec ax ay) (Vec bx by) = Vec (ax - ay) (by - by) (*) (Vec ax ay) (Vec bx by) = Vec (ax * ay) (by * by) abs (Vec ax ay) = Vec (abs ax) (abs ay) signum (Vec ax ay) = Vec (signum ax) (signum ay) fromInteger n = Vec (fromInteger n) (fromInteger n) add :: (Show t, Num t) => t -> t -> String add x y = (show x) ++ " + " ++ (show y) ++ " = " ++ (show (x + y)) main = do putStrLn (add (Vec 20 30) (Vec 40 50)) putStrLn (add (Vec 55.5 14.5) (Vec 210 210)) putStrLn (show (Vec "foo" "bar"))
$ ./vec <20,30> + <40,50> = <50,100> <55.5,14.5> + <210.0,210.0> = <70.0,420.0> <"foo","bar">
What’s going on:
Showtype class is simple enough, anything that can be converted to a
Stringjust needs to define its type-specific
show. Any vector, if its elements are
Showable as well. Vector of non-
Showable elements like a vector of functions would not work here.
Numis much harder – we only really want
Numdemands that we define a bunch of other things, which we just define elementwise. If we want to use
+we need to do all those things (or redefine some things).
Vector Integeris both in type classes
Show (Vector Integer)and type class
Num (Vector Integer)
Vector Stringis in type class
Show (Vector String), but not in
Num (Vector String)
As you can see Haskell demands high degree of precision. For this simple example we were able to satisfy the type checker, but at cost of a lot of complexity.
What if we wanted to do a totally reasonable
putStrLn (add "foo" "bar")? Well, the short story is that you simply can’t, and I don’t think that’s possible even with any of the available type system extension.
I’m giving this example mostly to show contrast with two other languages.
In OCaml, you can’t do any of such things at all, you’ll need separate
stringVectorToString etc. functions, which is just unbelievably tedious and ugly.
In Crystal you can do absolutely every thing I just mentioned, including the impossible to Haskell
putStrLn (add "foo" "bar"), and it statically type checks everything, all without a single type annotation (except for
Let’s write a FizzBuzz. This is a very straightforward solution:
import Data.List -- FizzBuzz in Haskell fizzbuzz :: Int -> String fizzbuzz n | n `mod` 15 == 0 = "FizzBuzz" | n `mod` 5 == 0 = "Buzz" | n `mod` 3 == 0 = "Fizz" | otherwise = show n fizzBuzzLoop :: Int -> String fizzBuzzLoop n = intercalate "n" (map fizzbuzz [1..n]) main = putStrLn (fizzBuzzLoop 100)
- there are many ways to define a function, we can do it by defining a list of cases with
| condition = resultand
| otherwise = default, which is a fairly unusual use of
(map f [1..n])to apply
fto every number from
intercalate(which we need to import from
Data.List) is a really weird name for a
String.join. That’s because Haskell doesn’t really have strings as a first class concept, they’re just lists of characters, and
intercalateattempts to be a more general name. Mainly it’s a really weird naming choice, and Haskell has a lot of such cases.
You might already notice something. Haskell code contains a lot of
f (...), and Haskell programmers are not in love with all those parentheses…
And so they defined
$. Which does nothing, except it syntactically applies parentheses to everything to its right side. So the equivalent code with some
$s could be:
import Data.List -- FizzBuzz in Haskell fizzbuzz :: Int -> String fizzbuzz n | n `mod` 15 == 0 = "FizzBuzz" | n `mod` 5 == 0 = "Buzz" | n `mod` 3 == 0 = "Fizz" | otherwise = show n fizzBuzzLoop :: Int -> String fizzBuzzLoop n = intercalate "n" $ map fizzbuzz [1..n] main = putStrLn $ fizzBuzzLoop 100
This is where we should probably stop. The code is perfectly readable if you’re used to the
Unfortunately it does not stop here. As I mentioned, Haskell attracts people who really love feeling clever, a lot more than they love writing readable code.
There are many ways they express their cleverness, but the main tool is the “pointfree” style – just never explicitly saying what the function operates on, and using various ways of composing functions with combinators.
To have full Haskell experience, I gave it a go too. Defining
divBy pointfree was simple enough, but the
fizzbuzz really took me a while. And I’m sure there are far more “clever” ways to write this:
import Data.List import Data.Bool import Control.Monad divBy :: Int -> Int -> Bool divBy = ((0 ==) .) . flip mod fizz :: Int -> String fizz = liftM3 bool show (const "Fizz") $ divBy 3 buzz :: Int -> String buzz = liftM3 bool fizz (const "Buzz") $ divBy 5 fizzbuzz :: Int -> String fizzbuzz = liftM3 bool buzz (const "FizzBuzz") $ divBy 15 fizzBuzzLoop :: Int -> String fizzBuzzLoop = intercalate "n" . map fizzbuzz . flip take [1..] main = putStrLn $ fizzBuzzLoop 100
For just some partial explanations:
- you can apply only some of the arguments to a function – a function that takes two arguments like
mod x yis really a function that takes one argument (number we’ll divide by) and then returns a function that takes the other argument (divisor) and returns the actual result
flip f x yis same as
f y x– if you want to do partial application of a function, but that function takes the arguments in wrong order, you can use
flipon it – for example if we want to create a function for “remainder of dividing by 3” (
_ mod 3), we can
flip mod, then apply
$just saves some parentheses and you never really need it
.is function composition
(f . g) xis
f (g x)
const xis a function that takes any argument and ignores it and returns
bool a b cis a function that takes
cand returns either
liftM3applies same argument to 3 functions
[1..]is infinite range starting from
take n listtakes first
nelements of a
list, which we flip to take list first and n last so we can partially apply it
- operators like
==are generally placed between arguments like
x + yor
x == y, but if you parenthesize them one or both sides missing like
(0 ==), they become functions that take the missing side or sides as arguments
This pointfree style is a fun puzzle. If you’re looking for some esoteric programming challenge, Haskell definitely provides one.
Unfortunately for some reason, this style of highly obfuscated Haskell is also a huge share of Haskell you’ll find in the wild.
Anyway, I’ll just stop here, or I’ll need to explain how monads works, and many have tried and failed that task before me.
Haskell had a very ambitious goal, and I’m really impressed by how far they managed to get without making compromises along the way. In many ways, Haskell is also a much higher quality language than let’s say OCaml. They really paid attention to the details, and Haskell issues are more often result of conscious design choices, not just random things which could have easily been done properly, but they never bothered, like so many of OCaml’s issues.
On the other hand, the thing they’ve been trying to achieve, perhaps it should have never been attempted. Also while some Haskell developers, I assume, are good people, it really tends to attract the kind of people who value being “clever” a bit too much. Of course, if you’re exactly this kind of person, Haskell will be great for you!
As for playing with Haskell as an esoteric language, I definitely recommend that.