300 likes | 397 Views
Design patterns (?) for control abstraction. What do parsers, -calculus reducers, and Prolog interpreters have in common?. What’s it all about?.
E N D
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 ofcontrol 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 combinators lit 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 ps seqs ps = foldr (seq (:)) (empty []) ps lits 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 digit op = alts [lits “+”, lits “-”, lits “*”, lits “/”] digit = alts [lits (show n) | n <- [0..9]] • Useful observations: • only the literalsreally “do” anyparsing – 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)]) -- >>= :: 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.hs Item needs to inspect inp! item :: Parser Char item = P (\inp -> case inp of [] -> [] (x:xs) -> [(x,xs)]) sat :: (Char -> Bool) -> Parser Char sat p = do { x <- item ; if p x then return x else mzero } 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 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 ofMonad mdefines sequential composition (>>=) and its unit (return) • an instance ofMonadPlus mdefines 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 where mzero = Nothing Nothing `mplus` ys = ys xs `mplus` 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 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 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 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 monad newtype ST ms a = ST {unST :: s -> m (a,s)} instance Monad m => Monad (ST ms) 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 ms) 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: • : Sequential composition true : return () • : Alternative composition false : mzero • : = (function definition) Predicates : substitution transformers Unification : explicit code v : 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