18
Final Tagless: Encoding a small well-typed expression language in PureScript without dependent types…or even sum types. Andrew Mohrland @cxfreeio

Final Tagless for much win

Embed Size (px)

Citation preview

Page 1: Final Tagless for much win

Final Tagless: Encoding a small well-typed expression language in

PureScript without dependent types…or even sum types.

Andrew Mohrland @cxfreeio

Page 2: Final Tagless for much win

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

Page 3: Final Tagless for much win

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

Page 4: Final Tagless for much win

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

Page 5: Final Tagless for much win

whew. that’s a tall order.

but let’s give it a go.

Page 6: Final Tagless for much win

first, let’s show that we don’t need sum types

(code example)

Page 7: Final Tagless for much win

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

Page 8: Final Tagless for much win

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

Page 9: Final Tagless for much win

let’s flesh out our language from before

(code example)

Page 10: Final Tagless for much win

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:

Page 11: Final Tagless for much win

now let’s create an instance and use it

(code example)

Page 12: Final Tagless for much win

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

Page 13: Final Tagless for much win

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.

Page 14: Final Tagless for much win

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!

Page 15: Final Tagless for much win

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

Page 16: Final Tagless for much win

(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!

Page 17: Final Tagless for much win

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!

Page 18: Final Tagless for much win

thanks. any questions?