In this “monad tutorial” we are not going to learn what monads are, how they are implemented or how they work internally. Instead we are going to look at ten example use cases. We will look at monadic APIs for safe concurrency, build dependencies, stream processing, probabilistic programming, web server programming, mutable references, logic programming, test specification, parsing and HTML5 canvas manipulation.
There is a repository on github with a minimal complete example for each monad discussed. If you want to play with monads you should clone that repository and start from there. Each example in the repository comes with an idea for a small extension. In this post we will only look at a snippet from each example.
The probability monad allows you to describe and compose probability distributions. The following snippet describes the normalized probability of possible outcomes when you roll two twenty-sided dice and take the maximum of the two rolls.
enemyRolls :: Probability Int enemyRolls = norm (do firstRoll <- twentySided secondRoll <- twentySided return (max firstRoll secondRoll))
Check out the full example to see how we can compose this probability distribution with others to calculate your odds of winning in a made-up role-playing game scenario.
STM stands for software transactional memory. The following swaps the content of two variables in a transaction.
swapTVars :: TVar Int -> TVar Int -> STM () swapTVars tvar1 tvar2 = do value1 <- readTVar tvar1 value2 <- readTVar tvar2 writeTVar tvar1 value2 writeTVar tvar2 value1
We can execute this transaction atomically. Or we can compose it with other transactions into one big transaction before doing so. In the full example we fork 100001 threads that concurrently try to swap the content of the same two variables.
In software transactional memory we do not block other threads when we want to execute a transaction atomically. We just execute it, watch out for conflicts and roll back if necessary. This rollback is always possible because we know that actions in the
STM monad do not have side effects.
Shake is a build system embedded into Haskell. The following snippet describes a build action. It reads a newline-delimited list of file names from the file
"shake-data/filenames". It sums the total number of lines in all listed files and writes the result to the file at
linecountAction :: FilePath -> Action () linecountAction linecountPath = do fileNames <- readFile' "shake-data/filenames" fileContents <- forM (lines fileNames) readFile' let linecount = sum (map (length . lines) fileContents) writeFile' linecountPath (show linecount)
When we run this program it remembers which files it read and wrote and skips work when we invoke it a second time and nothing changed.
Sometimes it is easier or more efficient to express an algorithm imperatively with explicit memory mutation instead of declaratively with immutable data. The histogram function in the following snippet is such an example. In Haskell we use the
ST (state thread) monad for this.
histogramST :: Vector Word8 -> ST s (MVector s Int) histogramST vector = do resultVector <- MVector.new 256 MVector.set resultVector 0 Vector.forM_ vector (\value -> do MVector.modify resultVector (+1) (fromIntegral value)) return resultVector
The full example exposes a pure
histogram function that uses the
histogramST function internally and freezes the resulting vector afterwards. This is an example of how monads allow us to keep side-effects local.
A pipe awaits values from upstream and yields values to downstream. The following snippet is a pipe that awaits messages from upstream and only yields them to downstream when the time since the previous message is larger than 3.
rateLimitLoop :: (Monad m) => Message -> Pipe Message Message m r rateLimitLoop previousMessage = do message <- await let timeDifference = timestamp message - timestamp previousMessage if timeDifference > 3 then do yield message rateLimitLoop message else do rateLimitLoop previousMessage
In the full example we compose this pipe with others and then run it on a stream of messages.
Let’s look at a how we’d write a silly web application using the
scotty web framework: ALLCAPS AS A SERVICE. When a user requests a route like
"/allcaps/example" we respond with the text
routes :: ScottyM () routes = do get "/" rootAction get "/allcaps/:input" allcapsAction allcapsAction :: ActionM () allcapsAction = do input <- param "input" text (Text.toUpper input)
In the full example we start our web server locally and listen for requests on port 3000.
Writing tests is an important part of software development. In the following snippet we specify tests for a
capitalize function that we define in the full example.
capitalizeSpec :: Spec capitalizeSpec = do specify "Capitalizes \"monad\"" (do capitalize "monad" `shouldBe` "Monad") specify "Empty string" (do capitalize "" `shouldBe` "") specify "Allcaps string" (do capitalize "NOTATION!" `shouldBe` "Notation") specify "Is idempotent" (do property (\s -> capitalize (capitalize s) == capitalize s))
We have three test cases and one property that is checked on randomly generated inputs. If you run the tests you will detect a typo in the test specification.
The logic monad provides us with a declarative way to to create backtracking search algorithms. In this example we use the
expressions function from the following snippet to search for solutions to the four fours puzzle.
expressions :: Int -> Logic Expression expressions numberOfFours = case numberOfFours of 1 -> do return Four _ -> do numberOfFoursLeft <- choose [1 .. numberOfFours - 1] let numberOfFoursRight = numberOfFours - numberOfFoursLeft expressionLeft <- expressions numberOfFoursLeft expressionRight <- expressions numberOfFoursRight operator <- choose [Add, Subtract, Multiply, Divide] when (operator == Divide) ( guard (not (evaluate expressionRight == 0))) return (Binary operator expressionLeft expressionRight)
expressions function generates arithmetic expressions that contain exactly the given number of fours. When we have to generate an expression that contains exactly one four we return a constant
Four. When we have to generate an expression that contains more than one four we generate a binary operation (
Divide) between two expressions. We non-deterministically split the number of fours for the left-hand-side and the right-hand-side of the binary operator.
A monadic API is particularly nice for parsing. In this example we parse strings that represent chemical formulas like
"NaCl". The following snippet is a part of that parser. It parses a chemical element like
"Na", optionally followed by a decimal number. If there is no number we return a default number of
elementAndNumberParser :: Parser (Element, Int) elementAndNumberParser = do element <- elementParser number <- option 1 decimal return (element, number)
Monadic parsers can parse any file format, but you can also use them as a more verbose, typed and compositional alternative to regular expressions.
canvas :: Canvas () canvas = do fillStyle("cornflowerblue") strokeStyle("red") shadowColor("rgba(50, 50, 50, 1.0)") shadowOffsetX(2) shadowOffsetY(2) shadowBlur(4) lineWidth(20) lineCap("round") beginPath() moveTo(120.5, 130); quadraticCurveTo(150.8, 130, 160.6, 150.5) quadraticCurveTo(190, 250.0, 210.5, 160.5) quadraticCurveTo(240, 100.5, 290, 70.5) stroke()
Monads are everywhere. I hope this convinces you that they are not magical, abstract or hard to use.