1 / 30

Design patterns (?) for control abstraction

Design patterns (?) for control abstraction. What do parsers, -calculus reducers, and Prolog interpreters have in common?. What’s it all about?.

kisha
Download Presentation

Design patterns (?) for control abstraction

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

  2. 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

  3. 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

  4. 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

  5. “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

  6. “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)]

  7. “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)

  8. 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))

  9. 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, …

  10. 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,…

  11. 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

  12. 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)

  13. 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 = []

  14. 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 = (++)

  15. 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))

  16. 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))

  17. 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))

  18. 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…

  19. 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 }

  20. What about semantics/reduction? • -calculus reduction semantics: • (v.M) N  M[vN] {context-free reduction; meant to be valid in all contexts} • refined by a reduction strategy (limited contexts): • Cnor[ (v.M) N ] ,nor Cnor[ M[vN] ] {context-sensitive, normal-order reduction} • Cnor[]  [] | (Cnor[] <expr>){reduction contexts; expressions with a hole }

  21. 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)

  22. And now, for something completely .. .. different? Embedding Prolog in Haskell

  23. 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

  24. 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=[]

  25. 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

  26. 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

  27. 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:::[]

  28. -- 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) ]

  29. -- 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)

  30. 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

More Related