I have a continuation exercise from uni for my Haskell subject where I’ve been given the following:
data Expr = Con Value
| And Expr Expr
data Value = IntValue Int
| BoolValue Bool
est :: Expr -> Val
est (Con v) = v
est (And x y) =
case (est x, est y) of
(BoolValue bool1, BoolValue bool2) -> BoolValue $ bool1 && bool2
_ -> error "And: expected Boolean arguments"
And I’m not sure what it truly does. It seems to be an evaluator of the terms defined in Expr. Could someone please explain it to me? My exercise involves me converting that into a GADTs which I’ve done as:
data Expr e where
Con :: Val -> Expr Val
And :: Expr e -> Expr e -> Expr e
Now they’re asking me to implement the following statically and make it type safe:
est :: Expr e -> e
est _ = -- implement this
I think you’re on the wrong track with your GADT. To see why, we’ll first look at the untyped version of
Exprand its evaluator (the first part of your question).Here are a couple of values of type
Exprwhich you can construct:So far, so good:
expr1represents the integer constant 42,expr2andexpr3the boolean constantsTrueandFalse. All values of typeExprwithConas their outermost constructor essentially look like this.Things start getting interesting when we add the
Andconstructor to the mix:expr4andexpr5are fine; they represent the expressionsTrue && TrueandTrue && (False && False), respectively. We would expect them to evaluate toTrueandFalse, but more on that soon. However,expr6looks fishy: it represents the expressionTrue && 42which doesn’t make sense (in Haskell, at least!).The expressions we have seen so far, except number 6, all have a value:
expr1has the integer value 42, the rest are booleans (True,False,True,Falseforexprs 2 through 5). As you can see, the values are either integers or booleans and so can be represented as values of typeValue.We can write an evaluator which takes an
Exprand returns itsValue. In words, the evaluator should return the value of a constant expression and if the expression is a logical ‘and’, it should return the logical ‘and’ of the values of constituent expressions, which need to be boolean values – you can’t take the logicalandof an integer and a boolean, or two integers. In code, this translates toThis is just a more verbose version of the first
est, with the operation of taking the logical ‘and’ of two evaluated expressions spun off into a separate function and slightly more informative error messages.Okay, hopefully this answers your first question! The problem boils down to the fact that
Exprvalues can either have a boolean or an integer value, and you can’t “see” that type anymore, so it’s possible toAndtwo expressions together for which this doesn’t make sense.One way to solve this would be to split
Exprinto two new expression types, one having integer values and the other with boolean values. That would look something like (I’ll give the equivalents of theexprs used above as well):Two things are interesting to note: we’ve gotten rid of the
Valuetype, and now it’s become impossible to construct the equivalent ofexpr6– this is because its obvious translationAndBool (ConBool True) (ConInt 42)will be rejected by the compiler (it might be worth trying this out), because of a type error:ConInt 42is of typeIntExprand you can’t use that as the second argument toAndBool.The evaluator would also need two versions, one for integer expressions and one for boolean expressions; let’s write them!
IntExprshould haveIntvalues, andBoolExprshould evaluate toBools:As you can imagine, this is going to get tiresome fast as you add more types of expressions (like
Char, lists,Double) or ways to combine expressions such as adding two numbers, building ‘if’ expressions or even variables where the type is not given in advance…This is where GADTs come in! Instead of making a separate type of expression for each possible resulttype of the evaluation (
IntExprandBoolExprabove), we are going to “tag” the expression type itself with the type it will evaluate to. So we will be sure that the result of evaluating a value of typeExpr Intwill be anIntand that aExpr Boolwill give us aBool. In effect, we’ll let the compiler do the typechecking for us, instead of doing it at runtime (as in the functionlogicalAndValueabove).For the moment, we have just two ways of constructing expressions: making a constant expression, and ‘and’-ing two boolean values together. The first way works like this: if we have an
Int, we turn it into aExpr Int, aBoolgets turned into aExpr Booland so on. So, the type signature for the “make constant expression” constructor will be:The second constructor takes two expressions which represent boolean values (so those two expressions are of type
Expr Bool) and returns another expression with a boolean value, i.e. the type of this constructor isPutting the pieces together, we get the following
Exprtype:Some example values:
Once again, the equivalent of
expr6doesn’t pass the typechecker: it would beAnd (Con True) (Con 42), butCon 42is aExpr Intand therefore can’t be used as an argument toAnd– it needs to be anExpr Bool.The evaluator becomes really easy now. Given an
Expr e(remember, this means that it’s an expression which has a value of typee) it returns ane. The value of a constant expression is the constant itself, and the value of the logical ‘and’ expression is the ‘and’ of the values of the operands, and we’re sure that those values areBools. This gives:The GADT you’ve given is the direct equivalent of the untyped
Expr, and it will still allow you to construct ‘bad’ values such asAnd (Con (BoolValue True)) (Con (IntValue 42)).By getting rid of the ‘Value’ type, we can be much more precise in stating what the type of an expression is; instead of saying “the type of an expression is an integer or a boolean, but I don’t know just yet” and running into problems when evaluating expressions, we make sure from the beginning that we know the type of the value of an expression and that we don’t combine them in ways that don’t make sense.
I hope you’ve made it so far – all those types, values, and expressions on various levels can get confusing! – and that you’ll be able to experiment a bit with extending the
Exprtype and the evaluator on your own.Simple things to try are making an expression that adds two integer values, using string or char constants, or making an ‘if-then-else’ expression which takes three arguments: the first of boolean type, and the second and third of the same type (but that type could be
Int,Bool,Charor whatever).