I'd like to have a type class that tells me how big various types are.
data Cardinality = Finite Natural | Infinite
class Sized a where cardinality :: Cardinality
It's pretty straightforward to write instances; for example:
instance Sized Void where cardinality = Finite 0
instance Sized () where cardinality = Finite 1
instance Sized Bool where cardinality = Finite 2
instance Sized a => Sized [a] where
cardinality = case cardinality @a of
Finite 0 -> Finite 1
_ -> Infinite
data X = X Y
data Y = Y X X
instance Sized X where cardinality = Finite 1
instance Sized Y where cardinality = Finite 1
In fact, it's so straightforward, it feels like it ought to be automatable. Perhaps generic programming will help?
class GSized f where gcardinality :: Cardinality
class Sized a where
cardinality :: Cardinality
default cardinality :: (Generic a, GSized (Rep a)) => Cardinality
cardinality = gcardinality @(Rep a)
Most of the instances are pretty straightforward:
instance GSized V1 where gcardinality = Finite 0
instance GSized U1 where gcardinality = Finite 1
instance GSized f => GSized (M1 i c f) where gcardinality = gcardinality @f
instance (GSized f, GSized g) => GSized (f :+: g) where
gcardinality = case (gcardinality @f, gcardinality @g) of
(Finite n, Finite n') -> Finite (n+n')
_ -> Infinite
instance (GSized f, GSized g) => GSized (f :*: g) where
gcardinality = case (gcardinality @f, gcardinality @g) of
(Finite 0, _) -> Finite 0
(_, Finite 0) -> Finite 0
(Finite n, Finite n') -> Finite (n*n')
_ -> Infinite
But then I get stuck. The straightforward thing for individual fields certainly doesn't work:
instance Sized c => GSized (K1 i c) where
gcardinality = cardinality @c
For recursive types, this is a very straightforward infinite loop. I've tried a variety of means of enriching the two classes involved here.
- I generalized
cardinalityandgcardinalityto be functions, so that I could assume I already knew the size of recursive occurrences. Then in theK1instance I could ask: if all your recursive instances were uninhabited, how big would you be? What if all your recursive instances had one inhabitant? And so on. - I generalized from a single cardinality to a "recurrence relation" like
x = a + bx. The intended meaning ofx = a + bxis that the typexhasainhabitants that don't involve recursion at all, andbvalues that can be paired up with a recursive call. (We can definex*x = xwithout losing anything of interest.) Then equations likex = n + 0xcorrespond to enumerations,x = 0 + xcorrespond to trivial 1-element recursive types, andx = a + bxare infinite. - I used lazy naturals instead of efficient ones.
- I tracked sets of possible cardinalities, ruling them out one by one as new information was learned.
- I took a stab at using
Generic1instead ofGeneric.
None of these ended up working well. There seem to be three core, hard problems/failure modes:
- A loop in the
K1definition for recursive types, or it works okay for recursive types but falls over for mutually recursive types. K1is used both for recursive occurrences and plain fields, and there doesn't seem to be an easy way to distinguish between these.- Choosing
Finite 0instead ofFinite 1for trivially recursive types.
Is there an approach that solves these problems? Where the generic instances for [Void], [()], X, and Y from above are all defined and correct?