Table of Contents

Lab 10. Monads

10.0. Understanding Monads

A monad is an algebraic structure used to describe computations as sequences of steps, and to handle side effects such as state and IO. They also provide a clean way to structure our programs.

In Haskell, Monads are defined as follows:

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

Recalling from this week's lecture, we know they (“»=”) is the equivalent to our “join” operation which performs the sequencing. We also know that m is of kind * ⇒ *, hence is a container.

Click to display ⇲

Click to hide ⇱

We also recall that not all containers are monads and that all monads are functors.


Monads are already implemented in Haskell, 'Maybe' being one of them:

instance Monad Maybe where
  mx >>= f = 
      case mx of 
        Just v -> f v
        Nothing -> Nothing
 
  return = Just

Do not forget about the syntactic sugar presented at lecture!

10.1. Working with 'Maybe'

This section is meant to accommodate you to using Monads by playing around with an already implemented and familiar Monad: Maybe. We will work with the Nat data type that you already should be familiar with. Add the following lines to your code:

data Nat = Zero | Succ Nat deriving Show
 
fromInt :: Int -> Maybe Nat 
fromInt x 
  | x < 0 = Nothing
  | otherwise = Just $ get x
      where get 0 = Zero
            get x = Succ (get (x-1))

Every exercise will require you to implement extra functions which process Nat numbers such as adding or subtracting. Use fromInt function to manually test your solutions.

10.1.1 Implement the following adding and subtracting functions. Using Maybe allows us to easily treat the case of negative numbers.

mminus :: Maybe Nat -> Maybe Nat -> Maybe Nat
mminus m n = undefined
 
mplus :: Maybe Nat -> Maybe Nat -> Maybe Nat
mplus m n = undefined

10.1.2 Implement multiplication (from scratch, do not use the already defined mplus).

mmulti :: Maybe Nat -> Maybe Nat -> Maybe Nat
mmulti m n = undefined

10.2. Implementing our 'Parser'

Firstly we need to understand the role of our parser. Given the type:

data Expr = Atom Int | Var String | Plus Expr Expr  deriving Show

our parser should be able to process the string: “1 + x + 2”
into and acceptable expression: Plus (Atom 1) $ Plus (Var “x”) (Atom 2)

Parsing a whole string at once is extremely inefficient and complex, hence we generally divide it into steps, such as:
parseAtom: “1 + x + 2” = (Atom 1, “+ x + 2”)

Also, we need to incorporate error-handling which we will represent using lists: the empty-list is an error, and the singleton list is a valid value containing it.

Finally, parsing is not just limited to expressions, hence we need to build a general implementation.

Now that we understand what we need to implement, this seems like a fitting job for Monads, as it incorporates sequencing, error-handling and modularity, which represent a perfect use for them.

Firstly, add the following helper and imports to your code:

import Data.Char
import Control.Applicative
 
-- helper to do the parsing for us
data Parser a = Parser (String -> [(a,String)])
parse (Parser p) s = p s

10.2.0 As as example, we can build a parser that always fails. Remember that we defined failures in parsing as empty lists.

failParser :: Parser a 
failParser = Parser $ \s -> []

10.2.1 Now implement a parser that takes a char and will parse only that char.

--If we need to parse 'A', we use this function to return us a parser that parses 'A'.
 
charParser :: Char -> Parser Char
charParser c = undefined

10.2.2 Implement a parser that takes a predicate of type (Char → Bool) and parses the characters which satisfy the predicate.

predicateParser :: (Char -> Bool) -> Parser Char
predicateParser p = undefined

Now that we know how to build basic parser, we should start sequencing them. We can rely on Monads for this purpose. Add the following lines to your code, they should be familiar from the lecture:

Click to display ⇲

Click to hide ⇱

instance Monad Parser where
  mp >>= f = Parser $ 
    \s -> case parse mp s of 
            [] -> []
            [(val,rest)] -> parse (f val) rest
 
  return x = Parser $ \s -> [(x,s)]
 
instance Applicative Parser where
  af <*> mp = 
    do 
      f <- af
      v <- mp
      return $ f v
  pure = return
 
instance Functor Parser where 
  fmap f mp = 
    do 
      x <- mp
      return $ f x
 
instance Alternative Parser where
  empty = failParser
  p1 <|> p2 = Parser $ \s -> case parse p1 s of
                                [] -> parse p2 s
                                x -> x



Here we have built our Parser Monad, made it an Applicative, Functor and Alternative. Alternative type-class allows us to switch between parsers in case one fails. This will be needed later in the exercises. Applicative type-class is necessary, but shall be excluded from our discussion.

10.2.3 Write a parser that parser a variable name, making use of the fact that Parser is a Monad. A variable is a String containing letters and numbers, but must start with a letter. Hint: Use predicateParser.

varP :: Parser String
varP = undefined

10.2.4 Implement starParser and plusParser. These 2 parsers work together to parse arbitrarily large strings.

{-
starParser will apply p zero or more times:
     - fails only after several applications
plusParser will apply p one or more times:
     - fails if it does not produce progress or after several applications
In short, plusParser needs to parse 'at least once', while starParser can go the plusParser route or just stop and fail after not parsing anything.
-}
 
plusParser :: (Parser a) -> Parser [a]
plusParser p = undefined
 
starParser :: (Parser a) -> Parser [a]
starParser p = undefined

From now on, the parser that we implemenent are a combination of parsers implemented up until that exercise

10.2.5 Now we can implement our variable parser using plusParser. Using this, implement a parser that returns the variable as Var type.

varParser :: Parser String 
varParser = undefined
 
varExprParser :: Parser Expr     
varExprParser = undefined

10.2.6 Implement a whitespace parser. Hint: use charParser.

whitespaceParser :: Parser String
whitespaceParser = undefined

10.2.7 You have all the ingredients to building an expression parser. As there are multiple logic ways to implement this, play around! If you're having trouble with it, you can use the logic presented at lecture or ask your TA, but we encourage you to come with one yourself.

exprParser :: Parser Expr
exprParser = undefined