These practice problems (unlike the exercises) are not assessed. Use them to test your understanding, to challenge yourself, or whatever floats your boat.
The purpose of the Either a monad is similar to the Maybe monad: it provides the ability to represent failing computations in a type-safe way.
…but in the Maybe monad, all failures are represented by Nothing, which seems less than optimally informative. With Either a, successful computations are represented by values of type a wrapped under the Left constructor.
Recall the employee database from Lecture 5:
type ID = Int
data Employee = Employee
{ idNumber :: ID
, name :: String
, supervisor :: Maybe ID
} deriving (Show,Eq)
type DB = [Employee]
blandingsDB :: DB
blandingsDB =
-- id name supervisor
[ Employee 1 "The Empress" $ Nothing,
Employee 2 "Lord Emsworth" $ Just 1,
Employee 3 "Beach the Butler" $ Just 3,
Employee 4 "Gloria Salt" $ Just 5
]
lookupID :: Int -> DB -> Maybe Employee
lookupID n [] = Nothing
lookupID n (e:es)
| idNumber e == n = Just e
| otherwise = lookupID n es
supervisorOf :: Int -> DB -> Maybe Employee
supervisorOf id db =
lookupID id db >>= \e ->
supervisor e >>= \id' ->
lookupID id' db >>= \e' -> return e'
{- alternatively in do notation:
supervisorOf :: Int -> DB -> Maybe Employee
supervisorOf id db =
do
e <- lookupID id db
id' <- supervisor e
e' <- lookupID id' db
return e'
-}Modify this to a function of type Int -> DB -> Either String Employee such that each possible reason for failure gives a different error message wrapped in Left.
You may find this combinator helpful for endowing Maybe computations with error messages:
failWith :: Maybe b -> a -> Either a b failWith res err = maybe (Left err) Right res
Generator a e r is an abstract representation of a computation as an infinitely branching tree. The computation that may perform observable events of type e, which will yield answers from the environment of type a; the tree-like structure comes about because what the computation does next may depend on a. Terminating computations yield a final return value r.
data Generator a e r = Final r | Next e (a -> Generator a e r)
For example, here's a Generator that represents a computation that receives two numbers, and prints their sum.
sumTwo :: Generator Int String Int sumTwo = Next "getInt" (\x -> Next "getInt" (\y -> Final(x + y)))
sumThree.sumToTwenty, which will keep doing getInt events until the sum of all thus received numbers is at least 20. (As you might expect, this requires recursion).Monad instance for Generator a e. Here's a template with everything except bind:instance Functor (Generator a e) where
fmap f (Final x) = Final $ f x
fmap f (Next y g) = Next y (\x -> fmap f (g x))
instance Applicative (Generator a e) where
pure = Final
mf <*> mg = do
f <- mf
g <- mg
return $ f g
instance Monad (Generator a e) where
return = pure
f >>= g = undefined -- TODO yield :: e -> Generator a e a yield x = Next x returnFor example, we can now express
sumTwo as follows:sumTwo' :: Generator Int String Int
sumTwo' =
do
x <- yield "getInt"
y <- yield "getInt"
return(x + y) or equivalently:sumTwo'' :: Generator Int String Int sumTwo'' = yield "getInt" >>= \x -> yield "getInt" >>= \y -> return(x + y)(If you are not convinced that these are the same, equational reasoning may help). Similarly rewrite
sumToTwenty, but without mentioning Next and Final.Generator a e is the most general of all monads, in the sense that given an abstract computation in Generator a e, we can define an interpreter function that translates this into a concrete computation in your favourite monad. This is parameterised on a mapping from events to monad actions.interpret :: Monad m => (e -> m a) -> Generator a e r -> m rGiven a correct implementation, the following should be an IO computation that reads two numbers from the command line, and returns their sum:
interpret (const $ read <$> getLine) sumTwo
x such thatinterpret x == id
-- Read a file name from stdIn, and print the first 10 characters -- of the file readAndPrint10 :: IO String readAndPrint10 = do fileName <- getLine file <- readFile fileName return $ take 10 fileUnlike previous examples, this computation requires two distinct monad actions, so the interpreter can no longer be a constant function. Moreover, one of the monad actions takes an argument and the other doesn't. Accounting for that will require a more sophisticated event type than
String. Here's my suggestion:data Event = GetLine | ReadFile String deriving (Eq,Show)
sumTwo in any monad, not just the IO monad. For example, try this:interpret (const $ [1..5]) sumTwoThus,
Generator can be used to write an abstract monadic computation with abstract monad actions, without committing up-front to which monad the computation should be run in.This means that IO computations written as generators can be tested without actually performing any IO. To do this, we would interpret them in a different monad. Can you use this idea to write a quickCheck property which tests whether sumTwo is correct, in the sense that it returns the sum of its inputs?This exercise has been about a simplified presentation of free monads, which will receive no further coverage in this course.
Resource created Monday 15 July 2024, 06:27:10 PM.