For starters to Haskell, Monad and IO may be the first hurdle on the road to mastering Haskell. This article will introduce the principles and design ideas behind Monad and IO in as simple a way as possible, hoping to give some thoughts and inspiration to Haskell beginners.

This article assumes that you have some understanding of functional programming, as this is the theoretical basis for discussing Monad and IO. At the same time, this article will use some basic Haskell syntax, such as function type definitions.

This article requires a knowledge of partial functions and currying. If you don’t know, you can refer to other articles on the web. In order to ease the reader’s burden of understanding, this paper only uses the currization function definition form when necessary.

preface

We know that functions in purely functional programming must be side-effect-free. That is, these functions cannot have state or change external state (such as using global variables or printing information on the screen), and each input must correspond to a unique output.

However, real-world programs almost always require some kind of side effect. For example, a program should at least be able to output some information, or write some files, etc., otherwise the program will be meaningless. Therefore, even functional programming languages need to introduce operations that have side effects.

In Haskell, the two most important concepts related to side effects are Monad and IO. This article will introduce the two concepts from the shallow to the deep.

This article is partly translated from Noel Winstanley’s What the hell are Monads? . If you have different views on the content of this article, you are welcome to make improvements to this article.

Introduction of states

Since functions must be stateless, we might as well try to introduce states as a starting point for our discussion of IO.

Suppose we write a database system in Haskell and provide an update function to update the records in the database. The update function is defined as follows:

update: : (DB.Int) - >Bool
Copy the code

The update function writes an Int to the database and returns a Bool indicating success.

But this definition is problematic because the update function necessarily changes the state of the DB, and according to the basic rules of functional programming, the DB parameter is immutable and the UPDATE function cannot change the state of the DB.

Update (Bool); update (Bool); update (Bool); update (Bool);

update: : (DB.Int) - > (DB.Bool)
Copy the code

With this modification, we are actually able to make DB objects have internal state. (External states are a little more complicated and will be discussed later.) But this approach creates an additional problem: it complicates the composition of different operations. Suppose we also define a query function as follows:

query: :DB- > (DB.Int)
Copy the code

Our main program then needs to do the following: query the database for a value XXX and write x+1x +1x +1 back to the database. So, our main program needs to be written like this:

(x, db1) = query db
(ok, db2) = update (db1, (x + 1))
Copy the code

The problem is that we can’t just add 111 to query and pass it to update, because in addition to XXX, query returns an extra DB that doesn’t make much sense to the program itself but has to be retained and passed on to future operations. This increases the complexity of the program and the burden of code reading and maintenance.

Monad is a type level syntactic sugar to reduce the complexity of concatenating such “stateful pure functions.”

Concatenation of state transition operations

Note: The content of the following two sections is more mathematical, if you do not understand the place, you can directly skip to the “external state management” section, this will not affect the discussion of external states and IO objects.

Before introducing Monad, let’s start with a simple case. It occurs to us that the update function actually represents a state transition operation. To model and simplify such operations, we first need to currify the update function as follows:

update: :Int -> DB- > (DB.Bool)
Copy the code

After parentheses:

update: :Int- > (DB- > (DB.Bool))
Copy the code

Then we find that (DB -> DB Bool) can be abstracted into a class of functions:

type StateTransDB a = DB- > (DB.a)
Copy the code

A function of type StateTransDB actually represents a state transition operation that takes an old state DB and returns a result, A, and a new state DB.

With state transition operations, we can use a function to represent the connection between state transition operations:

dbThen: :StateTransDB a -> (a -> StateTransDB b) -> StateTransDB b
Copy the code

Given the previous operation StateTransDB a and an intermediate function f :: (a -> StateTransDB b), f takes the return value of the previous operation A and returns the second operation StateTransDB b. DbThen returns the concatenation of the two operations. For example, the following function:

queryAndUpdate: :DB- > (DB.Int)
queryAndUpdate db = query db `dbThen` \x -> update (x + 1)
Copy the code

The first operation query and the second operation update are concatenated as follows: For the result XXX returned by query, call \x -> update(x+1) to perform update(x+1)update(x +1).

The queryAndUpdate operation is actually a composite of two operations, and we can perform both operations at once by calling queryAndUpdate DB and getting the final Bool result and the updated DB.

Monad

The above type design is actually universal. To easily express this type of operation, the concept of Monad has been introduced into Haskell. Monad itself is not stateful or side-effecting, but a special type that can act as syntactic sugar when concatenation of state transition operations is required.

A simple Monad type can be defined as follows:

class Monad m where
  >>= :: m a -> (a -> m b) -> m b
  >> :: m a -> m b -> m b
  return :: a -> m a

  m >> k = m >>= \_ -> k
Copy the code

This definition is very complicated, especially since there are four unknown types A, B, M, and K, it is difficult to infer their purpose from the type itself.

Using the database system above as an example, the database system can actually be represented as a Monad StateTransDB. Then, the most important type of >>= operation is:

> > = : :StateTransDB a -> (a -> StateTransDB b) -> StateTransDB b
Copy the code

This is actually the dbThen function we defined above. With Monad in place, our queryAndUpdate function can be written as follows:

queryAndUpdate db = query db >>= (\x -> update (x + 1))
Copy the code

Now we can continue to explain the remaining two operations (>> and return) :

> > : :StateTransDB a -> (StateTransDB b -> StateTransDB b)
  StateTransDB >> k = StateTransDB >>= \_ -> k

  return :: a -> StateTransDB a
Copy the code

>> represents ignoring the return value of the previous operation. Such as:

update (db, 42) >> query
Copy the code

This operation first writes 42 to the database, ignores the returned Bool, and then queries the database to return the result.

Return returns an empty state transition operation that returns the given value. For example, execute the following code:

ret42 = return 42
(db1, x) = ret42 db
Copy the code

After that, x = 42, and db1 and DB should be in the same state.

To implement Monad against StateTransDB, we simply need to:

instance Monad StateTransDB where
  (>>=) = dbThen
  return a = (\db -> (db, a))
Copy the code

Management of external state

Next we discuss managing external state. In the case of our database system, we only implemented internal state. But the actual program will inevitably manipulate external states, such as files on disk, on-screen displays, network sockets, and so on. Let’s assume that the entire external state is stored in a type called World.

Compared with internal states, external states are non-copiable and non-traceable. That is, it is not possible to store the World in a constant, call two different operations with the same World, and observe what happens on two different World lines.

Above, our design of “pure stateful functions” clearly does not satisfy these two constraints. As long as we can read the return value of an operation, we can make a copy of the World object, breaking both of these restrictions.

Different functional programming languages have different solutions to this problem, such as the Clean language, which extends the type system to limit World objects to being used only once, called uniqueness Type. A special type called IO is used in Haskell to prevent user programs from obtaining and copying World objects, and to make World objects always flow in one direction.

IO object

In Haskell, an IO object encapsulates an operation (such as an I/O operation) that has side effects. PutStrLn “Hello world!” Returns an IO object that stands for Hello World! This operation. With Monad, multiple I/O objects can be connected in series to one I/O object to perform I/O operations sequentially.

The above description may sound nonsensical, but we just need to look at the definition of IO objects:

type IO a = World- > (World.a)
Copy the code

That is, each IO object is actually a function that takes a World object and returns a modified World object. Since the function has World as an argument, you can naturally do anything that has side effects, including but not limited to I/O. (In fact, managing external state is essentially an I/O operation.)

However, how to generate the IO objects we need becomes a problem. For example, if we need to print a string to the screen, we might need to define a two-argument function like this:

println: : (String.World) - > (World, (),Copy the code

But that’s not the definition of IO. This can be solved by means of a function Currization:

println: :String- > (World- > (World, ()))
Copy the code

This definition is simplified to become:

println: :String -> IO(a)Copy the code

That’s the definition of the system function putStrLn.

In Haskell, IO is defined as an abstract data type, and only system libraries and Monad can construct IO objects. This avoids exposure of the World object.

IO series with Monad

Monad IO is unquestionably implemented in the Haskell standard library. The >>= operator in Monad IO is defined as follows:

> > = : :IO a -> (a -> IO b) -> IO b
Copy the code

This is very similar to the state transition function we discussed above.

To further simplify the code, do syntax is provided in Haskell. A classic example is:

sayHello = do
  putStrLn "What is your name?"
  name <- getLine
  putStrLn $ "Hello, " ++ name
Copy the code

Here, we are mainly concerned with what the syntactic sugar looks like when unwrapped. Is simple:

sayHello = \
  putStrLn "What is your name?" >> \
  getLine >>= \
  (\name -> putStrLn $ "Hello, " ++ name)
Copy the code

The data flow is as follows:

  • World_0 –putStrLn "What is your name?"– > World_1
  • World_1 –getLine– > (World_2,name)
  • IO_3 = putStrLn $ "Hello, " ++ name
  • World_2 –IO_3– > World_3

Bonus: FFI in Haskell

Modern programming languages inevitably use libraries written in other languages. Windows apis, for example, are provided to programmers in the form of C language interfaces. The technique of calling other languages in one language is called FFI (Foreign Function Interface).

But a fundamental problem with calling FFI functions in functional programming languages like Haskell is that FFI functions almost always have side effects, which breaks the basic rules of functional programming.

In Haskell, FFI functions are not heavily restricted, perhaps for the sake of brevity and binding convenience. Such as the following statement:

foreign import ccall "exp" c_exp :: Double -> Double
Copy the code

Defines a pure function double exp(double); FFI interface.

If the target function has side effects, declare the return value type as IO, for example:

foreign import ccall "my_func" myFunc :: Int -> IO Double
Copy the code

If you want to learn more about Haskell FFI, see the Haskell Wiki.

Bonus: Generalized Monad

In the previous article, we introduced the method of serial state transition operation with Monad. But Monad actually has a wider range of uses.

Recall the definition of the >>= operation in Monad:

>>= :: m a -> (a -> m b) -> m b
Copy the code

In the above article, we substituted StateTransDB into M to realize the concatenation of database state transition operations. But StateTransDB is a complex type (state transition function), so can we plug a simpler type into M?

You probably know that the Haskell library has a type called Maybe. It is defined as follows:

data Maybe a = Just a | Nothing
Copy the code

That is, a Maybe a represents a nullable A object that may contain an A value or nothing, similar to a pointer in C or Option< a > in Rust.

Like the state transition function, the Maybe type brings up an additional problem: if a function returns Maybe a, then in our code we have to expand the Maybe a into a by pattern matching in order to proceed with the call.

Suppose we define an immutable database DB, whose query operation is called doQuery, as follows:

doQuery: : (DB.Query) - >Maybe Record
Copy the code

Suppose we need to run a multi-tier nested query. Without syntactic sugar, our code might look like this:

r = case doQuery (db, q1) of
  Nothing -> Nothing
  Just r1 -> case doQuery (db, (q2 r1)) of
    Nothing -> Nothing
    Just r2 -> case doQuery (db, (q3 r2)) of
      Nothing -> Nothing
      Just r3 -> ...
Copy the code

This causes the same problem as above, and because of the existence of pattern matching, the code layer is severely deepened, resulting in the programmer is very taboo “arrow shape nesting”.

To solve this problem, we can define a then function as above:

then: :Maybe a -> (a -> Maybe b) -> Maybe b

m `then` f = case m of
  Nothing -> Nothing
  Just a -> f a
Copy the code

The then function is used to give the result of the previous operation Maybe a and an intermediate function that prints Maybe b and the result of the intermediate function Maybe b.

With the then function, our multi-layer nested query code can be improved to look like this:

r = doQuery (db, q1) `then` \r1 ->
    doQuery (db, (q2 r1)) `then` \r2 ->
    doQuery (db, (q3 r2)) `then` \r3 -> ...
Copy the code

In the Haskell library, the Maybe type also implements Monad, and the then function type is actually the >>= operation in Monad Maybe:

> > = : :Maybe a -> (a -> Maybe b) -> Maybe b
Copy the code

With the help of Haskell’s DO syntax, the above code can be further simplified to:

r = do
  r1 <- doQuery (db, q1)
  r2 <- doQuery (db, (q2 r1))
  r3 <- doQuery (db, (q3 r2))
  ...
  return rn
Copy the code

Note: The above code may create the illusion that the latter operation will be performed regardless of whether the former operation was successful or not. This illusion is caused by Monad Maybe’s definition of the >>= operation, because the >>= operation does not call an intermediate function when it encounters Nothing and does not trigger subsequent operations. Getting rid of this illusion, caused by the stereotype of imperative programming languages, requires practice beyond understanding the logic behind Monad and DO.

If we abstract away the above construction, we can see that Maybe actually represents a type wrapper that adds some additional functionality to existing types (state management in the case of state transition functions, nullable in the case of Maybe). In cases where type wrappers need to be unwrapped to expose the inner types, Monad as a programming paradigm can reduce code complexity and, for a skilled programmer, reduce the burden of reading code.

If you’re interested in using Monad in functional programming languages, check out another article: Monad – Angry Bugs on Huhu in 15 Minutes