Upload
andrewmohrland
View
66
Download
0
Embed Size (px)
Citation preview
Final Tagless: Encoding a small well-typed expression language in
PureScript without dependent types…or even sum types.
Andrew Mohrland @cxfreeio
our goal for this entire talk is to eventually be able to evaluate expressions like this:
boolean true `compare` ( (int 10 `add` int 20) `compare` int 30)
for example, we might have an interpreter E that evaluates the above expression to the Boolean true
but we also want E to evaluate something like:(int 10 `add` int 20)
to the Int value 30
The Problem
(Still) The ProblemAND we’d like to be able to seamlessly add ‘datatypes’
w/out having to change our interpreters:
`mul`(int 10 `add` int 20)
AND we’d like to be able to easily add new interpreters w/out changing our expressions or our datatypes:
prettyPrint ( `mul`(int 10 `add` int 20))
should yield a String that looks something like:
“(2 * (10 + 20))”
(Still) The Problem
AND we’d like our language to be type-safe.
the following should not compile, b/c it is an ill-typed expression:
`mul` (int 10 `compare` int 20)
whew. that’s a tall order.
but let’s give it a go.
first, let’s show that we don’t need sum types
(code example)
data Option a = None | Just a
optionMap f x = case x of None -> None Just x -> Just (f x)
data Optional a = Optional { fold :: forall ret. (a -> ret) -> ret -> ret }
optionalMap f (Optional o) = Optional { fold: \isSome isNone -> o.fold (f >>> isSome) isNone }
-- Some helpers. nothing = Optional { fold: \_ isNone -> isNone } just a = Optional { fold: \isSome _ -> isSome a }
so what we saw is that we can encode a datatype as a sum type
or we can encode it as a fold
so what does this have to do with anything?
well, ordinarily, we might think to encode our expression language with sum types:
data Expr = I Int | Add Expr Expr
but we can also think of it as a product of folds that fold to a representation repr of a result:
data Expr repr = Expr { i :: Int -> repr Int , add :: repr Int -> repr Int -> repr Int }
similar to what we did with Optional
let’s flesh out our language from before
(code example)
data Expr repr = Expr { int :: Int -> repr Int , boolean :: Boolean -> repr Boolean , add :: repr Int -> repr Int -> repr Int , compare :: forall a. (Eq a) => repr a -> repr a -> repr Boolean }
notice that repr is a type constructor!remember: we want our language to be well-typed, and this
gives us a way of expressing the return type
repr abstracts over different interpretations, like pretty-printing or evaluating to an Int
so we’re saying: “hey PureScript, no matter the interpretation, we want our expressions to be well-typed — and here are their return
types”
here’s what we came up with:
now let’s create an instance and use it
(code example)
data Eval ret = Eval ret
runEval (Eval ret) = ret
exprEval = Expr { int: \i -> Eval i , boolean: \b -> Eval b , add: \(Eval l) (Eval r) -> Eval (l + r) , compare: \(Eval l) (Eval r) -> Eval (l == r) }
so we came up with something like this:
and we were able to use it like this:
expr.int 30 `expr.compare` ( (expr.int 10 `expr.add` expr.int 20))
additionally, we showed that ill-typed expressions failed to compile
additionally, we can easily add new datatypes
we can extend our language with multiplication if we want:data Mult repr = Mult { mul :: repr Int -> repr Int -> repr Int }
multEval = Mult { mul: \(Eval l) (Eval r) -> Eval (l * r) }
and use it like so:expr.int 90 `expr.compare` ( expr.int 3 `mult.mul` ( expr.int 10 `expr.add` expr.int 20))
but there’s a problem.
this code is not extensible.
sure, we can now easily add datatypes like Mult
but we’ve baked in the interpretation! expr and mult are specific instances — exprEval and multEval!
all is not lost though.
what if we want to stringify our expressions? we need new instances!
we can use typeclasses, and let PureScript do the plumbing for us, for free!
so here is our final solution, using typeclassesclass Expr repr where int :: Int -> repr Int boolean :: Boolean -> repr Boolean add :: repr Int -> repr Int -> repr Int compare :: forall a. (Eq a) => repr a -> repr a -> repr Boolean
instance exprEval :: Expr Eval where int = Eval boolean = Eval add (Eval l) (Eval r) = Eval $ l + r compare (Eval l) (Eval r) = Eval $ l == r
instance exprStringify :: Expr Stringify where int = Stringify <<< show boolean = Stringify <<< show add (Stringify l) (Stringify r) = Stringify $ "(" ++ l ++ " + " ++ r ++ ")" compare (Stringify l) (Stringify r) = Stringify $ "(" ++ l ++ " == " ++ r ++ ")"
class Mult repr where mul :: repr Int -> repr Int -> repr Int
instance multEval :: Mult Eval where mul (Eval l) (Eval r) = Eval $ l * r
instance multStringify :: Mult Stringify where mul (Stringify l) (Stringify r) = Stringify $ "(" ++ l ++ " * " ++ r ++ ")"
(note that we need to annotate the typeclass constraints in PureScript, but GHC will infer it when -XNoMonomorphismRestriction
is set, alleviating even that small burden)
and we can use it like this:
let expr :: forall repr. (Mult repr, Expr repr) => repr Boolean expr = int 90 `compare` ( int 3 `mul` ( int 10 `add` int 20))
runEval expr === true runStringify expr === "(90 == (3 * (10 + 20)))"
now we can add both new folds and new datatypes at liberty! huzzah!
conclusion
for our expression language, we used an encoding of datatypes that didn’t involve sum types - we implemented
them as typeclass members instead
and we encoded evaluators as typeclass instances
in doing so, we were able to seamlessly add new datatypes and add new interpreters - all without having to re-compile old
code
typeclasses FTW!
additionally, our expressions are guaranteed to be well-typed… and we didn’t even have to use dependent types!
thanks. any questions?