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:
- we define
fib
recursively 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
fibStr
to return a string like"fib(8)=21\n"
or such - Haskell doesn't support string interpolation, so we need to++
a bunch of strings together. -
show
might look like a function, but it's not, I'll get to it in a moment -
fibLoop
is another recursive function for building up the whole answer string - and finally a little IO monad code to do the printing
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:
-
Show
type class is simple enough, anything that can be converted to aString
just needs to define its type-specificshow
. Any vector, if its elements areShow
able, isShow
able as well. Vector of non-Show
able elements like a vector of functions would not work here. -
Num
is much harder - we only really want+
and-
, butNum
demands 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 Integer
is both in type classesShow (Vector Integer)
and type classNum (Vector Integer)
-
Vector String
is in type classShow (Vector String)
, but not inNum (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 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)
- there are many ways to define a function, we can do it by defining a list of cases with
| condition = result
and| otherwise = default
, which is a fairly unusual use of=
-
(map f [1..n])
to applyf
to every number from1
ton
. -
intercalate
(which we need to import fromData.List
) is a really weird name for aString.join
. That's because Haskell doesn't really have strings as a first class concept, they're just lists of characters, andintercalate
attempts 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
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:
- you can apply only some of the arguments to a function - a function that takes two arguments like
mod x y
is 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 y
is same asf y x
- if you want to do partial application of a function, but that function takes the arguments in wrong order, you can useflip
on it - for example if we want to create a function for "remainder of dividing by 3" (_ mod 3
), we canflip mod
, then apply3
to it -
$
just saves some parentheses and you never really need it -
.
is function composition(f . g) x
isf (g x)
-
const x
is a function that takes any argument and ignores it and returnsx
-
bool a b c
is a function that takesc
and returns eithera
orb
depending onc
-
liftM3
applies same argument to 3 functions -
[1..]
is infinite range starting from1
-
take n list
takes firstn
elements of alist
, which we flip to take list first and n last so we can partially apply it - operators like
+
or==
are generally placed between arguments likex + y
orx == y
, but if you parenthesize them one or both sides missing like(+)
or(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.
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.
Top comments (3)
The typeclass/trait you defined in scala works flawlessly in Haskell:
doeas also cover
gets hairy if you want generic derivation like Num a => Additive a or Monoid a -> Additive a, which seem to require a few compiler extensions
Can even get your own operator as long as it's not +:
If you really want to use + you can even do this, but it's a terrible idea
I was really curious about your
instance Num
solution, but unfortunately it doesn't do what I want. It allows code we want to disallow and then crashes:I want
putStrLn (add "foo" 7)
to fail to type error, not at runtime.OK, more tries:
fromInteger = show
fails withCouldn't match type ‘a’ with ‘Char’
instance Num [Char]
is not supported by GHC without language extensionsBut of course it does the exact thing I wanted type system to stop:
Yep, with the fact that Num includes fromInteger and tries to convert different numbers before adding them, the only way to have + behaving as you want is by hiding it from the prelude and define your own typeclass with instances.
I didn't even try but should compile and work as you want it to. Of course I would never do that and define my own operator instead like the +^ above
Anyway it's only if you want the symbol to be +.
The additive instances above work fine out of the box, both with suffix and infix syntax