What's good about separating interface & implementation?
Users don't have to worry about how things work, and we can hide implementation details
We can swap out the implementation for a different one (assuming users aren't allowed to sneak a peek into the implementation)
Users are “protected from hurting themselves/others” by abusing/misusing the internals of the implementation.
What are some downsides?
Less concrete
People who care about “what is going on inside” can't see it as easily.
Some things can't be done because the interface doesn't support it (even though the underlying implementation could)
The tools used to create the abstraction may also need to be understood.
Wiki Extra: Abstraction in our Parser Code
Think back to our parsing code. Given the provided base operations (from which everything else is built):
fail :: String -> Parser a
return :: a -> Parser a
get :: Parser Char
<|> :: Parser a -> Parser a -> Parser a
>>= :: Parser a -> (a -> Parser b) -> Parser b
and our Secret internal representation:
newtype Parser a = ParsingFunction (String -> ParseResult a)
data ParseResult a = Success a String
| Failure String
Can you write a parser that ``puts back'' new never-before-seen characters into the input?
No, not via the interface we have. We don't have access to the input string in the current Parser representation.
But, if we had access to the (private) implementation, we could.
put :: Char -> Parser ()
put c = ParsingFunction (\input -> Success () (c : input))
But writing it this way can only be done via the (private) implementation; and if we change the implementation we need to change this code.
Can you write ifErrorIs ?
Can you write a parser that makes a decision based on the error message of a previous parser?
No. We can do something based on the failure of a previous parser (ex. <|>) but not on the error message of a previous parser. This is because we don't actually have a way to access the error message itself.
But, if we had access to the (private) implementation, we could.
ifErrorIs :: Parser a -> (String -> Parser a) -> Parser a
ifErrorIs (ParsingFunction pFn) errHandler = ParsingFunction newParser
where newParser input =
case pFn input of
Success v rest -> Success v rest
Failure msg -> let ParsingFunction qFn = errHandler msg
in qFn input
But writing it this way can only be done via the (private) implementation; and if we change the implementation we need to change this code.
Monads etc. Revisited
Consider this code for the list monad, what does it do?
[1,2,3] >>= (\i -> ([7,11,23] >>= (\j -> if isEven i then [i+j] else [])))
It produces
[9,13,25]
We could also add filtering by writing:
[1,2,3] >>= (\i -> ([7,11,23] >>= (\j -> if isEven i then return (i+j) else empty)))
The actual type of <+> in our parser library was not
The Applicative Typeclass
Using the >>= (“and then”) operator is a bit ugly. We could solve this with another typeclass, in this case Applicative which provides a new operator, <*>. For lists, this operator is:
<*> :: [a -> b] -> [a] -> [b]
In general, the two operations for Applicative are
pure :: Applicative t => a -> t a
<*> :: Applicative t => t (a -> b) -> t a -> t b
pure basically just the same as return, for lists they are defined as:
pure :: a -> [a]
pure x = [x]
return :: a -> [a]
return x = [x]
(Why have two names for the same thing? Two different interfaces! return is a function in the Monad interface and pure is a function in the Applicative interface!)
Rather than use the >>= (“and then”) explicitly, wouldn't it be better to have some nice syntactic sugar for it?
Prelude> [i+j | i <- [1,2,3], j <- [7,11,23]]
[8,12,24,9,13,25,10,14,26]
Prelude> isEven x = x `mod` 2 == 0
Prelude> [i+j | i <- [1,2,3], j <- [7,11,23], isEven i]
[9,13,25]
This would still mean the code above, using >>= but be much more pleasant to write!
Generalized list comprehensions?
The “and then” operation is general, so we could write:
Prelude> Just 2 >>= (\i -> (Just 11 >>= (\j -> if isEven i then return (i+j) else empty)))
Just 13
Prelude> Nothing >>= (\i -> (Just 11 >>= (\j -> if isEven i then return (i+j) else empty)))
Nothing
and
PicParser> p = num >>= (\i -> (num >>= (\j -> if isEven i then return (i+j) else empty)))
PicParser> parse p "2 11"
13
PicParser> parse p "1 11"
*** Exception: <input>:1:5 -- Oh noes
So does it make sense to be able to say:
[i+j | i <- Just 2, j <- Just 11, isEven i]
or
myparser = [i+j | i <- num, j <- num, isEven i]
It would be consistent with the underlying form that our syntactic sugar is turned into, but the square bracket syntax would be confusing, since it makes people think of lists. So, no, let's not do this.
Aside: In revisions to the language over the years, Haskell initially didn't, then did, then didn't again.
do Notation
Writing
do var <- p
q
is syntactic sugar for
p >>= (\var -> q)
And
do AAA
BBB
CCC
is syntactic sugar for
do AAA
do BBB
CCC
In do notation, our code becomes:
do i <- [1,2,3]
j <- [7,11,23]
return (i+j)
and
do i <- Just 2
j <- Just 11
return (i+j)
and
myparser = do i <- num
j <- num
return (i+j)
Interactive Session Code from Class
Section 2
You'll recall writing code like this (i.e., using <+> in the homework...
*SimpleExpr> :t num -- num is the number parser
num :: Parser Integer
*SimpleExpr> p = num <+> num
*SimpleExpr> parse p "40 2"
(40,2)
But <+> is actually defined using the bind/and-then operator.
*SimpleExpr> p = num >>= (\n1 -> num)
*SimpleExpr> parse p "40 2"
2
*SimpleExpr> p = num >>= (\n1 -> (num >>= (\n2 -> return 0)))
*SimpleExpr> parse p "40 2"
0
*SimpleExpr> p = num >>= (\n1 -> (num >>= (\n2 -> return (n1,n2))))
*SimpleExpr> parse p "40 2"
(40,2)
*SimpleExpr> p = num >>= (\n1 -> (num >>= (\n2 -> return (n1+n2))))
*SimpleExpr> parse p "40 2"
42
Aside, we can also do:
*SimpleExpr> p = return (+) <*> num <*> num
*SimpleExpr> parse p "40 2"
42
*SimpleExpr> p = return (+1) <*> num
*SimpleExpr> parse p "41"
42
*SimpleExpr> ourstar l r = l >>= (\f -> r >>= \v -> return (f v))
*SimpleExpr> p = return (+1) `ourstar` num
*SimpleExpr> parse p "41"
42
ghci -XMonadComprehensions
GHCi, version 8.8.2: https://www.haskell.org/ghc/ :? for help
Prelude> [(n1+n2) | n1 <- Just 40, n2 <- Just 2, (n1+n2) `rem` 2 == 0]
Just 42
Prelude>
*SimpleExpr> do { n1 <- [1,2,3,4]; n2 <- [100,200,300]; if (n1+n2) `rem` 2 == 0 then return (n1+n2) else empty }
[102,202,302,104,204,304]
*SimpleExpr> p = do { n1 <- num; n2 <- num; if (n1+n2) `rem` 2 == 0 then return (n1,n2) else empty }
*SimpleExpr> parse p "40 2"
(40,2)
*SimpleExpr> parse p "40 1"
*** Exception: <input>:1:5 -- No parse (via pfail)
*SimpleExpr> p = do { n1 <- num; n2 <- num; return (n1,n2); }
*SimpleExpr> parse p "40 1"
(40,1)
*SimpleExpr> p = do { n1 <- num; _ <- text "whoa"; n2 <- num; return (n1,n2); }
*SimpleExpr> parse p "40 whoa 2"
(40,2)
*SimpleExpr> p = do { n1 <- num; text "whoa"; n2 <- num; return (n1,n2); }
*SimpleExpr> parse p "40 whoa 2"
(40,2)
Finding More Monads
The code in the Pic2PS assignment looked like this:
doElems :: [Element] -> State -> (PostScriptCode, State)
doElems [] state = (PostScript.noCode, state)
doElems (elem : elems) state = let (ps1, state') = doElem elem state
(ps2, state'') = doElems elems state'
in (ps1 ++ ps2, state'')
How could we refactor this code to use a (new, custom) Monad (in broad strokes). What would the monadic version of the above code look like?
Much like the our Parser and IO monads, we'd create a state monad (e.g., called PicST) to make it easy to carry along the pic state without needing to deal with it so explicitly.
import Control.Monad
newtype PicST a = STfn (State -> (a, State))
instance Functor PicST where
-- fmap :: (a -> b) -> PicST a -> PicST b
-- fmap f (STfn sfn) = STfn mappedFn
-- where mappedFn state = let (v, state') = sfn state
-- in (f v, state')
--
fmap = liftM -- a Haskell builtin that uses Monad operations
instance Applicative PicST where
pure = return -- a Haskell builtin that uses Monad operations
(<*>) = ap -- a Haskell builtin that uses Monad operations
instance Monad PicST where
return v = STfn (\state -> (v, state))
(>>=) (STfn f) mkSTfn = STfn combined
where combined state = let (v, state') = f state
STfn g = mkSTfn v
in g state'
If you want to print this page, you can click
to show all the answers.