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.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With