razzi.abuissa.net

Razzi's guide to Haskell

Ok so I was really hyped on Haskell for a while. It’s got a strong set of paradigms:

Wait that’s it! Not object oriented, not imperative… I’ll add it has algebraic data types, but that’s not a paradigm per se. Also it uses lazy evaluation; this is really cool in my opinion, and definitely a unique choice.

I’m still glad I learned enough of it to understand what it brings to the table (I had a good time taking CIS 194 at UPenn), but I’m also glad I kinda stopped trying to learn and apply it. Here’s why. It almost immediately devolves (ascends?) into category theory, which is really cool but not something most working programmers know or have much interest in learning.

Like… there are plenty of tutorials on monads, and they’re a legit concept. But if you put this in front of your uninitiated coworker:

(>>=) :: m a -> (a -> m b) -> m b

It’s really abstract and it’s hard to know how to use it. In this small code snippet a and b are type variables, and for a fun and cool adventure into type variable land, read https://aphyr.com/posts/342-typing-the-technical-interview.

So here’s the problem. Haskell is really cool and the math behind it is powerful and fascinating. But you’re likely to get pulled into discussions like Aphyr’s neat hacking of types, and get increasingly far from building out whatever product you’re working on.

Of course, if you’re a researcher, this is great, and the product is probably a proof of concept to demonstrate whatever programming language research area etc you’re exploring. Further, languages like Haskell are actually general purpose and more geared towards application development than, for example, a theorem proving system like Coq https://coq.inria.fr/.

But there are other problems too. The language is from 1990, so 35+ years old (one year older than Python), so it has some unusual names (what other languages might call print Haskell calls putStrLn), and had to invent some conventions, much like how emacs calls pasting “yanking”. This can be useful for exact domain-specific terminology, but it’s also a huge learning curve.

Also, it’s greatest strength is also a weakness: being purely functional means not only that some things are hard to express (especially if you’re new to the language), but also that interoperability with the “impure” world is difficult. In 2025 there are millions of lines of code out there you’ll want to incorporate into your real-world projects; to Haskell’s credit there are indeed millions more that are poorly implemented or have a shaky foundation and you’ll be glad a typesafe language like Haskell will refuse them, but it’s an uphill battle especially integrating Haskell into an existing system.

Other language choices that are unwieldy: liberal use of symbols like <*>, >>= as we’ve seen, <$>, even <?> and .:. Then take language extensions. The fact that modifying the language is supported at all is kinda cool, but they can add yet another layer of indirection; take this Template Haskell snippet:

let tup = $(tupE $ take 4 $ cycle [ [| "hi" |] , [| 5 |] ])

There is actually quite good tooling, even applications like Hoogle that allow you to search for symbols like |>: https://hoogle.haskell.org/?hoogle=%7C%3E. And although it is compiled, it has an interpreted feel due to type inference and a REPL built into the main compiler: GHCI.

But back to the one last drawback. There are some just plain unwieldy functions. I still remember the feeling of frustration when I saw the definition of lift: “Lift a precedence-insensitive ReadP to a ReadPrec.” But what does it mean to “lift” at all here? I still don’t know what it means, but I do know the language implementation necessitates functions like lift2, lift3, all the way up to lift6… like, your language can be a beautiful pure engine of computation inside, but something as simple to other languages as “a function that works with a variable number of arguments” is somehow an edge case here.

Ok this is more of a rant than a guide so far. So, ready to jump in to the guide? :)

hello world

Put the following in hello.hs:

main = putStrLn "Hello, World!"

Cool! Short and sweet!

Compile and run it as follows:

$ ghc hello.hs
[1 of 1] Compiling Main             ( hello.hs, hello.o )
Linking hello ...
$ ./hello
Hello, World!

Alright, a self-contained binary hello that runs! (And some other intermediaries, hello.hi and hello.o)

Back to the source code itself, interestingly, what’s happening here is that we’re defining a function called main to be equal to putStrLn "Hello, World!". So rather than defining a function to have a body with a block of statements like you might do in c, we’re assigning one function to another.

In that same vein, we can define another function called greetWorld, and assign main to this:

greetWorld = putStrLn "Hello, World!"

main = greetWorld

Programming in Haskell is all about defining functions. Most programming is, really, but other languages mix in stuff like writing statements.

greeter

Now let’s make it more of a generic greeter program, by getting a parameter from the command line.

We’ll need an import, and we’re going to use do notation here, which we’ll break down shortly.

import System.Environment

nameOrDefault :: [String] -> String
nameOrDefault [] = "stranger"
nameOrDefault (arg:_) = arg

main = do
  args <- getArgs

  let name = nameOrDefault args

  let greeting = "Hello, " ++ name ++ "!"
  putStrLn greeting

Compile and run, invoking with and without an argument:

$ ghc hello.hs
[1 of 1] Compiling Main             ( hello.hs, hello.o )
Linking hello ...
$ ./hello
Hello, stranger!
$ ./hello doggie
Hello, doggie!

The behavior is to handle no arguments by outputting Hello, stranger!, otherwise take the first argument to be the name to greet.

This logic is in the nameOrDefault function:

nameOrDefault :: [String] -> String
nameOrDefault [] = "stranger"
nameOrDefault (arg:_) = arg

What it’s saying is:

nameOrDefault :: [String] -> String
nameOrDefault [] = "stranger"
nameOrDefault (arg:_) = arg

Now on to the main function. It’s still a function, even though it’s using do as syntactic sugar.

main = do
  args <- getArgs

  let name = nameOrDefault args

  let greeting = "Hello, " ++ name ++ "!"
  putStrLn greeting

We assign args to the result of getArgs (a function from System.Environment), then assign a local variable name to the result of running nameOrDefault on that.

Finally we concatenate "Hello, " to name and add an "!" at the end, assigning it to greeting, then we print out greeting.

greeter program without syntactic sugar

We can do the same computation without syntactic sugar. Here’s how it looks:

import System.Environment

nameOrDefault :: [String] -> String
nameOrDefault [] = "stranger"
nameOrDefault (arg:_) = arg

main :: IO ()
main = getArgs >>= (\args -> putStrLn $ "Hello, " ++ nameOrDefault args ++ "!")

This works, but let’s break things into even smaller pieces by defining a function makeGreeting that does the string concatenation.

import System.Environment

nameOrDefault :: [String] -> String
nameOrDefault [] = "stranger"
nameOrDefault (arg:_) = arg

makeGreeting :: String -> String
makeGreeting name = "Hello, " ++ name ++ "!"

main :: IO ()
main = getArgs >>= (\args -> putStrLn $ makeGreeting $ nameOrDefault args)

The (\args -> ...) is an anonymous function (lambda).

I won’t get in to >>= just yet, but here’s the function of $: group the function calls on the right hand side of it.

These are equivalent:

-- haskell style
putStrLn $ makeGreeting $ nameOrDefault args
-- most other languages style (but this is also valid Haskell)
putStrLn (makeGreeting (nameOrDefault args))

Using $ or (), we actually get a lint warning here from hlint, recommending not to use a lambda. Here’s it’s recommendation:

Avoid lambda
Found:
  \ args -> putStrLn $ makeGreeting $ nameOrDefault args
Why not:
  putStrLn . makeGreeting . nameOrDefault

Ok, incorporating that change:

main :: IO ()
main = getArgs >>= putStrLn . makeGreeting . nameOrDefault

This is what’s called “point-free” programming style.

Interestingly, once we broke our function into bite sized pieces, the data flow becomes clear.

We take the args (getArgs) and pass that to a composition of three functions: nameOrDefault, makeGreeting, and finally the outermost putStrLn.

Let’s compare it to our do notation style again:

main = do
  args <- getArgs

  let name = nameOrDefault args

  let greeting = "Hello, " ++ name ++ "!"
  putStrLn greeting

Yes, our point-free style is much more concise, but there’s one advantage I want to point out about the do notation style: local variables. Rather than coming up with function names for the highly specific “put hello, then the name, then the exclamation mark”, we are calling the result of that greeting.

Compare the complexities of the symbols names in the do notation style:

with the complexities of names in the functional style:

The key here for me is that in the do notation style, we get helpful metadata about intermediate result names. name and greeting are symbols I could print and debug; in the point-free style nothing is named since it’s all function composition.

It’s not a totally fair comparison since the point-free style uses another helper function, but I think this is actually reflective of the approach: when we’re not doing everything with composition, we’re more empowered to do things like "Hello, " ++ name without the need to define a function sayHello name = "Hello, " ++ name. Much like how in Java, since everything is a class, we have to wrap a greet function in a Greeter class, Haskell pushes us towards making functions for things like makeGreeting rather than doing any computation inline.

closing thoughts (for now)

There is so much more to Haskell, and I didn’t even cover >>= or the type of main yet. Consider this guide a work in progress, and let me know if you’d like to see more!

Let me end on a high note, and a reason I might still learn Haskell yet: Haskell performance is extremely high, much higher than what you might expect for a high-level language like this. I’d much rather write code and let the compiler optimize it than write extremely low level hand optimized c (though that is fun in its own way).

Also the Haskell type system is way more advanced than basically anything else out there. It’s an exaggeration but somewhat of a truism in Haskell that “if it compiles, it’s correct”.