Exercises part 3
-
Applicative
extendsFunctor
, because everyApplicative
is also aFunctor
. Proof this by implementingmap
in terms ofpure
and(<*>)
. -
Monad
extendsApplicative
, because everyMonad
is also anApplicative
. Proof this by implementing(<*>)
in terms of(>>=)
andpure
. -
Implement
(>>=)
in terms ofjoin
and other functions in theMonad
hierarchy. -
Implement
join
in terms of(>>=)
and other functions in theMonad
hierarchy. -
There is no lawful
Monad
implementation forValidated e
. Why? -
In this slightly extended exercise, we are going to simulate CRUD operations on a data store. We will use a mutable reference (imported from
Data.IORef
from the base library) holding a list ofUser
s paired with a unique ID of typeNat
as our user data base:DB : Type DB = IORef (List (Nat,User))
Most operations on a database come with a risk of failure: When we try to update or delete a user, the entry in question might no longer be there. When we add a new user, a user with the given email address might already exist. Here is a custom error type to deal with this:
data DBError : Type where UserExists : Email -> Nat -> DBError UserNotFound : Nat -> DBError SizeLimitExceeded : DBError
In general, our functions will therefore have a type similar to the following:
someDBProg : arg1 -> arg2 -> DB -> IO (Either DBError a)
We'd like to abstract over this, by introducing a new wrapper type:
record Prog a where constructor MkProg runProg : DB -> IO (Either DBError a)
We are now ready to write us some utility functions. Make sure to follow the following business rules when implementing the functions below:
-
Email addresses in the DB must be unique. (Consider implementing
Eq Email
to verify this). -
The size limit of 1000 entries must not be exceeded.
-
Operations trying to lookup a user by their ID must fail with
UserNotFound
in case no entry was found in the DB.
You'll need the following functions from
Data.IORef
when working with mutable references:newIORef
,readIORef
, andwriteIORef
. In addition, functionsData.List.lookup
andData.List.find
might be useful to implement some of the functions below.-
Implement interfaces
Functor
,Applicative
, andMonad
forProg
. -
Implement interface
HasIO
forProg
. -
Implement the following utility functions:
throw : DBError -> Prog a getUsers : Prog (List (Nat,User)) -- check the size limit! putUsers : List (Nat,User) -> Prog () -- implement this in terms of `getUsers` and `putUsers` modifyDB : (List (Nat,User) -> List (Nat,User)) -> Prog ()
-
Implement function
lookupUser
. This should fail with an appropriate error, if a user with the given ID cannot be found.lookupUser : (id : Nat) -> Prog User
-
Implement function
deleteUser
. This should fail with an appropriate error, if a user with the given ID cannot be found. Make use oflookupUser
in your implementation.deleteUser : (id : Nat) -> Prog ()
-
Implement function
addUser
. This should fail, if a user with the givenEmail
already exists, or if the data banks size limit of 1000 entries is exceeded. In addition, this should create and return a unique ID for the new user entry.addUser : (new : User) -> Prog Nat
-
Implement function
updateUser
. This should fail, if the user in question cannot be found or a user with the updated user'sEmail
already exists. The returned value should be the updated user.updateUser : (id : Nat) -> (mod : User -> User) -> Prog User
-
Data type
Prog
is actually too specific. We could just as well abstract over the error type and theDB
environment:record Prog' env err a where constructor MkProg' runProg' : env -> IO (Either err a)
Verify, that all interface implementations you wrote for
Prog
can be used verbatim to implement the same interfaces forProg' env err
. The same goes forthrow
with only a slight adjustment in the function's type.
-