DEV Community

Cover image for Haskell for madmen: Hello, monad!
DrBearhands
DrBearhands

Posted on • Updated on • Originally published at drbearhands.com

Haskell for madmen: Hello, monad!

Post from my blog. If you're enjoying the series, please consider buying me a coffee.

Intro

In this chapter we're going to write a hello world program. Doing so in Haskell requires monads, a concept from category theory. You might think this is overly complicated for something as simple as printing some output, but you'd be wrong, printing output is not simple at all. Just think about buffers, encoding, concurrency... If you judge a language by the ease with which you can write helloworld, you're going to pick a language that hides important "details".

We're first going to look at pure functions, i.e. functions without any side-effects, and their types. Purity is the default in Haskell, but does not allow writing output, which is a (side) effect.

We will then cover the IO monad, which is how Haskell handles necessary effects. This will allow us to finally write helloworld.

This chapter is probably the hardest part of this tutorial and what makes it for madmen.

Pure functions

In essence, every function in Haskell has exactly 1 input and 1 output. We can still use "multiple" inputs by making that output itself a function. Let's look at how this works with addition:

(+) 3 7
Enter fullscreen mode Exit fullscreen mode

Writing two values separated by a space is function application, f a is applying argument a to function f.

Function application in Haskell is left-associative, so the code above is equivalent to:

((+) 3) 7
Enter fullscreen mode Exit fullscreen mode

Here, we have a function (+), to which we apply 3. The result is a new function that takes a number and adds 3 to it. To this new function we then apply 7.

Now it's time to talk about types. As mentioned, a function only has one input and on output, and we type it using an arrow. A function that takes some type a as input and outputs some b has type a -> b. Consequently, a function that takes a a and produces a second function with type b -> c, has type a -> (b -> c). For our function (+), that means the type must be:

(+) :: Int -> (Int -> Int)
Enter fullscreen mode Exit fullscreen mode

(Note: in Haskell, :: indicates a type declaration)

Since arrows in type declarations are right-associative, the parentheses are superfluous in this case, and we can also write:

(+) :: Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

Because we know functions cannot have side-effects, the type declaration tells us pretty much exactly what the function is going to do: it will compute an Int given two other Ints. Nothing else will happen, and it won't matter when we evaluate this function.

We don't have to apply all arguments at once. The following is perfectly valid:

add3 :: Int -> Int
add3 = (+) 3

ten :: Int
ten = add3 7
Enter fullscreen mode Exit fullscreen mode

The (+) function is a bit special. Using symbols in parentheses as a function names turns it into an infix operator. We can therefore also write a much more natural looking sum:

ten :: Int
ten = 3 + 7
Enter fullscreen mode Exit fullscreen mode

Finally, a note about lazy evaluation. In the above examples, ten does not have the value 10, rather it is an expression that will result in the value 10. Thanks to referential transparency, the compiler can decide not to compute the value of ten right away, but just pass (a reference to) it's body (3 + 7) and postpone evaluation to whenever it's actually needed. Not only does this let us avoid unnecessary computations, but it also lets us use infinite data structures. An example:

infiniteListOf1s :: [Int]
infiniteListOf1s = 1 : infiniteListOf1s
Enter fullscreen mode Exit fullscreen mode

(Note: : is a "prepend" operator with type a -> [a] -> [a], e.g.
1 : [2,3] = [1, 2, 3])

This list is infinite! Because Haskell is non-strict, this is fine. The compiler will make sure we only compute the part of the list we actually need by using lazy evaluation. So as long as we don't try to read the whole list, it won't enter an infinite loop.

Effects

Let us now, armed with the above knowledge, look at the project generated by the command stack new.

In app/Main.hs we find the following:

module Main where

import Lib

main :: IO ()
main = someFunc
Enter fullscreen mode Exit fullscreen mode

This should confuse you. So let's look at the individual parts.

module Main where declares the name of the module. "Main" is a special name, as you probably expect.

import Lib imports the Lib module and adds all the symbols Lib exports to the current namespace. This includes someFunc, but we can't tell from the import statement, we will be changing this line to something more informative in a bit.

main :: IO () declares that the type of the value called main is IO (). This would quite rightly confuse you about now. Why isn't this a function? What are these mysterious parentheses? What is an IO?

Why isn't main a function? As mentioned, functions compute one value from another, but this isn't really the nature of computer programs. Rather, we want a program that does stuff, whereas a function is just an inert formula. main should be a list of actions / effects, such as writing to stdout or listening for http requests.

Then what is ()? This is a special type called unit. Every type has one or more inhabitants, a boolean has inhabitants True and False, an unsigned 8-bit integer is inhabited by numbers 0 through 2^8. Unit has only 1 inhabitant, the unit. This entails that a value with type unit carries no information. Having a pure function that outputs a unit would be pointless, because we could simply substitute the answer without ever needing to evaluate said function. However, the unit type acts a lot like the number 1, and has a lot of uses when combined with other types. One of those is as used here. Both the type and the inhabitant of unit are written as () in Haskell.

IO, if you're particularly observant, you may have noticed we seem to have applied it just like a function, but in the type declaration. Just like values have types, types have kinds. The kind of Int, (), String, Char, etc. is *, the kind of IO is * -> *. Just as with function application, the kind of IO (), that is () applied to IO, is therefore *. As for the "meaning" of IO, it turns a type that is computed with pure functions, into one that is computed using effects. We will see how to use impure functions in a bit.

Putting all that together, main, by its type IO () is a unit computed using side effects. Since () contains no information, IO () is just a series of effects / instructions "without" output. That is pretty much what we generally look for in a program!

Fixing the import statement

After all that stuff about the type of main, we turn to look at the next line, the term of main: main = someFunc. This is rather disappointing, as it merely references a single other value. someFunc is exported by the Lib module. We pretty much have to guess that, because with the current import notation, it is not explicit. This becomes a problem once you have more modules. So let's get ahead of ourselves and change that import to the following:

import Lib (someFunc)
Enter fullscreen mode Exit fullscreen mode

this will expose only someFunc from Lib, and tells us exactly where symbols are coming from. We can also make the import qualified, which forces us to mention the module name explicitly:

import qualified Lib
main = Lib.someFunc
Enter fullscreen mode Exit fullscreen mode

plus it's totally valid to do both:

import qualified Lib
import Lib (someFunc)
Enter fullscreen mode Exit fullscreen mode

Which will expose someFunc, but allows you to access other symbols through the explicit notation from above.

Combining effects with monads

Now it's time to look at what is happening in someFunc. But where can we find the Lib module that defines it? If you haven't changed anything, it should be src/Lib.hs (modules names must match relative path names). You can change which directory are part of your project in package.yaml.

The file should look like this:

module Lib
    ( someFunc
    ) where

someFunc :: IO ()
someFunc = putStrLn "someFunc"
Enter fullscreen mode Exit fullscreen mode

We've seen the module declaration before. This one also specifies which symbols to export, someFunc in this case.

someFunc is apparently defined as the application of the String "someFunc" applied to putStrLn. putStrLn is part of the standard prelude, which is implicitly imported in every file. It's type is String -> IO () and, as you might expect, what it does is write a string to stdout (without flushing buffers).

But how can we have multiple effects? Is there some function with type IO () -> IO () -> IO () that combines the effects in both arguments and that we have to tediously add everywhere? Well, such a function exists but there's better ways to go about it.

As we've seen, IO has kind * -> *. When a type of kind * -> * follows certain rules, we say that it is a monad. In particular, for any monad m, the following functions must exist:

return :: a -> m a
fmap :: (a -> b) -> m a -> m b
(>>=) :: m a -> (a -> m b) -> m b
Enter fullscreen mode Exit fullscreen mode

IO is a monad. Substituting IO for m, we know that we have at least the following functions:

return :: a -> IO a
fmap :: (a -> b) -> IO a -> IO b
(>>=) :: IO a -> (a -> IO b) -> IO b
Enter fullscreen mode Exit fullscreen mode

return simply pretends an pure computation is impure. Similarly, fmap turns a function over pure values into one over impure values. If, for instance, we read some input string, that string has type IO String, but our pure functions only work for String! fmap remedies that problem by lifting functions to IO. (>>=) is a bit more complicated. You can think of it a bit like unix pipes. It takes some impurely computed value, and feeds that value to the next impure computation.

Let's look at a few examples, take the time to let this sink in:

computeHelloWorld :: IO String
computeHelloWorld = return "Hello, World!"

computeHelloWorldLength :: IO Int
computeHelloWorldLength = fmap length computeHelloWorld

greetTheWorld :: IO ()
greetTheWorld = computeHelloWorld >>= putStrLn
Enter fullscreen mode Exit fullscreen mode

(putStrLn is the output-writing function with type String -> IO ())

For a slightly more useful example we'll use getLine from the prelude. It has type IO String and will get a line from stdin. We can now do:

nameToGreeting :: String -> String
nameToGreeting name = "Hello " ++ name ++ ", I am monad."

greetPerson :: IO ()
greetPerson =
  fmap nameToGreeting getLine >>= putStrLn
Enter fullscreen mode Exit fullscreen mode

This is not very easy to read, and it would get a lot worse if we were to also write to stdout before reading from stdin. Luckily, Haskell has some syntactic sugar for monads in the form of do-blocks.

greetPerson :: IO ()
greetPerson =
  do
    putStrLn "I am monad, what is your name?"
    personName <- getLine
    putStrLn ("Hello, " ++ personName ++ "! What shall we *do* together?")
Enter fullscreen mode Exit fullscreen mode

The type of a do-block will be the type of its last element.

Side-note

Suppose we have the type IO (IO a). By (>>=), we know that it is equivalent to IO (). Because for any foo :: IO (IO a) we can do bar = foo >>= id, resulting in a bar :: IO a. But by return bar we get foo again! In other words, it doesn't matter how often you apply a monad to a type, it will be equivalent (technically, isomorphic) to a single application of that monad. This is what the famous phrase "monads are just monoids in the category of endofunctors" means (sort-of). A monoid being something that stays the same (type) under repeated application of a certain operation.

Conclusion

You've learned how effects can be represented in a functionally pure language by using monads. It is usually better to avoid using IO monads when possible. Trivially, we could write our entire program using monads, but we would lose many advantages of programming in Haskell.

I would personally argue that an IO monad is not a good way to represent effects, but it's the current standard for generic functionally pure programming languages. Some interesting alternatives include uniqueness types (used in Clean), free monads (Haskell) and model-update systems (Elm).

You've also been introduced to lazy evaluation. There are both advantages and disadvantages to it, but that is outside the scope of this chapter.

Final code branch for this chapter

Top comments (6)

Collapse
 
bootcode profile image
Robin Palotai

Side-discussion: free monads are still monads. And from what I get, free monads are not really that different from programming against a MonadFoo m constraint, in the sense that you can give different implementations at the call site for MonadFoo, while with the free monad you can have different interpreters.

Collapse
 
drbearhands profile image
DrBearhands

You're right, I should have said there are other ways to represent effects than using one single IO monad for everything.

Collapse
 
bootcode profile image
Robin Palotai

No no, sorry, I didn't want to suggest that. I just replied on this paragraph:

I would personally argue that monads are not a good way to represent effects, but they are the current standard for generic functionally pure programming languages. Some other interesting ways of representing effects include uniqueness types and free monads.

I would say the simpler monad tutorials are, the better :)

Thread Thread
 
drbearhands profile image
DrBearhands

I know, emphasis on one monad rather than monads ;-)

Collapse
 
lautarolobo profile image
Lautaro Lobo

I'm only here to say that I thought that your profile pic was some kind of draw, or a weird Papagayo... then I opened your profile to see it big and boom, I realized that I'm f**ing blind! 😂

Collapse
 
bittnkr profile image
Info Comment hidden by post author - thread only accessible via permalink
bittnkr

Reading this I realized how much I love JavaScript. :D

Some comments have been hidden by the post's author - find out more