design patterns (?) for control abstraction what do parsers, -calculus reducers, and prolog...

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

Upload: thomasine-beasley

Post on 25-Dec-2015

212 views

Category:

Documents


0 download

TRANSCRIPT

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

Design patterns (?) for control abstraction

What do parsers, -calculus reducers, and Prolog interpreters have in

common?

Page 2: 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

Page 3: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 4: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 5: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

“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

Page 6: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 7: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 8: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 9: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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!

Page 10: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 11: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 12: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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)

Page 13: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 14: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 15: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 16: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 17: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 18: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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…

Page 19: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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 }

Page 20: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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 }

Page 21: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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)

Page 22: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

And now, for something completely ..

.. different?

Embedding Prolog in Haskell

Page 23: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 24: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 25: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 26: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 27: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 28: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 29: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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

Page 30: Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog interpreters have in common?

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