Design patterns (?) for control abstraction What do parsers, -calculus reducers, and Prolog...

Post on 25-Dec-2015

212 views 0 download

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