Abstracting Code Patterns
a.k.a. Dont Repeat Yourself
Lists
Rendering the Values of a List
-- >>> incList [1, 2, 3]
-- ["1", "2", "3"]
showList :: [Int] -> [String]
showList [] = []
showList (n:ns) = show n : showList ns
Squaring the values of a list
-- >>> sqrList [1, 2, 3]
-- 1, 4, 9
sqrList :: [Int] -> [Int]
sqrList [] = []
sqrList (n:ns) = n^2 : sqrList ns
Common Pattern: map
over a list
Refactor iteration into mapList
Reuse map
to implement inc
and sqr
Trees
Incrementing and Squaring the Values of a Tree
-- >>> showTree (Node 2 (Node 1 Leaf Leaf) (Node 3 Leaf Leaf))
-- (Node "2" (Node "1" Leaf Leaf) (Node "3" Leaf Leaf))
showTree :: Tree Int -> Tree String
showTree Leaf = ???
showTree (Node v l r) = ???
-- >>> sqrTree (Node 2 (Node 1 Leaf Leaf) (Node 3 Leaf Leaf))
-- (Node 4 (Node 1 Leaf Leaf) (Node 9 Leaf Leaf))
sqrTree :: Tree Int -> Tree Int
sqrTree Leaf = ???
sqrTree (Node v l r) = ???
QUIZ: map
over a Tree
Refactor iteration into mapTree
! What should the type of mapTree
be?
mapTree :: ???
showTree t = mapTree (\n -> show n) t
sqrTree t = mapTree (\n -> n ^ 2) t
{- A -} (Int -> Int) -> Tree Int -> Tree Int
{- B -} (Int -> String) -> Tree Int -> Tree String
{- C -} (Int -> a) -> Tree Int -> Tree a
{- D -} (a -> a) -> Tree a -> Tree a
{- E -} (a -> b) -> Tree a -> Tree b
Lets write mapTree
QUIZ
Wait … there is a common pattern across two datatypes
type List a = [a]
mapList :: (a -> b) -> List a -> List b -- List
mapTree :: (a -> b) -> Tree a -> Tree b -- Tree
gmap :: (Mappable t) => (a -> b) -> t a -> t b
Lets make a class
for it!
What type should we give to fmap
?
{- A -} (b -> a) -> t b -> t a
{- B -} (a -> a) -> t a -> t a
{- C -} (a -> b) -> [a] -> [b]
{- D -} (a -> b) -> t a -> t b
{- E -} (a -> b) -> Tree a -> Tree b
Reuse Iteration Across Types
And now we can do
-- >>> fmap (\n -> n + 1) (Node 2 (Node 1 Leaf Leaf) (Node 3 Leaf Leaf))
-- (Node 4 (Node 1 Leaf Leaf) (Node 9 Leaf Leaf))
-- >>> fmap show [1,2,3]
-- ["1", "2", "3"]
Exercise: Write a Functor
instance for Result
data Result a
= Error String
| Ok a
instance Functor Result where
fmap f (Error msg) = ???
fmap f (Ok val) = ???
When you’re done you should see
-- >>> fmap (\n -> n ^ 2) (Error "oh no") Node 2 (Node 1 Leaf Leaf) (Node 3 Leaf Leaf))
-- (Node 4 (Node 1 Leaf Leaf) (Node 9 Leaf Leaf))
Next: A Class for Sequencing
Recall our old Expr
datatype
data Expr
= Number Int
| Plus Expr Expr
| Div Expr Expr
deriving (Show)
eval :: Expr -> Int
eval (Number n) = n
eval (Plus e1 e2) = eval e1 + eval e2
eval (Div e1 e2) = eval e1 `div` eval e2
-- >>> eval (Div (Number 6) (Number 2))
-- 3
But, what is the result
A crash! Lets look at an alternative approach to avoid dividing by zero.
The idea is to return a Result Int
(instead of a plain Int
)
- If a sub-expression had a divide by zero, return
Error "..."
- If all sub-expressions were safe, then return the actual
Result v
eval :: Expr -> Result Int
eval (Number n) = Value n
eval (Plus e1 e2) = case e1 of
Error err1 -> Error err1
Value v1 -> case e2 of
Error err2 -> Error err2
Value v1 -> Result (v1 + v2)
eval (Div e1 e2) = case e1 of
Error err1 -> Error err1
Value v1 -> case e2 of
Error err2 -> Error err2
Value v1 -> if v2 == 0
then Error ("yikes dbz:" ++ show e2)
else Value (v1 `div` v2)
The good news, no nasty exceptions, just a plain Error
result
λ> eval (Div (Number 6) (Number 2))
Value 3
λ> eval (Div (Number 6) (Number 0))
Error "yikes dbz:Number 0"
λ> eval (Div (Number 6) (Plus (Number 2) (Number (-2))))
Error "yikes dbz:Plus (Number 2) (Number (-2))"
The bad news: the code is super duper gross
Lets spot a Pattern
The code is gross because we have these cascading blocks
case e1 of
Error err1 -> Error err1
Value v1 -> case e2 of
Error err2 -> Error err2
Value v1 -> Result (v1 + v2)
but really both blocks have something common pattern
- Evaluate
e
- If the result is an
Error
then return that error. - If the result is a
Value v
then do some further processing onv
.
Lets bottle that common structure in two functions:
>>=
(pronounced bind)return
(pronounced return)
(>>=) :: Result a -> (a -> Result b) -> Result b
(Error err) >>= _ = Error err
(Value v) >>= process = process v
return :: a -> Result a
return v = Value v
NOTE: return
is not a keyword; it is just the name of a function!
A Cleaned up Evaluator
The magic bottle lets us clean up our eval
eval :: Expr -> Result Int
eval (Number n) = return n
eval (Plus e1 e2) = eval e1 >>= \v1 ->
eval e2 >>= \v2 ->
return (v1 + v2)
eval (Div e1 e2) = eval e1 >>= \v1 ->
eval e2 >>= \v2 ->
if v2 == 0
then Error ("yikes dbz:" ++ show e2)
else return (v1 `div` v2)
The gross pattern matching is all hidden inside >>=
Notice the >>=
takes two inputs of type:
Result Int
(e.g.eval e1
oreval e2
)Int -> Result Int
(e.g. The processing function that takes thev
and does stuff with it)
In the above, the processing functions are written using \v1 -> ...
and \v2 -> ...
NOTE: It is crucial that you understand what the code above is doing, and why it is actually just a “shorter” version of the (gross) nested-case-of eval
.
A Class for >>=
Like fmap
or show
or jval
or ==
, the >>=
operator is useful across many types, so we capture it in an interface/typeclass:
Notice how the definitions for Result
fit the above, with m = Result
instance Monad Result where
(>>=) :: Either a -> (a -> Either b) -> Either b
(Error err) >>= _ = Error err
(Value v) >>= process = process v
return :: a -> Result a
return v = Value v
Syntax for >>=
In fact >>=
is so useful there is special syntax for it.
Instead of writing
you can write
Thus, we can further simplify our eval
to:
eval :: Expr -> Result Int
eval (Number n) = return n
eval (Plus e1 e2) = do v1 <- eval e1
v2 <- eval e2
return (v1 + v2)
eval (Div e1 e2) = do v1 <- eval e1
v2 <- eval e2
if v2 == 0
then Error ("yikes dbz:" ++ show e2)
else return (v1 `div` v2)
Writing Applications
In most language related classes, we start with a “Hello world!” program.
With 130, we will end with it.
Purity and the Immutability Principle
Haskell is a pure language. Not a value judgment, but a precise technical statement:
The “Immutability Principle”:
A function must always return the same output for a given input
A function’s behavior should never change
No Side Effects
Haskell’s most radical idea: expression ==> value
- When you evaluate an expression you get a value and nothing else happens
Specifically, evaluation must not have an side effects
change a global variable or
print to screen or
read a file or
send an email or
launch a missile.
Purity
Means functions may depend only on their inputs
- i.e. functions should give the same output for the same input every time.
But… how to write “Hello, world!”
But, we want to …
- print to screen
- read a file
- send an email
A language that only lets you write factorial
and fibonacci
is … not very useful!
Thankfully, you can do all the above via a very clever idea: Recipe
Recipes
This analogy is due to Joachim Brietner
Haskell has a special type called IO
– which you can think of as Recipe
A value of type Recipe a
is
a description of an effectful computations
when when executed (possibly) perform some effectful I/O operations to
produce a value of type
a
.
Recipes have No Effects
A value of type Recipe a
is
Just a description of an effectful computation
An inert, perfectly safe thing with no effects.
(L) chocolate cake, (R) a sequence of instructions on how to make a cake.
They are different (hint: only one of them is delicious.)
Merely having a Recipe Cake
has no effects: holding the recipe
Does not make your oven hot
Does not make your your floor dirty
Executing Recipes
There is only one way to execute a Recipe a
Haskell looks for a special value
The value associated with main
is handed to the runtime system and executed
The Haskell runtime is a master chef who is the only one allowed to cook!
How to write an App in Haskell
Make a Recipe ()
that is handed off to the master chef main
.
main
can be arbitrarily complicatedwill be composed of many smaller recipes
Hello World
The function putStrLn
- takes as input a
String
- returns as output a
Recipe ()
putStrLn msg
is a Recipe ()
when executed prints out msg
on the screen.
… and we can compile and run it
QUIZ: Combining Recipes
Next, lets write a program that prints multiple things:
main :: IO ()
main = combine (putStrLn "Hello,") (putStrLn "World!")
-- putStrLn :: String -> Recipe ()
-- combine :: ???
What must the type of combine
be?
{- A -} combine :: () -> () -> ()
{- B -} combine :: Recipe () -> Recipe () -> Recipe ()
{- C -} combine :: Recipe a -> Recipe a -> Recipe a
{- D -} combine :: Recipe a -> Recipe b -> Recipe b
{- E -} combine :: Recipe a -> Recipe b -> Recipe a
Using Intermediate Results
Next, lets write a program that
- Asks for the user’s
name
using
- Prints out a greeting with that
name
using
Problem: How to pass the output of first recipe into the second recipe?
QUIZ: Using Yolks to Make Batter
Suppose you have two recipes
and we want to get
What must the type of combineWithResult
be?
{- A -} Yolk -> Batter -> Batter
{- B -} Recipe Yolk -> (Yolk -> Recipe Batter) -> Recipe Batter
{- C -} Recipe a -> (a -> Recipe a ) -> Recipe a
{- D -} Recipe a -> (a -> Recipe b ) -> Recipe b
{- E -} Recipe Yolk -> (Yolk -> Recipe Batter) -> Recipe ()
Looks Familar
Wait a bit, the signature looks familiar!
Remember this
Recipe
is an instance of Monad
In fact, in the standard library
So we can put this together with putStrLn
to get:
or, using do
notation the above becomes
Exercise
- Compile and run to make sure its ok!
- Modify the above to repeatedly ask for names.
- Extend the above to print a “prompt” that tells you how many iterations have occurred.
Monads are Amazing
Monads have had a revolutionary influence in PL, well beyond Haskell, some recent examples
Big data pipelines e.g. LinQ and TensorFlow
A Silly App to End CSE 130
Lets write an app called moo inspired by cowsay
A Command Line App
moo
works with pipes