ekmett / lens

Lenses, Folds, and Traversals - Join us on web.libera.chat #haskell-lens
http://lens.github.io/
Other
2.03k stars 274 forks source link

Add coercion Iso #579

Closed treeowl closed 9 years ago

treeowl commented 9 years ago

I couldn't find anything like this, which of course doesn't mean there isn't such.

coercionToIso' :: Coercion a b -> Iso' a b
coercionToIso' c = case (c, sym c) of
                     (Coercion, Coercion) ->
                         rmap (fmap coerce) . lmap coerce

It might also be useful to offer this "out of the air" version:

coercibleIso' :: Coercible a b => Iso' a b
coercibleIso' = coercionToIso' Coercion
treeowl commented 9 years ago

A slightly prettier expression:

coercionToIso' c =  rmap (fmap . coerceWith $ sym c) . lmap (coerceWith c)
treeowl commented 9 years ago

Ah, but the prettier one is probably lazy in an unfortunate way.

ekmett commented 9 years ago

rmap f . lmap g should be able to be rewritten in terms of dimap.

The major concern I'd have about such a combinator is how bad inference will be when using it in practice.

Using Control.Lens.Internal.Coerce to get coerce' we have:

iso coerce coerce' :: (Coercible t b, Coercible s a) => Iso s t a b

Using coerce' reverses the second Coercible constraint there so that when we instantiate s = t, a = b, this becomes the definition you gave, but subsumes it otherwise.

treeowl commented 9 years ago

There are a few options for the more general combinator, unfortunately. One is to use the fact that Coercible instances floating in the air generally come in pairs, so instead of

coercibleIso1 :: forall s t a b . (Coercible t b, Coercible s a) => Iso s t a b
coercibleIso1 = dimap coerce (fmap (coerce (id :: t -> t)))

we could use

coercibleIso2 :: forall s t a b . (Coercible b t, Coercible s a) => Iso s t a b
coercibleIso2 = dimap coerce (fmap coerce)

Inference does seem likely to be on the poor side, but I don't think that's a reason to exclude these—someone who wants one can just use a type signature. The Iso' version,

coercibleIso' :: forall a b . Coercible a b => Iso' a b
coercibleIso' = dimap coerce (fmap (coerce (id :: a -> a)))

seems likely to offer considerably better inference. I am a bit concerned about the core I see without specialization. I get

coercibleIso'
  :: forall a_a4T0 b_a4T1.
     Coercible a_a4T0 b_a4T1 =>
     Iso' a_a4T0 b_a4T1
coercibleIso' =
  \ (@ a_a56o)
    (@ b_a56p)
    ($dCoercible_a5bz :: Coercible a_a56o b_a56p)
    (@ (p_a5bC :: * -> * -> *))
    (@ (f_a5bD :: * -> *))
    ($dProfunctor_a5bE :: Profunctor p_a5bC)
    ($dFunctor_a5bF :: Functor f_a5bD) ->
    dimap
      $dProfunctor_a5bE
      (\ (tpl_B2 :: a_a56o) ->
         case $dCoercible_a5bz of _ { MkCoercible tpl1_B3 ->
         tpl_B2 `cast` ...
         })
      (fmap
         $dFunctor_a5bF
         (case $dCoercible_a5bz of _ { MkCoercible $dCoercible2_d5mJ ->
          (id) `cast` ...
          }))

with the Coercible "dictionaries" unpacked on the wrong side of the dimap. I don't know why that's happening.

ekmett commented 9 years ago

Using coerce' there means that in the simple case where you use it as a getter, or monomorphically you only wind up needing to construct and pass the one constraint.

The combinators that consume a getter all do that unification for you. This is why view requires both arguments to match. Otherwise things like _JSON which pick up a FromJSON constraint on one side and a ToJSON constraint on the other would require tons of type signatures.

ekmett commented 9 years ago

We can optimize this further by using something like

coerce #. lmap (fmap coerce')

This will lift the one coerce out of the Profunctor.

treeowl commented 9 years ago

Makes sense, although I still don't understand why the MkCoercible gets unpacked so late.