Haskell for Elm developers: giving names to stuff (Part 8 – IO)

cekrem1 pts0 comments

Haskell for Elm developers: giving names to stuff (Part 8 - IO)

🥷🏻"<br>/>

🌙<br>🌞

Welcome back! In my last post, we explored Traversable and discovered that “the answer is always traverse”. Today we are going to tackle something that scares a LOT of newcomers to Haskell, and yet — surprise, surprise! — you have been doing it in Elm all along: IO ! 🎉

Funnily enough, all the way back in Part 1 I promised I would eventually write about IO… only took me 8 posts to keep my word! 😅

There is a famous question that every Elm developer eventually asks when they peek over the fence into Haskell: “Wait… if Haskell is pure, how does it print to the screen, read files or make HTTP requests?” 🤔

The answer is IO, and the beautiful thing is that the mental model is exactly the one you already have from Elm’s Task and Cmd. Let me prove it to you! 😉

Quick refresher before we dive in, since we will lean on both concepts: in Elm, a Task is a composable description of an effect — you can chain tasks with Task.andThen, map over them, and they carry a typed error channel. A Cmd is the one-shot instruction you actually hand to the Elm runtime from init/update; it is fire-and-forget, and the only way it talks back to you is through a message. The two meet at Task.perform/Task.attempt, which turn a Task into a Cmd — because the runtime only accepts Cmds. Keep that pipeline (Task ➝ Cmd ➝ runtime) in mind: it maps directly onto what Haskell does with IO (which stands for Input/Output, btw!).

The big misconception

The most common misconception about IO is thinking that an IO a value is the side effect. It is not! An IO a is a description of an effectful computation that produces a value of type a when (and only when) it is eventually run.

Does that sound familiar? It should! It is precisely how the Elm docs describe a Task:

A task is a description of something that needs to be done. […] The actual benefit of a task is that it does not do anything until you give it to the runtime.

Swap the word “task” for “IO action” and you have a perfect definition of IO in Haskell. An IO a value is a recipe , not the act of cooking. You can pass it around, store it in a list, combine it with other recipes — and nothing happens until the runtime decides to actually run it. 🍳

greet :: IO ()<br>greet = putStrLn "Hello, Elm developer!"

Defining greet does not print anything. greet is just a value of type IO () — a description that says “when run, print this line”. Referential transparency is preserved! ✨

IO is a Monad (of course it is 🙃)

If you have been following the series, you already know everything you need to use IO, because (you guessed it!) IO is a Monad! (Go back and read Part 3 - Monads! if you need a refresher.)

That means it has all the machinery you already love:

fmap :: (a -> b) -> IO a -> IO b -- Functor<br>pure :: a -> IO a -- Applicative<br>(>>=) :: IO a -> (a -> IO b) -> IO b -- Monad

And just like with Task, the way you chain effectful steps together is with the monadic bind (>>=), which is the equivalent of Elm’s Task.andThen! Compare them side by side:

(>>=) :: IO a -> (a -> IO b) -> IO b<br>Task.andThen :: (a -> Task x b) -> Task x a -> Task x b -- (args flipped)

So this little program that asks for your name and greets you (Elm runs in the browser, so there are no real console Tasks — bear with me and imagine these getLine/putLine exist just to show the shape)…

import Task

greetUser : Task x ()<br>greetUser =<br>getLine<br>|> Task.andThen (\name -> putLine ("Hello, " ++ name ++ "!"))

…is, in Haskell, basically identical:

greetUser :: IO ()<br>greetUser =<br>getLine >>= \name -> putStrLn ("Hello, " <> name <> "!")

And thanks to do notation (remember Part 3?), we can write it in that lovely imperative-looking style:

greetUser :: IO ()<br>greetUser = do<br>name getLine<br>putStrLn ("Hello, " <> name <> "!")

That name is exactly the do notation desugaring of >>= we saw for Task. Same Monad, same intuition, different effect. 💜

So where is the Cmd? Enter main

Here is where the most interesting comparison lives. In Elm, you can build a Task all day long, but a Task never runs on its own . To actually make something happen, you have to hand it to the runtime by turning it into a Cmd:

Task.perform : (a -> msg) -> Task Never a -> Cmd msg<br>Task.attempt : (Result x a -> msg) -> Task x a -> Cmd msg

A Cmd is the Elm Architecture’s way of saying “hey runtime, please go run this effect and send me a message back when you are done”. You never run the effect — the Elm runtime does, at the boundary of your program. Your update function stays beautifully pure: it just returns Cmds as data. 📨

Haskell works exactly the same way, except the “boundary” has a name you already know:

main :: IO ()

main is the single IO action that the Haskell runtime (the RTS) actually executes when your program starts. Everything you build is just data — descriptions of effects — until it becomes part of main. In other words:

main is to Haskell what the Elm runtime is...

task haskell runtime name part greetuser

Related Articles