I mistakenly believed that it was impossible to do in Haskell (because having a “disjunction” of constraints is tricky) but after reading section 3.1 of Hiromi Ishii’s functional pearl “Witness Me — Constructive Arguments Must Be Guided with Concrete Witness” turns out it’s doable!
Here’s a gist with an example. The gist defines a helper datatype data RU r1 r2 = RU r1 r2
for the “union” of two records, along with a suitable HasField
instance.
First, we must define a type-level binary search tree:
data Map symbol q
= E
| N (Map symbol q) symbol q (Map symbol q)
deriving (Show, Eq)
Along with some helper type families. Here are their kind signatures:
type Insert :: Symbol -> q -> Map Symbol q -> Map Symbol q
-- entries in the first map win in collisions
type Combine :: Map Symbol q -> Map Symbol q -> Map Symbol q
type Find :: Symbol -> Map Symbol q -> q
-- makes all entries in a map equal to some "value" of kind q
type Overwrite :: q -> Map symbol z -> Map symbol q
-- gets the field map of a conventional record, using Generics
type RecordCode :: Type -> Map Symbol Type
Having done that, the next step is to define a version of HasField
which takes an extra parameter of kind Location
, which tells us where to find the field:
data Location =
InFirst
| InSecond
type UnionHasField :: Location -> Symbol -> Type -> Type -> Constraint
class UnionHasField location x r a | location x r -> a where
getFieldU :: r -> a
Here are the instances for RU
:
instance HasField k r1 a => UnionHasField InFirst k (RU r1 r2) a where
getFieldU (RU r1 _) = getField @k r1
instance HasField k r2 a => UnionHasField InSecond k (RU r1 r2) a where
getFieldU (RU _ r2) = getField @k r2
The final step is to define a HasField
instance for RU
which delegates to UnionHasField
. And, while delegating, we must also calculate the Location
of each field of the union:
instance
UnionHasField
(Find k (Overwrite InFirst (RecordCode r1) `Combine` (Overwrite InSecond (RecordCode r2))))
k
(RU r1 r2)
a =>
HasField k (RU r1 r2) a
where
getField r = getFieldU @(Find k (Overwrite InFirst (RecordCode r1) `Combine` (Overwrite InSecond (RecordCode r2)))) @k r
We obtain the map of fields of r1
, and we make all the “values” equal to InFirst
. The same for r2
but with InSecond
. Then we combine the maps and find the record in which we should find the field k
.
(It’s a bit annoying that we must write that complicated type family expression two times, one in the constraints and one in the type application. It might even be slower to compile. How to avoid that?)
Now we can do something like:
data Person = Person {name :: String, age :: Int}
deriving (Show, G.Generic)
data Address = Address {street :: String, number :: Int}
deriving (Show, G.Generic)
personAddress = RU (Person "John" 50) (Address "Rue del Percebe" 2)
main :: IO ()
main = do
print $ getField @"name" personAddress
print $ getField @"age" personAddress
print $ getField @"street" personAddress
print $ getField @"number" personAddress