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

2 answers

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




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)



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.



All Articles