Differences
This shows you the differences between two versions of the page.
Both sides previous revision Previous revision Next revision | Previous revision | ||
pp:2025:l11 [2025/05/16 20:07] cata_chiru |
pp:2025:l11 [2025/05/18 13:35] (current) cata_chiru [11.3. Nice to read] |
||
---|---|---|---|
Line 1: | Line 1: | ||
====== Lab 11. Classes, Applicatives and More Monads ====== | ====== Lab 11. Classes, Applicatives and More Monads ====== | ||
- | ===== 11.0.0 Introduction ===== | + | ===== 11.0 Introduction ===== |
A powerful tool for understanding classes in Haskell is the '':info <class>'' in the ghci command line: | A powerful tool for understanding classes in Haskell is the '':info <class>'' in the ghci command line: | ||
Line 7: | Line 7: | ||
<code haskell> | <code haskell> | ||
>:info Eq | >:info Eq | ||
- | type Eq :: * -> Constraint | + | type Eq :: * -> Constraint -- We'll return to this a bit later :-) |
class Eq a where -- Shows the blue-print of the class | class Eq a where -- Shows the blue-print of the class | ||
(==) :: a -> a -> Bool | (==) :: a -> a -> Bool | ||
Line 32: | Line 32: | ||
</code> | </code> | ||
- | We see that the only requirement for Monad to work is overriding '' $ ( >>= ) $ ''. | + | We see that the only requirement for Monad to work is overriding ''( >>= )''. |
- | However, the catch comes from the restriction <code haskell> class Applicative m => Monad m ... </code> | + | However, the catch comes from the restriction: <code haskell> class Applicative m => Monad m ... </code> |
By looking at the Applicative: | By looking at the Applicative: | ||
Line 62: | Line 62: | ||
We see that pure and (<*>) are implemented using return and ( >>= ), the actual needs to implement a Monad. | We see that pure and (<*>) are implemented using return and ( >>= ), the actual needs to implement a Monad. | ||
- | However, it might be curious at first how we implement the functions of a class deemed as mandatory for another, with calls from the latter. This is due to the ad-hoc polymorphism in Haskell and language's recursive nature (recall the linked definitions between (==) and (/=). | + | 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. | ||
- | ===== 11.0.1 Tasks ===== | + | ===== 11.1. Tasks ===== |
- | ==== Functor ===== | + | 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: |
- | The **Functor** typeclass represents the mathematical functor: a mapping between categories. \\ | + | 1. data MyList a = ... |
- | In Haskell, Functors are defined as follows: | + | 2. data MyTree a = ... |
- | <code haskell> | + | |
- | class Functor m where | + | 3. data MyMaybe a = ... |
- | fmap :: (a -> b) -> f a -> f b | + | |
- | </code> | + | 4. data MyPair a b = ... |
<note> | <note> | ||
- | Remember the type of ''map''? | + | 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. \\ |
- | <code haskell> | + | |
- | map :: (a -> b) -> [a] -> [b] | + | |
- | </code> | + | |
- | This is a specific implementation of ''fmap'', that exists in Haskell for historic reasons, a list also has a ''Functor'' instance defined, with ''fmap = map'' | + | |
- | </note> | + | |
- | We can declare a ''Functor'' instance for any abstract type ''f a'' which has the ability to 'map' over it's value, while preserving the structure of ''f''. | + | However, MyPair needs 2 types to form an object. We can think of MyPair as a hash set (key, value) and curry the types: |
- | Declaring ''Functor'' instance for ''f'', allows us to use functions relating to mapping for any type ''f a''. | + | <code haskell> instance Class (MyPair a) where ... </code> |
- | For ''fmap'' to have a predictible behaviour, any ''Functor'' instance we define needs to obey the **Functor Laws**: | + | Meaning "I already got type a, I expect you to give type b and apply your operations only on the object of type b.". |
- | <code haskell> | + | |
- | -- Functors must preserve identity morphisms | + | |
- | fmap id = id | + | |
- | -- Functors preserve composition of morphisms | + | </note> |
- | fmap (f . g) = fmap f . fmap g | + | |
- | </code> | + | |
- | ==== Monad ==== | ||
- | 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. \\ | + | Create objects for each class and test the validity of your class implementations with examples for show, fmap, $<*>$, $>>=$. |
- | \\ | + | |
- | In Haskell, Monads are defined as follows: | + | |
- | <code haskell> | + | |
- | class Monad m where | + | |
- | return :: a -> m a | + | |
- | (>>=) :: m a -> (a -> m b) -> m b | + | |
- | </code> | + | |
- | <note> | + | ===== 11.2. More Applicatives ===== |
- | You can think of Monads as **containers** that keep a variable inside a context. We will see several examples in the next sections. | + | |
- | </note> | + | |
- | For monads to behave correctly, a monad instance needs to obey the **Monad Laws**: | + | Applicates, as monads, are used preponderantly for type construction and validation. |
- | <code haskell> | + | |
- | -- left identity | + | |
- | return a >>= h = h a | + | |
- | -- right identity | + | 5. For the first task we will want to validate a database of users. |
- | m >>= return = m | + | |
- | -- associativity | + | Being given the following data types: |
- | (m >>= g) >>= h = m >>= (\x -> g x >>= h) | + | |
- | </code> | + | |
- | **NOTE:** any Monad instance has a corresponding Functor and Applicative (we will not discuss applicatives for this course, but Haskell requires it). | ||
- | If you defined a Monad instance for a class ''Foo'', you can define the Functor and Applicative as follows: | ||
- | <hidden> | ||
<code haskell> | <code haskell> | ||
- | instance Functor Foo where | ||
- | fmap f fx = fx >>= (\x -> return (f x)) | ||
- | | ||
- | instance Applicative Foo where | ||
- | pure = return | ||
- | ff <*> fx = ff >>= (\f -> fx >>= (\x -> return (f x))) | ||
- | </code> | ||
- | </hidden> | ||
- | ==== Do notation ==== | + | data User = User |
+ | { name :: String | ||
+ | , age :: Int | ||
+ | , balance :: Int | ||
+ | , password :: String | ||
+ | } deriving Show | ||
+ | |||
+ | data Validation e a = Valid a | Invalid e deriving Show | ||
- | You can probably notice a lot of monadic code uses binds and lambda functions. Take for example the following snippet that uses monads to unpack 2 ''Maybe'' values and add them. | ||
- | <code haskell> | ||
- | add ma mb = ma >>= (\a -> | ||
- | mb >>= (\b -> | ||
- | return (a + b))) | ||
</code> | </code> | ||
- | Haskell provides **do-notation**, which is a syntactic sugar for this kind of expressions (also called hanging lambdas). | + | 5.1. Create the instance of Applicative for Validation. |
+ | |||
+ | 5.2. Create functions to validate each attribute of a user. | ||
<code haskell> | <code haskell> | ||
- | add ma mb = do | + | validateAttribute :: a -> Validation [String] a |
- | a <- ma | + | |
- | b <- mb | + | |
- | return (a + b) | + | |
</code> | </code> | ||
- | ===== 10.1. Working with 'Maybe' ===== | + | For example, validateAge will only allow Users with positive ages under < 150 years, otherwise it will return an Error. |
- | ''Maybe'' is already defined as a Functor and Monad: | + | For validateName, we want to make sure name starts with a capital letter and only contains letters. |
- | <code haskell> | + | |
- | instance Functor Maybe where | + | |
- | fmap f x = | + | |
- | case x of | + | |
- | Just v -> Just (f v) | + | |
- | Nothing -> Nothing | + | |
- | instance Monad Maybe where | + | Password has to have at least 6 characters, a majuscule and a special character. |
- | return = Just | + | |
- | mx >>= f = | + | Balance can also be negative. |
- | case mx of | + | |
- | Just v -> f v | + | |
- | Nothing -> Nothing | + | |
- | </code> | + | |
- | This section is meant to accommodate you to using Functors and Monads by playing around with an already implemented and familiar Monad: Maybe. | + | 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. |
- | **10.1.1.** Implement a function ''add5'' that adds 5 to a ''Maybe Int'', use ''fmap'' instead of ''case'' or pattern matching. | ||
<code haskell> | <code haskell> | ||
- | add5 :: Maybe Int -> Maybe Int | + | validateUser :: String -> Int -> Int -> String -> Validation [String] User |
</code> | </code> | ||
- | **10.1.2.** Implement functions ''add'', ''sub'' and ''mult'' that add, substract and multiply ''Maybe Int'', use ''do'' notation instead of ''case'' or pattern matching. | + | 5.4 Create unpackUser that takes a tuple and returns a User: |
<code haskell> | <code haskell> | ||
- | add :: Maybe Int -> Maybe Int -> Maybe Int | + | unpackUser :: (String, Int, Int, String) -> User |
- | sub :: Maybe Int -> Maybe Int -> Maybe Int | + | |
- | mult :: Maybe Int -> Maybe Int -> Maybe Int | + | |
</code> | </code> | ||
- | ===== 10.2. Working with IO ===== | + | 5.5 Being given the following list of user data: |
- | We can finally learn about IO in Haskell. Because it's a pure functional language, where we have no side-effects, IO is not really possible, reading external values or printing values is inherently a side-effect. Because of this, IO is defined as a Monad, where the context is the external world. | + | <code haskell> |
+ | 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!")] | ||
- | Very simply put, something with type ''IO a'' is a value of type ''a'' coming from the external world. When we print something, the type is simply ''IO ()'', because there is no value coming from the external world, we just interact with it. | + | type BankAccount = [Validation [String] User] |
+ | </code> | ||
+ | |||
+ | Create a function that filters the invalid users and adds them to a database: | ||
- | Let's look at the type annotations of some usual IO functions: | ||
<code haskell> | <code haskell> | ||
- | print :: Show a => a -> IO () | + | addUsers :: BankAccount -> [User] -> BankAccount |
- | putStrLn :: String -> IO () | + | |
- | getLine :: IO String | + | |
</code> | </code> | ||
- | <note> | + | 5.6 Filter the users with a negative balance. |
- | As a side-point, now that we know IO we can also write complete haskell programs that are executable. \\ | + | |
- | The main function in Haskell has the following type annotation: | + | |
<code haskell> | <code haskell> | ||
- | main :: IO () | + | filter_debtors :: BankAccount -> BankAccount |
</code> | </code> | ||
- | This means a ''main'' function is just a function that interacts with the external world. \\ | ||
- | You can use the commandline utility ''runhaskell main.hs'' to quickly test your Haskell programs without compiling them. | + | 6. Recall what a parser is from the course lecture: |
- | </note> | + | |
- | **10.2.1.** Write a ''Hello World'' program and execute it. | + | <code haskell> |
+ | import Data.Char | ||
+ | import Control.Applicative | ||
- | **10.2.2.** Write a program that reads a value ''n'' and prints the ''n-th'' fibonacci number. | + | data Expr = Var String | Val Int | Plus Expr Expr deriving Show |
- | <note> You can use ''read'' to convert from strings to certain types (is the opposite of ''show'').</note> | + | data Parser a = Parser (String -> [(a,String)]) |
- | **10.2.3.** Write a program that reads a value ''n'', a vector of ''n'' numbers and prints them in sorted order. | + | parse (Parser p) s = p s |
- | <note> Don't forget ''IO'' is also a ''Functor'', we can ''fmap'' over yet 'unread' values. </note> | + | 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)] | ||
- | **10.2.4.** Write a program that reads a value ''n'', and prints the sequences given by the **Collatz conjecture**. If the current number ''x'' is even, the next number is ''x / 2'', if ''x'' is odd the next number is ''3 * x + 1'', the sequence stops when you hit the number 1. | + | instance Applicative Parser where |
+ | af <*> mp = | ||
+ | do | ||
+ | f <- af | ||
+ | v <- mp | ||
+ | return $ f v | ||
+ | pure = return | ||
- | ===== 10.3. Lists ===== | + | instance Functor Parser where |
+ | fmap f mp = | ||
+ | do | ||
+ | x <- mp | ||
+ | return $ f x | ||
- | Let's take a moment and think about lists, we already experimented with Functors over lists a lot. It's simply the ''map'' function we are extremely familiar with, but what would mean to **sequence** 2 lists using $(>>=)$? \\ | ||
- | **Hint:** We actually have already seen this behaviour before while using list comprehensions. | ||
- | <hidden> | + | charParser :: Char -> Parser Char |
- | Sequencing lists is achieved by taking the cartesian product of the 2 lists and flattening it. | + | charParser c = Parser $ \s -> |
- | <code haskell> | + | case s of |
- | [1, 2, 3] >>= (\x -> [4, 5, 6] >>= (\y -> return (x, y))) = [(1, 4), (1, 5), (1, 6), (2, 4), (2, 5), (2, 6), (3, 4), (3, 5), (3, 6)] | + | [] -> [] |
+ | (x:xs) -> if (x == c) then [(x,xs)] else [] | ||
- | -- isn't this familiar? | + | predicateParser :: (Char -> Bool) -> Parser Char |
- | [(x, y) | x <- [1, 2, 3], y <- [4, 5, 6]] | + | predicateParser p = Parser $ \s -> |
- | </code> | + | case s of |
- | </hidden> | + | [] -> [] |
+ | (x:xs) -> if p x then [(x,xs)] else [] | ||
- | What is the context a list puts a value in? **Non-determinism**, a list represent the fact that the object can have multiple possible values. | ||
- | We will take a simple example where the list monad might be very helpful. Conside a 8x8 chess board. A position would be: | ||
- | <code haskell> | ||
- | type Pos = (Int, Int) | ||
- | </code> | ||
- | **10.3.1.** Make a function that given a position, tells you where a Knight might be able to move ([[https://youtu.be/gjMsHsd7N1Y?si=3xUvCE2iGbevyELV&t=14| how a Knight moves]]). | ||
- | <code haskell> | ||
- | moveKnight :: Pos -> [Pos] | ||
- | moveKnight (x, y) = do | ||
- | ??? | ||
</code> | </code> | ||
- | **10.3.2.** Make a function that given a start position and a target position, tells you if a Knight can get to the target position in exactly 3 moves. | + | 6.1 Try to understand and reimplement the instances for Functor, Applicative and Monad for Parser yourself. |
- | <code haskell> | + | |
- | canReachIn3 :: Pos -> Pos -> Bool | + | 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. |
- | canReachIn3 = undefined | + | |
- | </code> | + | |
- | **10.3.3.(* * *)** Make a function that given a number ''k'', a start position and a target position, tell you if a Knight can get to the target position in exactly ''k'' moves. | ||
<code haskell> | <code haskell> | ||
- | canReachInK :: Int -> Pos -> Pos -> Bool | + | letter :: Parser Char |
- | canReachInK = undefined | + | letter = undefined |
- | </code> | + | |
- | ===== 10.4. Probability Distributions (M3 flashbacks) ===== | + | digit :: Parser Char |
- | + | digit = undefined | |
- | Let's also try to define our own Monads. Keep in mind that we should never have the goal of making something a monad, rather we should define types that model aspects of our problem, and if we notice that they represent types with context that can be abstracted using monads, use them for simplicity. | + | |
- | + | ||
- | One such type is a Probability Distribution, we can define it as a list of pairs of value and probability. | + | |
- | + | ||
- | <code haskell> | + | |
- | newtype Prob a = Prob [(a, Float)] deriving Show | + | |
</code> | </code> | ||
- | **10.4.1.** Define a ''Functor'' instance for ''Prob'', mapping over a probability should change the value, but not affect the probability. Check that the functor laws apply. | + | 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: |
- | <code haskell> | + | |
- | instance Functor Prob where | + | |
- | fmap = undefined | + | |
- | prob :: Prob Int | ||
- | prob = Prob [(1, 0.5), (2, 0.25), (3, 0.25)] | ||
- | |||
- | -- fmap (+3) prob = Prob [(4, 0.5), (5, 0.25), (6, 0.25)] | ||
- | </code> | ||
- | |||
- | **10.4.2.** Define a auxiliary function ''flatten'' that takes a probability of probabilities (''Prob (Prob a)'') and returns a probability (''Prob a''). | ||
<code haskell> | <code haskell> | ||
- | flatten :: Prob (Prob a) -> Prob a | + | letterDigit :: Parser (Char,Char) |
- | flatten = undefined | + | letterDigit = undefined |
- | nested_prob :: Prob (Prob Int) | + | positive_parse_ex = parse letterDigit "a1b2c3" -- by calling this in the command line we should get [(('a','1'),"b2c3")] |
- | nested_prob = Prob [(prob, 0.7), (prob, 0.3)] | + | negative_parse_ex = parse letterDigit "aa2a" -- by calling this in the command line we should get [] |
- | + | ||
- | -- flatten nested_prob = Prob [(1,0.35),(2,0.175),(3,0.175),(1,0.15),(2,7.5e-2),(3,7.5e-2)] | + | |
</code> | </code> | ||
- | **10.4.3.** Using ''flatten'', define a ''Monad'' instance for ''Prob'' (it should behave in a similar fashion with the List monad, but also keep the probability context). Check that the monad laws apply. | ||
- | <code haskell> | ||
- | instance Monad Prob where | ||
- | return x = undefined | ||
- | m >>= f = undefined | ||
- | -- Monad expects Applicative to be declared as well, take this as it is | + | ===== 11.3. Nice to read ===== |
- | instance Applicative Prob where | + | |
- | pure = return | + | |
- | pf <*> px = do | + | |
- | f <- pf | + | |
- | x <- px | + | |
- | return (f x) | + | |
- | </code> | + | |
- | Let's play around with our monad, let's define a coin: | + | 1. https://mmhaskell.com/monads/applicatives |
- | <code haskell> | + | |
- | data Coin = Heads | Tails deriving Show | + | |
- | coin :: Prob Coin | + | 2. https://learnyouahaskell.com/functors-applicative-functors-and-monoids |
- | coin = Prob [(Heads, 0.5), (Tails, 0.5)] | + | |
- | unfair_coin :: Prob Coin | + | 3. https://learnyouahaskell.com/a-fistful-of-monads |
- | unfair_coin = Prob [(Heads, 0.6), (Tails, 0.4)] | + | |
- | flip :: Prob [Coin] | + | 4. https://www.youtube.com/watch?v=FLAPIgvlVnE&t=1945s&ab_channel=JamesHobson |
- | flip = do | + | |
- | x <- coin | + | |
- | y <- coin | + | |
- | z <- coin | + | |
- | return [x, y, z] | + | |
- | </code> | + | |
- | **10.4.4.** Make a function that returns the probability distribution of a fair **n**-sided die. | + | 5. [Data.Char Library](https://hackage.haskell.org/package/base-4.21.0.0/docs/Data-Char.html) |
- | <code haskell> | + | |
- | die :: Int -> Prob Int | + | |
- | die n = undefined | + | |
- | + | ||
- | -- (,) <$> (die 20) <*> (die 6) -- the probability distribution of rolling a d20 followed by a d6 | + | |
- | </code> | + | |
- | **10.4.5.** Let's use this framework to solve a M3 problem: "Jo has took a test for a disease. The result of the test is either positive or negative and the test is 95% reliable: in 95% of cases of people who really have the disease, a positive result is returned, and in 95% of cases of people who do not have the disease, a negative result is obtained. 1% of people of Jo’s age and background have the disease. Jo took the test, and the result is positive. What is the probability that Jo has the disease?" |