-- HTML: Previous | Contents | Next
-- Title: Do notation and Idioms
-- Author: Edwin Brady
-- IGNORE
-- I want these green:
T_pure : ();
T_apply : ();
T_bind : ();
T_return : ();
-- and this blue:
data T = Tcon;
-- START
{--
This part of the tutorial describes some higher level syntactic sugar,
for writing programs involving sequencing ("do" notation) and
application (idioms) of data types.
--}
-- Section: Do notation
{-- Recall that when we write I\/O programs we use "do" notation to
sequence the side-effecting operations: --}
{->
greet : IO ();
greet = do { putStr "What is your name? ";
name <- getStr;
putStrLn ("Hello " ++ name);
};
>-}
{-- In fact "do" notation is merely syntactic sugar, which expands to
applications of the following "bind" function, which executes an
action then feeds the output to the next action: --}
{->
bind : IO a -> (a -> IO b) -> IO b;
>-}
{-- The above "greet" function could also be written with "bind"
explicitly: --}
greet : IO ();
greet = bind (putStr "What is your name? ")
(\x => bind getStr
(\name => putStrLn ("Hello " ++ name)));
{-- Clearly, the "do" notation is much easier to read, and it is much
clearer what is going on! Conveniently, therefore, "do" notation can
be rebound to work with types other than "IO" - any type "T" for which
there can be "bind" and "return" operations, with the following types:
--}
{->
T_bind : T a -> (a -> T b) -> T b;
T_return : a -> T a;
>-}
{-- For example, we can write a bind operation for "Maybe" as follows: --}
{->
maybeBind : Maybe a -> (a -> Maybe b) -> Maybe b;
maybeBind Nothing mf = Nothing;
maybeBind (Just x) mf = mf x;
>-}
{-- For the "return" operation, we can use "Just". We can use these
inside a "do" block by a "do using" declaration, which takes the
"bind" and "return" operations as parameters. For example, a function
which adds two "Maybe Int"s: --}
do using (maybeBind, Just) {
m_add : Maybe Int -> Maybe Int -> Maybe Int;
m_add x y = do { x' <- x; -- Extract value from x
y' <- y; -- Extract value from y
return (x' + y'); -- Add them
};
}
{-- This function will extract the values from "x" and "y", if they
are available, or return "Nothing" if they are not. Managing the
"Nothing" cases is achieved by "maybeBind", hidden by the "do"
notation. --}
{->
Idris> m_add (Just 20) (Just 22)
Just 42 : Maybe Int
Idris> m_add (Just 20) Nothing
Nothing : Maybe Int
>-}
{-- Haskell programmers will probably have wanted this section to
include the word 'monad', so there it was. Any others may feel free not
to worry :-). --}
-- Section: Idioms
{-- While "do" notation gives an alternative meaning to sequencing,
/idioms/ give an alternative meaning to application. The notation and
larger example in this section is inspired by Conor McBride and Ross
Paterson's paper \"Applicative Programming with Effects\".
First, let us revisit "m_add" above. All it's really doing is applying
an operator to two values extracted from "Maybe Int"s. We could
abstract out the application:
--}
m_app : Maybe (a->b) -> Maybe a -> Maybe b;
m_app (Just f) (Just a) = Just (f a);
m_app _ _ = Nothing;
{-- Using this, we can write an alternative "m_add" which uses this
alternative notion of function application, with explicit calls to "m_app": --}
{->
m_add' : Maybe Int -> Maybe Int -> Maybe Int;
m_add' x y = m_app (m_app (Just (+)) x) y;
>-}
{-- Rather than having to insert "m_app" everywhere there is an
application, we can use /idiom brackets/ to do the job for us. The
"idiom" keyword takes two parameters, the first explaining what to do
with pure values, the second being the application function. --}
idiom (Just, m_app) {
m_add' : Maybe Int -> Maybe Int -> Maybe Int;
m_add' x y = [| x + y |];
}
{-- Any type "T" can be used in this way, given "pure" and "apply"
functions with types of the following form: --}
{->
T_pure : a -> T a;
T_apply : T (a->b) -> T a -> T b;
>-}
-- Subsection: An error-handling interpreter
{-- Idiom notation is commonly useful when defining
evaluators. McBride and Paterson describe such an evaluator, for a
language similar to the following: --}
data Expr = Var String -- variables
| Val Int -- values
| Add Expr Expr; -- addition
{-- Expressions in this language are evaluated with respect to an
environment, mapping "String"s to "Int" values. We can use a list of
pairs for this, and write a "fetch" function to retrieve the
value. This may fail, so it returns a "Maybe Int": --}
fetch : String -> List (String & Int) -> Maybe Int;
fetch x (Cons (v,val) xs) with strEq x v {
| True = Just val;
| False = fetch x xs;
}
fetch x Nil = Nothing;
{-- It is convenient to define a type synonym for environments. Type
synonyms are just functions: --}
Env = List (String & Int);
{-- Now we'd like to evaluate things which might fail, which carry an environment,
without the noise usually associated with this. Enter the idiom brackets...
We'll take our idiom to be "Env -> Maybe a".
First, we need to know how to inject pure values into this idiom: --}
pure : a -> Env -> Maybe a;
pure x e = Just x;
{-- Then we need to know how to apply things which carry an environment and
might fail: --}
ap : (Env -> Maybe (a -> b)) -> (Env -> Maybe a) -> (Env -> Maybe b);
ap f g x with (f x, g x) {
| (Just fx, Just gx) = Just (fx gx);
| _ = Nothing;
}
{-- Finally, we write the evaluator which can use idiom brackets to
evaluate things clearly in this idiom: --}
idiom (pure, ap) {
eval : Expr -> Env -> Maybe Int;
eval (Var x) = fetch x;
eval (Val x) = [| x |];
eval (Add x y) = [| eval x + eval y |];
}
{-- We'll set up a simple environment to test the evaluator, and try a
couple of expressions. "testExpr1" will succeed, but "testExpr2" will
fail due to using an undefined variable: --}
testEnv = Cons ("x", 42) (Cons ("y", 6) Nil);
testExpr1 = Add (Var "x") (Var "y");
testExpr2 = Add (Var "x") (Var "z");
{->
Idris> eval testExpr1 testEnv
Just 48 : Maybe Int
Idris> eval testExpr2 testEnv
Nothing : Maybe Int
>-}
-- Subsection: And without the idiom brackets...?
{-- To properly appreciate the value of idiom brackets, it's worth
seeing what the definition would have looked like without them. The
most obvious way uses "with" to extract the values from the
intermediate evaluations: --}
{->
eval : Expr -> Env -> Maybe Int;
eval (Var x) g = fetch x g;
eval (Val x) g = Just x;
eval (Add x y) g with (eval x g, eval y g) {
| (Just x', Just y') = Just (x'+y');
| _ = Nothing;
}
>-}
{-- The situation improves slightly with "do" notation: --}
{->
do using (maybeBind, Just) {
eval : Expr -> Env -> Maybe Int;
eval (Var x) g = fetch x g;
eval (Val x) g = return x;
eval (Add x y) g = do { x' <- eval x g;
y' <- eval y g;
return (x'+y'); };
}
>-}
-- HTML:

Source for this chapter
-- HTML: Previous | Contents | Next