Search code examples
haskelltypeshaskell-lenstype-families

Type families can't return a RankN type -- workarounds or alternatives?


I'm playing with an extensible record library, and I'm wanting to write a function field that can operate as either a Lens or a Traversal based on whether or not the Symbol key is in the list of keys. The type family is given:

type family LensOrTraversal key keys s t a b where
    LensOrTraversal key '[] s t a b = 
        Traversal s t a b
    LensOrTraversal key (key =: val ': xs) s t a b = 
        Lens s t a b
    LensOrTraversal key (foo =: bar ': xs) s t a b = 
        LensOrTraversal key xs s t a b

This code gives me an error:

/home/matt/Projects/hash-rekt/src/Data/HashRecord/Internal.hs:433:5: 
error:
    • Illegal polymorphic type: Traversal s t a b
    • In the equations for closed type family ‘LensOrTraversal’
      In the type family declaration for ‘LensOrTraversal’

Ideally, I'd like to be able to reuse the field name for both lenses and traversals, as it would allow you to write

>>> let testMap = empty & field @"foo" .~ 'a'
>>> :t testMap
HashRecord '["foo" =: Char]
>>> testMap ^. field @"foo" 
'a'
>>> testMap ^. field @"bar"
Type error
>>> testMap ^? field @"bar"
Nothing

which follows common lens idioms. I can provide an fieldTraversal function that does what I want, but I'd prefer to overload the name field if possible. How would you work around this limitation of type families?


Solution

  • A lens is already a traversal, only its Rank2 quantifier doesn't make use of the full constraint (it merely requires Functor, not Applicative).

    type Lens s t a b      = ∀ f . Functor f     => (a -> f b) -> s -> f t
    type Traversal s t a b = ∀ f . Applicative f => (a -> f b) -> s -> f t
    

    It is at the level of this constraint that you should introduce your type family:

    import GHC.Exts (Constraint)
    type family FieldOpticConstraint key keys :: (* -> *) -> Constraint where
      FieldOpticConstraint key '[] = Applicative
      FieldOpticConstraint key (key =: val ': xs) = Functor
      FieldOpticConstraint key (_ ': xs) = FieldOpticConstraint key xs
    

    Then field should not yield a LensOrTraversal, but always a custom Rank2-signature with the constraint determined by the type family.