design patterns (?) for control abstraction what do parsers, -calculus reducers, and prolog...
TRANSCRIPT
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 first-
class 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 combinatorslit x (x’:xs) | x==x’ = [(x,xs)]lit x _ = []
empty v xs = [(v,xs)]fail xs = []
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 psseqs ps = foldr (seq (:)) (empty []) pslits xs = seqs [ lit x | x<-xs ]
type Parser v = String -> [(v,String)]
“the” example, continued• A grammar/parser for arithmetic expressions:
expr = alts [number, seqs [lits “(”, expr, op, expr, lits “)”]]number = rep1 digitop = 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.hsnewtype Parser a = P {papply :: (String -> [(a,String)])}
instance Monad Parser where
-- return :: a -> Parser a
return v = P (\inp -> [(v,inp)])
-- >>= :: Parser a -> (a -> Parser b) -> Parser b
(P p) >>= f = 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.hsitem :: Parser Charitem = P (\inp -> case inp of [] -> [] (x:xs) -> [(x,xs)])
sat :: (Char -> Bool) -> Parser Charsat p = do { x <- item
; if p x then return x else mzero}
bracket :: Parser a -> Parser b -> Parser c -> Parser bbracket open p close =
do {open; x <- p; close; return x}…char, digit, letter, .., many, many1, sepby, sepby1,…
Item needs to inspect inp!
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 context-sensitive 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 application-
specific 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)
Monadclass 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 where mzero = Nothing Nothing `mplus` ys = ys xs `mplus` ys = xs
instance MonadPlus [ ] where mzero = [] mplus = (++)
State transformer monadnewtype 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 m s a -> (a -> ST m s b) -> ST m s b (ST p) >>= f = 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 monadnewtype 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 m s a -> (a -> ST m s b) -> ST m s b (ST p) >>= f = 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 monadnewtype 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 m s a -> (a -> ST m s b) -> ST m s b (ST p) >>= f = 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 CharlitP p = ST (\inp -> case dropWhile isSpace inp of { (x:xs) | p x -> [(x,xs)] ; otherwise -> [] })
lit :: Char -> Parser Charlit 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) )
thenX,Y: app(X,Y,[1,2])
proofX=[] 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 HaskellEmbedding takes little more than a page of
code (mostly, unification). The rest is our good old friends, state transformer monads:
: Sequential composition
true : return () : Alternative composition
false : mzero : = (function definition)
Predicates : substitution transformers
Unification : explicit code
v : fresh variables
Prolog, embedded in Haskellapp 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 x2y_1=1:::2:::[] x_0=[]y_1=2:::[] x_0=1:::[]y_1=[] x_0=1:::2:::[]
-- importsdata Term = Var String | Atom String | Nil | Term:::Termtype Subst = [(String,Term)] ..showSubst s = ..simplify s = ..
data State = State { subst :: Subst , free :: Integer }type Predicate = ST [] State
fresh :: String -> Predicate Termfresh 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