100 Languages Speedrun: Episode 54: Haskell

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.

Hello, World!

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!

Fibonacci

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:

Type Classes

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 + and 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:

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

It means add has type t -> t -> String, but not for any t, only for t which is an instance of type classes Show (types with show) and 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.

Defining own types

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:

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 addIntVectors, addFloatVectors, intVecToString, floatVectorToString, 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 Vector constructor).

FizzBuzz

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)

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

Pointfree Fizzbuzz

This is where we should probably stop. The code is perfectly readable if you're used to the $ operator.

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 fizzBuzzLoop and 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:

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.

Should you use Haskell?

No.

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.

Code

All code examples for the series will be in this repository.

Code for the Haskell episode is available here.