From c1514ee89fbcfd432db184fb23d4520815fe85af Mon Sep 17 00:00:00 2001 From: Michael Snoyman Date: Sun, 11 Sep 2016 11:33:43 +0300 Subject: [PATCH] Monads are like Lannisters --- posts.yaml | 3 + posts/monads-are-like-lannisters.md | 366 ++++++++++++++++++++++++++++ 2 files changed, 369 insertions(+) create mode 100644 posts/monads-are-like-lannisters.md diff --git a/posts.yaml b/posts.yaml index 4124532..d1d7fdd 100644 --- a/posts.yaml +++ b/posts.yaml @@ -1,3 +1,6 @@ +- file: posts/monads-are-like-lannisters.md + title: "Monads are like Lannisters" + day: 2016-09-12 - file: posts/appveyor-haskell-windows-ci.md title: "Using AppVeyor for Haskell+Windows CI" day: 2016-08-31 diff --git a/posts/monads-are-like-lannisters.md b/posts/monads-are-like-lannisters.md new file mode 100644 index 0000000..6ea8c24 --- /dev/null +++ b/posts/monads-are-like-lannisters.md @@ -0,0 +1,366 @@ +As many people are likely aware, monads (incorrectly) have a bad rap +in the programming community for being difficult to learn. A string of +extremely flawed monad tutorials based on analogies eventually led to +a +[blog post by Brent Yorgey](https://byorgey.wordpress.com/2009/01/12/abstractsion-intuition-and-the-monad-tutorial-fallacy/) +about the flaws of this analogy-based approach. And we've seen +[great](http://haskellbook.com/) +[learning](http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html) +[materials](https://haskell-lang.org/documentation) on Haskell and +monads. + +However, I'm disappointed to see the analogy-based route +disappear. Based on a +[recent Twitter poll](https://twitter.com/snoyberg/status/773892889868963841) +I ran, which is held to the highest levels of scientific and +statistical scrutiny, it's obvious that there is very high demand for +a rigorous analogy-based monad tutorial. I claim that the flaw in all +previous analogy based tutorials is lack of strong pop culture +references. Therefore, I'm happy to announce the definitive guide to +monads: Monads are like Lannisters. + +Spoiler alert: if you haven't completed book 5 or season 6 of Haskell +yet, there _will_ be spoilers. + +## Prereqs + +The examples below will all be Haskell scripts that can be run with +the Stack build tool. Please +[grab Stack to play along](https://haskell-lang.org/get-started). Copy-paste +the full example into a file like `foo.hs` and then run it with `stack +foo.hs`. (This uses Stack's +[script interpreter support](https://www.fpcomplete.com/blog/2016/08/bitrot-free-scripts).) + +## Hear me roar + +Many people believe that the Lannister house words are "A Lannister +always pays his debts" (or her debts of course). We'll get to that +line in a moment. This belief however is false: the _true_ Lannister +house words are **Hear me roar**. So let's hear a Lannister roar: + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +roar :: IO () +roar = putStrLn "Roar" + +main :: IO () +main = do + roar -- Tywin + roar -- Cersei + roar -- Jaime + roar -- Tyrion +``` + +Roaring is clearly an _output_, and therefore it makes sense that our +action is an `IO` action. But roaring doesn't really do much besides +making sound, so its return is the empty value `()`. In our `main` +function, we use `do` notation to roar multiple times. But we can just +as easily use the `replicateM_` function to replicate a monadic action +multiple times and discard (that's what the `_` means) the results: + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +import Control.Monad (replicateM_) + +roar :: IO () +roar = putStrLn "Roar" + +main :: IO () +main = replicateM_ 4 roar +``` + +## Tyrion, the scholar + +As we all know, Tyrion is a prolific scholar, consuming essentially +any book he can get his hands on (we'll discuss some other consumption +next). Fortunately, monads are there to back him up, with the `Reader` +monad. Let's say that Tyrion is doing some late night research on wine +production (epic foreshadowment) in various kingdoms, and wants to +produce a total: + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +import Data.Map (Map) +import qualified Data.Map as Map +import Control.Monad.Trans.Reader +import Data.Maybe (fromMaybe) + +type Kingdom = String +type WineCount = Int +type WineData = Map Kingdom WineCount + +tyrionResearch :: Reader WineData Int +tyrionResearch = do + mnorth <- asks $ Map.lookup "north" + mriverlands <- asks $ Map.lookup "riverlands" + return $ fromMaybe 0 mnorth + fromMaybe 0 mriverlands + +main :: IO () +main = print $ runReader tyrionResearch $ Map.fromList + [ ("north", 5) + , ("riverlands", 10) + , ("reach", 2000) + , ("dorne", 1000) + ] +``` + +While Tyrion may have chosen inferior kingdoms for wine production, it +does not take away from the fact that the `Reader` type has allowed +him access to data without having to explicitly pass it around. For an +example this small (no dwarf joke intended), the payoff isn't +great. But `Reader` is one of the simplest monads, and can be quite +useful for larger applications. + +## Tyrion, the drinker + +Let's try another one. We all know that Tyrion also likes to _drink_ +wine, not just count it. So let's use our `Reader` monad to give him +access to a bottle of wine. + +_However_, unlike reading data from a book, drinking from a bottle +actually changes the bottle. So we have to leave our pure `Reader` +world and get into the `ReaderT IO` world instead. Also known as +_monad transformers_. (Unfortunately I don't have time now for a full +post on it, but consider: __Transformers: Monads in Disguise__.) + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +import Control.Monad (replicateM_) +import Control.Monad.Trans.Reader +import Control.Monad.IO.Class +import Control.Concurrent.Chan + +data Wine = Wine +type Bottle = Chan Wine + +drink :: MonadIO m => Bottle -> m () +drink bottle = liftIO $ do + Wine <- readChan bottle + putStrLn "Now I'm slightly drunker" + +tyrionDrinks :: ReaderT Bottle IO () +tyrionDrinks = replicateM_ 10 $ ReaderT drink + +main :: IO () +main = do + -- Get a nice new bottle + bottle <- newChan + + -- Fill up the bottle + replicateM_ 20 $ writeChan bottle Wine + + -- CHUG! + runReaderT tyrionDrinks bottle +``` + +## A Lannister always pays his debts + +What is a debt, but receiving something from another and then +returning it? Fortunately, there's a monad for that too: the `State` +monad. It lets us take in some value, and then give it back - perhaps +slightly modified. + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +import Control.Monad.Trans.State + +type Sword = String + +forge :: State Sword Sword +forge = do + "Ice" <- get + -- do our Valyrian magic, and... + + -- Repay our debt to Brienne + put "Oathkeeper" + + -- And Tywin gets a sword too! + return "Widows Wail" + +killNedStark :: IO Sword +killNedStark = do + putStrLn "Off with his head!" + return "Ice" + +main :: IO () +main = do + origSword <- killNedStark + + let (forTywin, forBrienne) = runState forge origSword + + putStrLn $ "Tywin received: " ++ forTywin + putStrLn $ "Jaime gave Brienne: " ++ forBrienne +``` + +Not exactly justice, but the types have been satisfied! A monad always +pays its debts. + +## Throwing + +Monads are also useful for dealing with exceptional cases: + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +import Control.Exception +import Data.Typeable (Typeable) + +data JaimeException = ThrowBranFromWindow + deriving (Show, Typeable) +instance Exception JaimeException + +jaime :: IO () +jaime = do + putStrLn "Did anyone see us?" + answer <- getLine + if answer == "no" + then putStrLn "Good" + else throwIO ThrowBranFromWindow + +main :: IO () +main = jaime +``` + +## Killing + +And thanks to asynchronous exceptions, you can also kill other threads +with monads. + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +import Control.Concurrent +import Control.Exception +import Control.Monad +import Data.Typeable (Typeable) + +data JaimeException = Defenestrate + deriving (Show, Typeable) +instance Exception JaimeException + +bran :: IO () +bran = handle onErr $ forever $ do + putStrLn "I'm climbing a wall!" + threadDelay 100000 + where + onErr :: SomeException -> IO () + onErr ex = putStrLn $ "Oh no! I've been killed by: " ++ show ex + +jaime :: ThreadId -> IO () +jaime thread = do + threadDelay 500000 + putStrLn "Oh, he saw us" + throwTo thread Defenestrate + threadDelay 300000 + putStrLn "Problem solved" + +main :: IO () +main = do + thread <- forkIO bran + jaime thread +``` + +Exercise for the reader: modify `bran` so that he properly recovers +from that exception and begins warging instead. (WARNING: +[don't recover from async exceptions in practice](https://haskell-lang.org/library/safe-exceptions).) + +## Exiting + +You can also exit your entire process with monads. + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +import System.Exit + +tommen :: IO () +tommen = do + putStrLn "Oh, my dear wife!" + exitFailure + +main :: IO () +main = tommen +``` + +## Passing the baton of reign + +We've seen Lannisters pass the baton of reign from family member to family member. We've also seem them ruthlessly destroy holy insitutions and have their private, internal affairs exposed. As it turns out, we can do all of that with some explicit [state token](https://wiki.haskell.org/Evaluation_order_and_state_tokens) manipulation! + +```haskell +#!/usr/bin/env stack +-- stack --resolver lts-6.15 --install-ghc runghc + +{-# LANGUAGE MagicHash #-} +{-# LANGUAGE UnboxedTuples #-} +import GHC.Prim +import GHC.Types + +joffrey :: IO () +joffrey = do + putStrLn "Look at your dead father's head Sansa!" + putStrLn "Oh no, poisoned!" + +tommen :: IO () +tommen = do + putStrLn "I'm in love!" + putStrLn "*Swan dive*" + +cersei :: IO () +cersei = undefined -- season 7 isn't out yet + +unIO :: IO a -> State# RealWorld -> (# State# RealWorld, a #) +unIO (IO f) = f + +main :: IO () +main = IO $ \s0 -> + case unIO joffrey s0 of + (# s1, () #) -> + case unIO tommen s1 of + (# s2, () #) -> + unIO cersei s2 +``` + +## Honorable mentions + +There's much more to be done with this topic, and there were other +ideas besides Lannisters for this post. To give some mention for other +ideas: + +* There's plenty of other Lannister material to work with (skinning + animals, Jaime and Cersei affairs, or Tyrion proclivities). But I + had to draw the line somewhere (both for length of post and topics I + felt like discussing...). Feel free to comment about other ideas. +* One of my favorites: [monads are like onions](https://twitter.com/00Syssiphus/status/774240234766819332) +* Monads are [just your opinion, man](https://twitter.com/GabrielG439/status/774234282323697664) +* "they're like Gargoyles, they decorate the public facing interface + of important landmarks in Haskell-land." + [link](https://twitter.com/tritlo/status/774235047780032512) + +Personally, I was going to go with a really terrible "monads are like +printers" based on them taking plain paper in and spitting out printed +pages, but Lannisters was a lot more fun. Also, I'm sure there's +_something_ great to be done with presidential candidates, at the very +least `throwTo opponent Insult`. + +## Disclaimer + +Since I'm sure _someone_ is going to get upset about this: YES, this +post is meant entirely as parody, and should not be used for actually +learning anything except some random details of Game of Thrones, and +perhaps a bit of Haskell syntax. Please use the learning materials I +linked at the beginning for actually learning about Haskell and +monads. And my real advice: don't actually "learn monads," just start +using Haskell and you'll pick them up naturally.