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? :)
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.
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
is a function that takes a list of strings, and returns a string.nameOrDefault [] = "stranger"
nameOrDefault
is passed an empty list, return "stranger"
.nameOrDefault (arg:_) = arg
nameOrDefault
is passed a list with the first item being arg
(and we don’t care about the rest), return arg
(since it’s a list of strings, arg
will be a string, and the types work out.)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
.
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:
do
(syntactic sugar)args
<-
(syntactic sugar)let
(expression)getArgs
(a library function)name
nameOrDefault
(our function)greeting
++
(builtin function)putStrLn
(builtin function)with the complexities of names in the functional style:
getArgs
(a library function)>>=
(builtin function)putStrLn
(builtin function).
(builtin function)makeGreeting
(our function)nameOrDefault
(our function)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.
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”.