Search code examples
haskellcoercionderivingviahaskell-optics

Unable to coerce during `DerivingVia` when using optics types like `Prism'`


I'm unable to automatically derive instances using DerivingVia on any type that uses types from optics-core like Prism'; the error I get from compiler is:

src/Main.hs:24:13-19: error:
    • Couldn't match type ‘Foo’ with ‘Stringable Foo’
        arising from the coercion of the method ‘mkPrism’
          from type ‘MyPrism (Stringable Foo)’ to type ‘MyPrism Foo’
    • When deriving the instance for (MkPrism Foo)
   |
24 |   deriving (MkPrism) via (Stringable Foo)
   |             ^^^^^^^

The type in question is:

newtype MyPrism a
  = MyPrism (Prism' String a)

The compilation succeeds if I replace the Prism' type with its underlying representation, viz.:

newtype MyPrism a
  = MyPrism (a -> String, String -> Maybe a)
  • Why do I see coercion issues when using Prism'? Is it because the Optic constructor is not exported by the package?
  • What is the usual way to solve this?

You can see the full program to reproduce this here: https://github.com/srid/q-optics-coercion/blob/f1e3df5e1c5c0e4a51eacb6439fb8c627e4c7bd5/src/Main.hs ... if you have Nix installed, you can run bin/run in this repo.


EDIT: Once I import Optics.Internal.Optic, the error message becomes:

src/Main.hs:25:13-19: error:
    • Couldn't match representation of type ‘p i Foo Foo’
                               with that of ‘p i (Stringable Foo) (Stringable Foo)’
        arising from the coercion of the method ‘mkPrism’
          from type ‘MyPrism (Stringable Foo)’ to type ‘MyPrism Foo’
    • When deriving the instance for (MkPrism Foo)
   |
25 |   deriving (MkPrism) via (Stringable Foo)
   |

Solution

  • Generally speaking, profunctor optics (such as those used in the lens and optics packages) aren't directly coercible. Roughly speaking, the internal representation of a prism is something like:

    newtype Prism' s a 
      = Prism' (forall p f. (Choice p, Applicative f) => p a (f a) -> p s (f s))
    

    (Well, at least that's what it is for Control.Lens. For Optics.Core, it might be a little more complicated.)

    The problem here is that a and s appear as parameters to higher-kinded type variables (f and p) in this type. As a result, their "roles" are nominal (see the Role Inference section in the GHC manual for an explanation why), and these nominal roles prevent direct coercion.

    Fortunately, there are some functions in Optics.Coerce that are designed to deal with this. You won't be able to derive a MyPrism type directly via the newtype, but you should be able to write:

    instance MkPrism Foo where
      mkPrism = MyPrism . coerceA . coerceB $ p
        where MyPrism p = mkPrism :: MyPrism (Stringable Text)