Isomorphisms between 3 and more types using lens

2020-08-17 06:57发布

问题:

Inspired by a question on polymorphic function between ADTs I'm trying to create isomorphisms between multiple (not just 2) types, so that every time I need an isomorphic but not the same type, I can sprinkle my code with some convert.

Suppose I have 3 ADTs:

data AB = A | B deriving (Show)
data CD = C | D deriving (Show)
data EF = E | F deriving (Show)

Using lens I can implement 2 isomorphisms between AB and CD, and CD and EF:

{-# LANGUAGE MultiParamTypeClasses #-}
class Isomorphic a b where
  convert :: Iso' a b

instance Isomorphic AB CD where
  convert = iso ab2cd cd2ab
    where ab2cd A = C
          ab2cd B = D
          cd2ab C = A
          cd2ab D = B

instance Isomorphic AB EF where
  convert = iso ab2ef ef2ab
    where ab2ef A = E
          ab2ef B = F
          ef2ab E = A
          ef2ab F = B

Converting A to E is easy: A^.convert :: EF. Converting D to B is also easy: D^.from convert :: AB. But if I want to convert from C to E via A, I have to annotate types for every intermediary conversion:

(C^.from convert :: AB)^.convert :: EF

I understand why compiler can't infer intermediate types. It could be that there are several isomorphisms through which one could get from C to E. But can I simplify my code so I don't manually annotate types everywhere?

I could just write yet another instance to convert directly between CD and EF, but what if I have more than 3 types? If I had 5 isomorphic types, I would have to specify 10 instances, because the number of isos between isomorphic objects is a number of edges in a complete graph, which is a triangular number. I'd rather specify n-1 instances, with a trade-off that I write more convert or from convert.

Is there an idiomatic way to establish isomorphisms between multiple types using Iso from lens so that there is least amount of boilerplate and I don't have to type-annotate everything? If I have to use TemplateHaskell for that, how do I do that?

The motivation is that in my work I have many ridiculously complicated but stupid types, where () -> (() -> ()) -> X and ((), X) are isomorphic to X. I have to manually wrap and unwrap everything and I would like some polymorphic way to reduce the complicated types to simpler isomorphic types.

回答1:

You can structure your isomorphisms as a star graph: have a canonical "hub" type to which all others connect. The downside is that you will have to specify the hub explicitly in each instance, and you will only be able to convert between types which share a hub. However your two requirements (good type inference, and a linear number of instances) will be met. Here's how you would do that:

{-# LANGUAGE TypeFamilies #-}
import Control.Lens
import Unsafe.Coerce

data AB = A | B deriving (Show)
data CD = C | D deriving (Show)
data EF = E | F deriving (Show)

class Isomorphic a where
    type Hub a
    convert :: Iso' a (Hub a)

viaHub :: (Isomorphic a, Isomorphic b, Hub a ~ Hub b) => a -> b
viaHub x = x ^. convert . from convert

instance Isomorphic AB where
    type Hub AB = AB
    convert = id

instance Isomorphic CD where
    type Hub CD = AB
    convert = unsafeCoerce -- because I'm too lazy to do it right

instance Isomorphic EF where
    type Hub EF = AB
    convert = unsafeCoerce

In ghci:

> viaHub A :: EF
E
> viaHub A :: CD
C
> viaHub E :: AB
A
> viaHub E :: CD
C

Here's how you might use this for your examples:

class Unit a where unit :: a
instance Unit () where unit = ()
instance Unit b => Unit (a -> b) where unit _ = unit

instance Isomorphic X where
    type Hub X = X
    convert = id

instance (Unit a, Isomorphic b) => Isomorphic (a -> b) where
    type Hub (a -> b) = Hub b
    convert = iso ($unit) const . convert

instance Isomorphic a => Isomorphic ((), a) where
    type Hub ((), a) = Hub a
    convert = iso snd ((,)()) . convert

instance Isomorphic a => Isomorphic (a, ()) where
    type Hub (a, ()) = Hub a
    convert = iso fst (flip(,)()) . convert

Now you will have, e.g.

viaHub :: (() -> (() -> ()) -> X) -> ((), X)