Lab 11. Classes, Applicatives and More Monads

A powerful tool for understanding classes in Haskell is the :info <class> in the ghci command line:

>:info Eq
type Eq :: * -> Constraint -- We'll return to this a bit later :-)
class Eq a where -- Shows the blue-print of the class
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
  {-# MINIMAL (==) | (/=) #-} -- The minimal requirements to be implemented by a data type
                              --          to be included in the class
  	-- Defined in ‘GHC.Classes’
instance (Eq a, Eq b) => Eq (Either a b) -- Instances of the class
  -- Defined in ‘Data.Either’
instance Eq a => Eq (Maybe a) -- Defined in ‘GHC.Maybe’
instance Eq Integer -- Defined in ‘GHC.Num.Integer’

If we look at Monad:

>:info Monad
type Monad :: (* -> *) -> Constraint
class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  {-# MINIMAL (>>=) #-}
...

We see that the only requirement for Monad to work is overriding ( »= ).

However, the catch comes from the restriction:

 class Applicative m => Monad m ... 

By looking at the Applicative:

>:info Applicative
type Applicative :: (* -> *) -> Constraint
class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
  GHC.Base.liftA2 :: (a -> b -> c) -> f a -> f b -> f c
  (*>) :: f a -> f b -> f b
  (<*) :: f a -> f b -> f a
  {-# MINIMAL pure, ((<*>) | liftA2) #-}

We see that besides $ ( >>= ) $, we need pure and $ ( <*> ) $.

If we recall the definitions from the previous laboratory:

instance Applicative Foo where
  pure = return
  ff <*> fx = ff >>= (\f -> fx >>= (\x -> return (f x)))  

We see that pure and (<*>) are implemented using return and ( »= ), the actual needs to implement a Monad.

However, it might initially be curious how we implement the functions of a class deemed mandatory for another with calls from the latter. This is due to the ad-hoc polymorphism in Haskell and the language's recursive nature (recall the linked definitions between $(==)$ and $(/=)$). And historically, Monad was added before Applicative in Haskell, so this assures backwards compatibility.

However, as you will see in the next exercises, you can first implement the instance for Applicative and use its functions to derive Monad.

For the following exercises: Define the data structure and instances for Eq, Show, Functor, Applicative (in 2 ways from scratch and based on Monad) and Monad (in 2 ways from scratch and based on Applicative) for the following:

1. data MyList a = …

2. data MyTree a = …

3. data MyMaybe a = …

4. data MyPair a b = …

As you can see from :info, the type for our classes is unary (type Class :: * → Constraint), meaning that they expect to add just another type as parameter, as in MyList that expects and a.

However, MyPair needs 2 types to form an object. We can think of MyPair as a hash set (key, value) and curry the types:

 instance Class (MyPair a) where ... 

Meaning “I already got type a, I expect you to give type b and apply your operations only on the object of type b.”.

Create objects for each class and test the validity of your class implementations with examples for show, fmap, $<*>$, $>>=$.

Applicates, as monads, are used preponderantly for type construction and validation.

5. For the first task we will want to validate a database of users.

Being given the following data types:

data User = User
  { name     :: String
  , age      :: Int
  , balance :: Int
  , password :: String
  } deriving Show
 
data Validation e a = Valid a | Invalid e deriving Show

5.1. Create the instance of Applicative for Validation.

5.2. Create functions to validate each attribute of a user.

validateAttribute :: a -> Validation [String] a

For example, validateAge will only allow Users with positive ages under < 150 years, otherwise it will return an Error.

For validateName, we want to make sure name starts with a capital letter and only contains letters.

Password has to have at least 6 characters, a majuscule and a special character.

Balance can also be negative.

5.3. Make use of the above functions, and the functions from Applicative, as well as infixed fmap (<$>) to create a validation function for the user.

validateUser :: String -> Int -> Int -> String -> Validation [String] User

5.4 Create unpackUser that takes a tuple and returns a User:

unpackUser :: (String, Int, Int, String) -> User

5.5 Being given the following list of user data:

user_data = [("Catalin", 25, -1000, "aBc123!"), ("Nicoleta", 19, 1000, "da"), ("Nicoleta", 19, 1000, "Daba12_"), ("Andrei", 30, 2000, "pAr.la2"), ("Iust1n", 45, 3000, "kajD413!"), ("Iustin", 45, 3000, "kajD413!"), ("Maria", 28, -1500, "parOla3!")]
 
type BankAccount = [Validation [String] User]

Create a function that filters the invalid users and adds them to a database:

addUsers :: BankAccount -> [User] -> BankAccount

5.6 Filter the users with a negative balance.

filter_debtors :: BankAccount -> BankAccount

6. Recall what a parser is from the course lecture:

import Data.Char
import Control.Applicative
 
data Expr = Var String | Val Int | Plus Expr Expr deriving Show
 
data Parser a = Parser (String -> [(a,String)])
 
parse (Parser p) s = p s
 
instance Monad Parser where
--    (>>=) :: Parser a -> (a -> Parser b) -> Parser b 
    mp >>= f = Parser $ \s ->
                case parse mp s of
                     [] -> []
                     [(v,r)] -> parse (f v) r
    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
 
 
charParser :: Char -> Parser Char
charParser c = Parser $ \s -> 
                case s of 
                    [] -> []
                    (x:xs) -> if (x == c) then [(x,xs)] else []
 
predicateParser :: (Char -> Bool) -> Parser Char
predicateParser p = Parser $ \s ->
                        case s of
                            [] -> []
                            (x:xs) -> if p x then [(x,xs)] else []

6.1 Try to understand and reimplement the instances for Functor, Applicative and Monad for Parser yourself.

6.2 Using the definitions above and functions from Data.Char [5], implement letter - that parses one letter of the input, and digit - that parses one digit (given as a char) of the input.

letter :: Parser Char
letter = undefined
 
digit :: Parser Char
digit = undefined

6.3 Use letter, digit and functions from the classes studied in this laboratory to define letterDigit that parses one letter and one digit one after the other:

letterDigit :: Parser (Char,Char)
letterDigit = undefined
 
positive_parse_ex = parse letterDigit "a1b2c3" -- by calling this in the command line we should get [(('a','1'),"b2c3")]
negative_parse_ex = parse letterDigit "aa2a" -- by calling this in the command line we should get []