How to cancel my template
I am using the syntactic library to create AST. To evaluate an AST for a value (Haskell), all my nodes must be an instance of a syntax class EvalEnv
:
class EvalEnv sym env where
compileSym :: proxy env -> sym sig -> DenotationM (Reader env) sig
Syntactic also provides a default implementation:
compileSymDefault :: (Eval sym, Signature sig)
=> proxy env -> sym sig -> DenotationM (Reader env) sig
but the constraint on is sig
not valid in cases EvalEnv
, making the following (say, overlapping) instance impossible:
instance EvalEnv sym env where
compileSym = compileSymDefault
All my custom AST nodes are GADTs, usually with multiple constructors, where the parameter a
always satisfies the constraint for compileSymDefault
:
data ADDITIVE a where
Add :: (Num a) => ADDITIVE (a :-> a :-> Full a)
Sub :: (Num a) => ADDITIVE (a :-> a :-> Full a)
As a result, I found that all my instances for EvalEnv
look like this:
instance EvalEnv ADDITIVE env where
compileSym p Add = compileSymDefault p Add
compileSym p Sub = compileSymDefault p Sub
This template instance is identical for all AST nodes, and each of the GADT constructors must be specified separately, since the signature of the GADT constructor implies restrictions compileSymDefault
.
Is there any way to avoid having to list every constructor for every node type I do?
source to share
If I understand the problem correctly, templating comes from having to use template matching with each constructor to bring the required context into scope. Apart from the constructor name, all case branches are identical.
The following code uses the removeBoilerplate
rank-2 function that can be used to bring the context into scope. Two example functions are first defined using template code and then converted to use a helper function removeBoilerplate
.
If you have a lot of GADTs, you will need one removeBoilerplate
for each one. Thus, this approach is beneficial if you need to remove the templated template more than once for each type.
I'm not familiar with the syntax to be 100% sure this will work, but it looks like it has a good chance. You will probably need to change the function type a little removeBoilerplate
.
{-# LANGUAGE GADTs , ExplicitForAll , ScopedTypeVariables ,
FlexibleContexts , RankNTypes #-}
class Class a where
-- Random function requiring the class
requiresClass1 :: Class a => a -> String
requiresClass1 _ = "One!"
-- Another one
requiresClass2 :: Class a => a -> String
requiresClass2 _ = "Two!"
-- Our GADT, in which each constructor puts Class in scope
data GADT a where
Cons1 :: Class (GADT a) => GADT a
Cons2 :: Class (GADT a) => GADT a
Cons3 :: Class (GADT a) => GADT a
-- Boring boilerplate
boilerplateExample1 :: GADT a -> String
boilerplateExample1 x@Cons1 = requiresClass1 x
boilerplateExample1 x@Cons2 = requiresClass1 x
boilerplateExample1 x@Cons3 = requiresClass1 x
-- More boilerplate
boilerplateExample2 :: GADT a -> String
boilerplateExample2 x@Cons1 = requiresClass2 x
boilerplateExample2 x@Cons2 = requiresClass2 x
boilerplateExample2 x@Cons3 = requiresClass2 x
-- Scrapping Boilerplate: let list the constructors only here, once for all
removeBoilerplate :: GADT a -> (forall b. Class b => b -> c) -> c
removeBoilerplate x@Cons1 f = f x
removeBoilerplate x@Cons2 f = f x
removeBoilerplate x@Cons3 f = f x
-- No more boilerplate!
niceBoilerplateExample1 :: GADT a -> String
niceBoilerplateExample1 x = removeBoilerplate x requiresClass1
niceBoilerplateExample2 :: GADT a -> String
niceBoilerplateExample2 x = removeBoilerplate x requiresClass2
source to share
You cannot abandon your template, but you can reduce it a little. Neither does it remove your template , nor can new GHC Generics code infer instances for GADT, like yours. It is possible to instantiate EvalEnv
with template haskell , but I will not discuss that.
We can reduce the number of templates we write very little. The idea we ran into with the problem is that forall a
there is an instance Signature a
for anyone ADDITIVE a
. Let's make a class of things for which this is true.
class Signature1 f where
signatureDict :: f a -> Dict (Signature a)
Dict
is the GADT that captures the constraint. To determine it is required {-# LANGUAGE ConstraintKinds #-}
. Alternatively, you can import it from Data.Constraint
within the constraints package .
data Dict c where
Dict :: c => Dict c
To use a constructor constraint, Dict
we must match a template to it. Then we can write compileSym
through signatureDict
and compileSymDefault
.
compileSymSignature1 :: (Eval sym, Signature1 sym) =>
proxy env -> sym sig -> DenotationM (Reader env) sig
compileSymSignature1 p s =
case signatureDict s of
Dict -> compileSymDefault p s
Now we can write down ADDITIVE
instances of it, capturing the idea that there is always an instance Signature a
for anyone ADDITIVE a
.
data ADDITIVE a where
Add :: (Num a) => ADDITIVE (a :-> a :-> Full a)
Sub :: (Num a) => ADDITIVE (a :-> a :-> Full a)
instance Eval ADDITIVE where
evalSym Add = (+)
evalSym Sub = (-)
instance Signature1 ADDITIVE where
signatureDict Add = Dict
signatureDict Sub = Dict
instance EvalEnv ADDITIVE env where
compileSym = compileSymSignature1
Writing an instance Signature1
doesn't have much of the benefit of writing an instance EvalEnv
. The only advantages we got is that we took an idea that might be useful elsewhere, and the instance is a Signature1
little easier to write down.
source to share