Haskell: A Great Procedural Language
We are starting to see the benefits of side effects as first class values, so
let’s shift to a higher gear. We have seen that Haskell allows us to store side
effect objects in variables without accidentally executing their effects. The
next step is storing these side effect objects in data structures.
We could, for example, create a list of three different ways to get the username
of the currently logged in user.
getting_usernames :: [IO (Maybe String)] getting_usernames = [lookupEnv "USER", lookupEnv "LOGNAME", lookupEnv "SUDO_USER"]
This list cannot be executed for its side effects directly, because the list
itself is not a side effect – it’s a list of side effects. There are library
functions to deal with this, though. One is
sequenceA :: [IO a] -> IO [a]
This is what we need in this case: it takes a list of side effect objects and
creates a new side effect object that executes all the side effects of the list,
and then produces a list of all the values produced by those side effects. To
make a side effect that produces a list of candidate usernames, we define
actual_usernames :: IO [Maybe String] actual_usernames = sequenceA getting_usernames
If we execute this side effect and print the result (either by connecting it up
with print using the >>= operator, or in a do block), then on my system we
get the result
[Just "kqr", Just "kqr", Nothing]
Sometimes we have a list of side effects but we don’t care about the value they
produce. This might be the case if we have collected a bunch of log statements
from a pure function. We want to execute their side effects (the actual logging
action) but we don’t care about what the log function itself returns (it is
usually a void or unit-type value.)
log_statements :: [IO ()] log_statements = [ log Info "Creating user", log Warn "User already found" log Info "Updating user" ]
As a reminder: these function calls to the log function do not cause anything
to be logged. The log function returns a side effect object that, when
executed, makes the logging happen. The log_statements variable contains a
list of such side effect objects – it is not itself a side effect object.
To execute these, we can again combine the side effects in the list into one
side effect object with sequenceA. When we do so, we get a side effect object
that produces the value [(), (), ()]. To get the code to type check, we may
have to discard this value. We already know how to do this, because discarding
values is what the *> operator does.
execute_logged :: IO () execute_logged = sequenceA log_statements *> pure ()
When the side effect of execute_logged runs, it will run the side effects of
the log statements and then discard the dummy values produced in the process.
Remember that loaded die from before? Now we can check that it indeed always
returns the same number. First we create a list by repeating the same
side effect object an infinite number of times.
many_loaded_dice :: [IO Int] many_loaded_dice = repeat loaded_die
Then we construct a side effect object that executes the first few of these and
keeps the values they produced.
some_thrown_dice :: IO [Int] some_thrown_dice = sequenceA (take 20 many_loaded_dice)
If we connect this up with a print (again, do block or the >>=
operator) and execute it, we get
[4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4]
We could do the same thing with a better die:
many_good_dice :: [IO Int] many_good_dice = repeat (randomRIO (1,6)) some_thrown_dice :: IO [Int] some_thrown_dice = sequenceA (take 20 many_good_dice)
If we print this once, we get
[2,1,4,1,3,2,2,2,1,4,6,4,1,4,6,4,5,3,4,6]
If we print it again, we might get
[4,5,3,2,4,2,5,4,6,1,1,5,1,3,6,4,4,5,1,4]
Even though we constructed the list by repeating the same side effect object,
we get a fresh set of random numbers every time the composite side effect object
is executed. This is additional proof that what we store in the list is not the
result of the side effect, but the side effect itself.
But also note what we did. We used list functions (repeat, take 20) to
manipulate a data structure of side effect objects as if they were regular
values – because they are! Then we used a side effect manipulation function
(sequenceA) to combine the side effects in the list into one new side effect
object that executes all of them. This is a kind of meta programming, except
it’s not using a special macro language but performed at the level of regular
values.



