Transcript Document
Design patterns (?) for
control abstraction
What do parsers, -calculus reducers,
and Prolog interpreters have in
common?
What’s it all about?
• If you’ve been anywhere near functional
programmers during the last decade, you’ll have
heard a lot about parser combinators, monads,
monadic parser combinators, domain-specific
embedded languages (DSEL), ..
• There are a lot more details to these, but the
common theme are libraries of control
abstractions, built up from higher-order functions
• We’ll look at a few examples and useful ideas that
are not quite as well-known as they could be
Control structures
• Language designers and program verifiers are
used to thinking in terms of program calculi,
focussing on essential structure, e.g., basic
operations and their units:
– Sequential composition of actions/no action
– Alternative composition of choices/no choice
– [Parallel composition of processes/no process][not for today]
• Concrete languages come with their own complex,
built-in control structures (historical design)
– can be mapped to and understood as combinations of
basic operations, but have grown into fixed forms
which may not be a good match for the problem at hand
User-defined control structures
• Languages in which control structures are firstclass objects (higher order functions/procedures)
make it easy to “roll your own” control structures
– OOP: modelling of real-world objects
– FP: modelling of real-world control structures?
• Design freedom needs guidance – try to identify:
– domain-specific control structures
– general-purpose control structures (sequence,
alternative, parallels, recursion, ..)
• Reversed mapping (purpose-built designs)
– build libraries of complex, domain-specific structures
from basic, general-purpose control structures
“the” example: parser combinators
• Old idea (e.g., Wadler 1985):
– assume an operation for each BNF construct (literals,
sequential/alternative composition,..)
– define what each construct does in terms of parsing
– translate your grammar into a program using these
constructs (almost literal translation)
you’ve got a parser for the grammar!
Philip Wadler, “How to Replace Failure by a List of Successes”,
FPLCA’85, Springer LNCS 201
“Old-style” parser combinators
lit x (x’:xs) | x==x’ = [(x,xs)]
lit x _
= []
empty v xs = [(v,xs)]
fail xs
= []
type Parser v = String -> [(v,String)]
alt p q xs
= (p xs)++(q xs)
seq f p q xs = [ (f v1 v2,xs2)
| (v1,xs1) <- p xs
, (v2,xs2) <- q xs1
]
rep p = alt (seq (:) p (rep p)) (empty [])
rep1 p = seq cons p (rep p)
alts ps = foldr alt fail ps
seqs ps = foldr (seq (:)) (empty []) ps
lits xs = seqs [ lit x | x<-xs ]
“the” example, continued
• A grammar/parser for arithmetic expressions:
expr
= alts [number,
seqs [lits “(”, expr, op, expr, lits “)”]]
number = rep1 digit
op
= alts [lits “+”, lits “-”, lits “*”, lits “/”]
digit = alts [lits (show n) | n <- [0..9]]
• Useful observations:
– only the literals really “do” any parsing – the
combinators form a coordination layer on top of that,
organising the application of literal parsers to the input
– the parsing is domain-specific, the coordination is not
• Modern variants tend to use monads for the
coordination layer (e.g., Hutton and Meijer 1996)
From Hugs’ ParseLib.hs
newtype Parser a
= P {papply :: (String -> [(a,String)])}
instance Monad Parser where
-- return
:: a -> Parser a
return v
= P (\inp -> [(v,inp)])
-- >>=
(P p) >>= f
:: Parser a -> (a -> Parser b) -> Parser b
= P (\inp -> concat [ papply (f v) out
| (v,out) <- p inp])
instance MonadPlus Parser where
-- mzero
:: Parser a
mzero
= P (\inp -> [])
-- mplus
:: Parser a -> Parser a -> Parser a
(P p) `mplus` (P q) = P (\inp -> (p inp ++ q inp))
From Hugs’ ParseLib.hs
item
item
:: Parser Char
= P (\inp -> case inp of
[]
-> []
(x:xs) -> [(x,xs)])
sat
sat p
:: (Char -> Bool) -> Parser Char
= do { x <- item
; if p x then return x else mzero
}
Item needs to
inspect inp!
bracket :: Parser a -> Parser b -> Parser c -> Parser b
bracket open p close =
do {open; x <- p; close; return x}
…
char, digit, letter, .., many, many1, sepby, sepby1,
…
Grammar combinators?
• We can write the coordination layer to be
independent of the particular task (parsing)
– Then we can plug in different basic actions instead of
literal parsers, to get different grammar-like programs
• Instead of just parser combinators, we get a
general form of control combinators, applicable to
all tasks with grammar-like specifications
– obvious examples: generating language strings,
unparsing (from AST to text), pretty-printing, …
– Less obvious: syntax-directed editing, typing (?),
reduction strategies (think contexts and contextsensitive rules), automated reasoning strategies,…
That monad thing..(I)
• Parsers transform Strings to produce ASTs,
unparsers transform ASTs to produce Strings,
editors and reducers transform ASTs, ..
– Generalise to state transformers
• Combinators for sequence, alternative, etc. are so
common that we will use them often
– Make them so general that one set of definitions works
for all applications? one size fits all?
– Overload one set of combinators with applicationspecific definitions? do the variants still have
anything in common?
– A mixture of both: capture the commonalities, enable
specialisation (a framework). Monad, MonadPlus
That monad thing..(II)
• Type constructors:
– data [a] = [] | (a:[a])
– data Maybe a = Nothing | Just a
– data ST m s a = s -> m (a,s)
• Type constructor classes: for type constructor m,
– an instance of Monad m defines sequential
composition (>>=) and its unit (return)
– an instance of MonadPlus m defines alternative
composition (mplus) and its unit (mzero)
(over things of type m a)
Monad
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>)
:: m a -> m b -> m b
fail
:: String -> m a
-- Minimal complete definition: (>>=), return
p >> q = p >>= \ _ -> q
fail s = error s
-- some Instances of Monad
instance Monad Maybe where
Just x >>= k = k x
Nothing >>= k = Nothing
return
= Just
fail s
= Nothing
instance Monad [ ] where
(x:xs) >>= f = f x ++ (xs >>= f)
[]
>>= f = []
return x
= [x]
fail s
= []
MonadPlus
class Monad m => MonadPlus m where
mzero :: m a
mplus :: m a -> m a -> m a
-- some Instances of MonadPlus
instance MonadPlus Maybe
mzero
=
Nothing `mplus` ys =
xs
`mplus` ys =
where
Nothing
ys
xs
instance MonadPlus [ ] where
mzero = []
mplus = (++)
State transformer monad
newtype ST m s a = ST {unST :: s -> m (a,s)}
instance Monad m => Monad (ST m s) where
-- return
:: a -> ST m s a
return v
= ST (\inp -> return (v,inp))
-- >>=
(ST p) >>= f
:: ST m s a -> (a -> ST m s b) -> ST m s b
= ST (\inp -> do {(v,out) <- p inp
; unST (f v) out
})
instance MonadPlus m => MonadPlus (ST m s) where
-- mzero
:: ST m s a
mzero
= ST (\inp -> mzero)
-- mplus
:: ST m s a -> ST m s a -> ST m s a
(ST p) `mplus` (ST q) = ST (\inp -> (p inp `mplus` q inp))
State transformer monad
newtype ST m s a = ST {unST :: s -> m (a,s)}
instance Monad m => Monad (ST m s) where
-- return
:: a -> ST m s a
return v
= ST (\inp -> return (v,inp))
-- >>=
(ST p) >>= f
:: ST m s a -> (a -> ST m s b) -> ST m s b
= ST (\inp -> do {(v,out) <- p inp
; unST (f v) out
})
instance MonadPlus m => MonadPlus (ST m s) where
-- mzero
:: ST m s a
mzero
= ST (\inp -> mzero)
-- mplus
:: ST m s a -> ST m s a -> ST m s a
(ST p) `mplus` (ST q) = ST (\inp -> (p inp `mplus` q inp))
State transformer monad
newtype ST m s a = ST {unST :: s -> m (a,s)}
instance Monad m => Monad (ST m s) where
-- return
:: a -> ST m s a
return v
= ST (\inp -> return (v,inp))
-- >>=
(ST p) >>= f
:: ST m s a -> (a -> ST m s b) -> ST m s b
= ST (\inp -> do {(v,out) <- p inp
; unST (f v) out
})
instance MonadPlus m => MonadPlus (ST m s) where
-- mzero
:: ST m s a
mzero
= ST (\inp -> mzero)
-- mplus
:: ST m s a -> ST m s a -> ST m s a
(ST p) `mplus` (ST q) = ST (\inp -> (p inp `mplus` q inp))
Parsing, again
-- newtype ST m s a = ST {unST :: s -> m (a,s)}
type Parser a = ST [] String a
-- combinators for free
-- basic parsers still needed
litP
:: (Char -> Bool) -> Parser Char
litP p = ST (\inp -> case dropWhile isSpace inp of
{ (x:xs) | p x -> [(x,xs)]
;
otherwise -> []
})
lit
:: Char -> Parser Char
lit c = litP (==c)
-- as well as auxiliary combinations…
AS/Parser/Grammar for
data Exp = Var String | App Exp Exp | Lam String Exp
deriving Show
exp = var `mplus` app `mplus` abs
var = do { v <- litP isAlpha
; return $ Var [v] }
app = do { lit '(' ; e1 <- exp ; e2 <- exp ; lit ')'
; return $ App e1 e2 }
abs = do { lit '\\' ; Var v <- var ; lit '.' ; e <- exp
; return $ Lam v e }
What about semantics/reduction?
• -calculus reduction semantics:
– (v.M) N M[vN]
{context-free reduction;
meant to be valid in all contexts}
• refined by a reduction strategy (limited contexts):
– Cnor[ (v.M) N ] ,nor Cnor[ M[vN] ]
{context-sensitive, normal-order reduction}
– Cnor[] [] | (Cnor[] <expr>)
{reduction contexts;
expressions with a hole }
Translation to Haskell
• -reduction in Haskell:
beta (App (Lam v m) n) =
return $ substitute v n m
beta _ = fail "not a redex“
• Normal-order reduction strategy in Haskell:
norStep e@(App m n) = beta e `mplus`
(norStep m >>= (\m'-> return (App m' n)))
norStep _ = fail "not an application”
nor e = (norStep e >>= nor) `mplus` (return e)
And now,
for something completely ..
.. different?
Embedding Prolog in Haskell
Prolog, by example
programming in predicate logic:
– define predicates via facts and rules
– find solutions to queries about your "knowledge base":
app([],Y,Y).
app([X | XS],Y,[X | ZS]):-app(XS,Y,ZS).
?- app(X,Y,[1,2]).
X=[], Y=[1,2];
X=[1], Y=[2];
X=[1,2], Y=[];
no
Prolog, by example
Where's the logic?
assume
(Y:app([],Y,Y) true)
(X,XS,Y,ZS:
app([X|XS],Y,[X|ZS]) app(XS,Y,ZS) )
then
X,Y: app(X,Y,[1,2])
proof
X=[] Y=[1,2]
X=[1] Y=[2]
X=[1,2] Y=[]
Prolog, de-sugared
Closed-world assumption and de-sugaring:
A,B,C:
App(A,B,C)
(A=[] B=C)
X,XS,Y,ZS:
(A=[X|XS] C=[X|ZS] app(XS,Y,ZS))
Need equivalence rather than implication,
as well as explicit unification and
existential quantification, but now
we're ready to go
Prolog, embedded in Haskell
Embedding takes little more than a page of
code (mostly, unification). The rest is our
good old friends, state transformer monads:
true
false
Predicates
Unification
v
:
:
:
:
:
:
:
:
Sequential composition
return ()
Alternative composition
mzero
= (function definition)
substitution transformers
explicit code
fresh variables
Prolog, embedded in Haskell
app a b c = (do { a === Nil ; b === c })
+++
(exists "" $ \x->
exists "" $ \xs->
exists "" $ \zs->
do { a === (x:::xs) ; c === (x:::zs)
; app xs b zs })
x2 = exists "x" $ \x-> exists "y" $ \y->
app x y (Atom "1":::Atom "2":::Nil)
Prolog> solve x2
y_1=1:::2:::[] x_0=[]
y_1=2:::[] x_0=1:::[]
y_1=[] x_0=1:::2:::[]
-- imports
data Term = Var String | Atom String | Nil | Term:::Term
type Subst = [(String,Term)] ..showSubst s = ..simplify s = ..
data State = State { subst :: Subst , free :: Integer }
type Predicate = ST [] State
fresh :: String -> Predicate Term
fresh n = ST $ \s-> return (Var (n++"_"++show (free s))
,s{free=free s+1})
-- unification: substitution transformer
(===) :: Term -> Term -> Predicate ()
true,false :: Predicate ()
true = return ()
false = mzero
exists :: String -> (String -> Predicate a) -> Predicate a
exists n p = do { v <- fresh n ; p v }
solve x = mapM_ (putStrLn.showSubst)
[ subst $ simplify s | (_,s) <- unST x (State [] 0) ]
-- examples
app a b c = (do { a === Nil ; b === c })
+++
(exists "" $ \x->
exists "" $ \xs->
exists "" $ \ys->
do { a === (x:::xs) ; c === (x:::ys)
; app xs b ys })
x0 = exists "x" $ \x->
app (Atom "1":::x) Nil (Atom "1":::Atom "2":::Nil)
x1 = exists "z" $ \z->
app (Atom "1":::Atom "2":::Nil) Nil z
x2 = exists "x" $ \x-> exists "y" $ \y->
app x y (Atom "1":::Atom "2":::Nil)
Summary
• Parser combinators are only one of many
examples of a programming pattern with a
grammar-like coordination language
(Wadler'85 already suggested tacticals as another
example; there has been some recent work on
rewriting strategy combinators, e.g., for compiler
optimisations and other program transformations)
• Monad,MonadPlus,state transformers, do
notation facilitate reuse in this pattern
• Both transformer/containers and plain containers
(Maybe,[],Trees, ..) fit the pattern
• Coordination and computation can be defined
separately to enhance modularity