I'd like to be able to define trees using mutually inductive datatypes, and then write a generic function which can only run on certain parts of the tree, depending on what a node's descendants can be. So, for instance, if I had datatypes
data A = A1 B | A2 C D
newtype B = B String
newtype C = C String
newtype D = D Int
I want to be able to define a HasString
predicate and a function getString
like this:
getString :: (HasString x) => x -> String
such that HasString
is satisfied for A1
, B
, and C
, but not for D
.
The standard way to do this sort of thing is just with a manual traversal:
class GetString t where
getString :: t -> String
instance GetString B where
getString (B s) = s
-- .... instances for A, C
In writing this out, we've basically created a proof that every A
transitively contains a String
.
Now, I want to do this using generic machinery, so I can get it to run on datatypes other than A
. And I care about things other than getString
too.
Currently, I can construct a type-level predicate that indicates whether a datatype transitively contains a String
. I can even build it using generic proof machinery:
type family Or a b where
Or True a = True
Or a True = True
Or a b = False
type family And a b where
And True True = True
And a b = False
type family BaseHasString f where
BaseHasString B = True
BaseHasString C = True
BaseHasString x = False
type family Has f t where
Has f A = Or (f A) (And (Has f B) (Or (Has f C) (Has f D)))
Has f x = f x
type HasString t :: Constraint = (Has BaseHasString t ~ True)
(I should also be able to generalize this further using GHC Generics.)
However, now that I have my HasString
predicate, I can't figure out how to actually use it.
So, how can I write my getString
function?