This article was originally posted on the on February 20th, 2017. Check out for more Haskell content! Monday Morning Haskell blog the blog So last week we discussed what a monad is. It isn’t some scary thing only wizards with arcane knowledge of category theory can understand. It’s just a with a couple functions describing a particular context. These functions, when used properly, can what we can do while keeping our code purely functional. type class dramatically expand We haven’t gone over all the “laws” these functions need to follow. But if we explore enough examples, we’ll have an intuitive grasp of what should happen. We saw some simple examples last time with the , , and monads. In this article, we will look at the and monads. Maybe Either IO Reader Writer Global Variables (or a lack thereof) In Haskell, our code is generally “pure”, meaning functions can with the to them. This effectively means we cannot have global variables. We can have global expressions, but these are fixed at compile time. If user behavior might change them, we have to wrap them in the monad, which means they can’t be used from pure code. only interact arguments passed IO Consider this example where we might want to have an containing different parameters as a global variable. However, we might have to load these from a config file or a command line interface, which requires the monad. Environment IO main :: IO ()main = do env <- loadEnv let str = func1 env print str data Environment = Environment { param1 :: String , param2 :: String , param3 :: String } loadEnv :: IO EnvironmentloadEnv = … func1 :: Environment -> Stringfunc1 env = “Result: “ ++ (show (func2 env)) func2 :: Environment -> Intfunc2 env = 2 + floor (func3 env) func3 :: Environment -> Floatfunc3 env = … -- Some calculation based on the environment The only function is . However is an impure function. This means it cannot directly call , an impure function. This means the environment has to be passed through as a variable to the other functions, just so they can ultimately pass it to . In a language with global variables, we could save as a global value in . Then could access it directly. There would be no need to have it as a parameter to and . In larger programs, these “pass-through” variables can cause a lot of headaches. actually using the environment func3 func3 loadEnv func3 env main func3 func1 func2 The Reader Solution The Reader monad solves this problem. It effectively creates a global read-only value of a specified type. All functions within the monad can “read” the type. Let’s look at how the monad changes the shape of our code. Our functions the as an , as they can access it through the monad. Reader no longer need Environment explicit parameter main :: IO ()main = do env <- loadEnv let str = runReader func1 env print str data Environment = Environment { param1 :: String , param2 :: String , param3 :: String } loadEnv :: IO EnvironmentloadEnv = … func1 :: Reader Environment Stringfunc1 = do res <- func2 return (“Result: “ ++ (show res)) func2 :: Reader Environment Intfunc2 = do env <- ask let res3 = func3 env return (2 + (floor res3)) func3 :: Environment -> Float... The function so we can use it. The monad’s bind action allows us to glue different actions together together. In order to call a reader action from pure code, all we need to do is call the function and supply the environment as a parameter. All functions within the action will be able to treat it like a global variable. ask unwraps the environment Reader runReader It might not seem like we’ve accomplished much, but our code is much more intuitive now. We keep as it was. It makes sense to describe it as a function from an to a value. However, our other two functions no longer take the environment as an explicit parameter. They simply exist in a where the . func3 Environment context environment is a global variable Accumulating Values Now, to motivate the Writer monad, let’s talk about the accumulation problem. Suppose we have a few different functions. Each will perform some string operations we’ve assigned an arbitrary “cost” to. We want to keep track of how “expensive” it was to run the full computation. We can do this by using to keep track of the cost we’ve seen so far. We then keep passing the accumulated value along. accumulator arguments -- Calls func2 if even length, func3 and func4 if oddfunc1 :: String -> (Int, String)func1 input = if length input `mod` 2 == 0 then func2 (0, input) else (i1 + i2, str1 ++ str2) where (i1, str1) = func3 (0, tail input) (i2, str2) = func4 (0, take 1 input) -- Calls func4 on truncated versionfunc2 :: (Int, String) -> (Int, String)func2 (prev, input) = if (length input) > 10 then func4 (prev + 1, take 9 input) else (10, input) -- Calls func2 on expanded version if a multiple of 3func3 :: (Int, String) -> (Int, String)func3 (prev, input) = if (length input) `mod` 3 == 0 then (prev + f2resI + 3, f2resStr) else (prev + 1, tail input) where (f2resI, f2resStr) = func2 (prev, input ++ "ab") func4 :: (Int, String) -> (Int, String)func4 (prev, input) = if (length input) < 10 then (prev + length input, input ++ input) else (prev + 5, take 5 input) However, an isn’t the only type of value we could accumulate. We could instead be accumulating a list of strings to print as log messages so we know what computations were run. There is a : the typeclass. Int generalization of this behavior Monoid The Monoid Typeclass In this example, is a simple example of a Monoid. Let’s look at the monoid typeclass definition: Int class Monoid a where mempty :: a mappend :: a -> a -> a This is effectively an . It defines two functions. The function is an initial value for our monoid. Then with , we can combine two values of this type into a result. It is quite easy to how we can make a monoid instance for : accumulation class mempty mappend Int instance Monoid Int where memty = 0 mappend a b = a + b Our accumulator starts at 0, and we combine values by adding them. Using Writer to Track the Accumulator The monad is parameterized by some monoidal type. Its main job is to keep track of an accumulated value of this type. So it’s operations live in the context of having a global value that they can modify in this particular way. We can change our code examples above to use the monad as follows: Writer Writer func1 :: String -> (String, Int)func1 input = if length input `mod` 2 == 0 then runWriter (func2 input) else runWriter $ do str1 <- func3 input str2 <- func4 (take 1 input) return (str1 ++ str2) func2 :: String -> Writer Int Stringfunc2 input = if (length input) > 10 then do tell 1 func4 (take 9 input) else do tell 10 return input func3 :: String -> Writer Int Stringfunc3 input = if (length input) `mod` 3 == 0 then do tell 3 func2 (input ++ “ab”) else do tell 1 return $ tail input func4 :: String -> Writer Int Stringfunc4 input = if (length input) < 10 then do tell (length input) return (input ++ input) else do tell 5 return (take 5 input) Notice we no longer need to actually of the accumulator. It is now wrapped by the monad. We can increase it in any of our functions by calling “tell”. Now our code is much simpler and our types are cleaner. explicitly keep track Writer Conclusion The Reader and Writer monads both offer to deal with . The monad allows you to keep track of a shared global state. It allows you to avoid passing that state as an explicit parameter to functions that don’t really use it. The monad allows you to keep track of a global accumulated value using a monoid. If you want to see how to combine these ideas together, head over to the and check out the latest article there on the ! pure functional ways common side effects Reader Writer Monday Morning Haskell Blog State Monad Hopefully this article has helped convinced you that monads (and Haskell for that matter) aren’t all that scary! If this has inspired you to pick up Haskell and start writing some code, check out our free for getting stated! checklist Not quite ready for monads but want to try some different Haskell skills? Check out our . It includes 2 chapters of material on recursion and higher order functions, as well as 10 practice problems with a test harness. recursion workbook
Share Your Thoughts