kcsongor / generic-lens

Generically derive traversals, lenses, and prisms.
435 stars 53 forks source link

GetField doesn't work with data families #96

Open isovector opened 4 years ago

isovector commented 4 years ago

The following program works!

data Foo0
  = FooV0
    { _fooId :: Int
    , _fooName :: String
    }
  deriving (Generic, Show, Eq)

data Foo1
  = FooV1
  { _fooId        :: Int
  , _fooName      :: String
  , _fooHonorific :: String
  }
  deriving (Generic, Show, Eq)

v1 = FooV1 2 "james" "sir"
v0 = FooV0 undefined undefined

v0' = copyField @"_fooId" @Int v1 $ copyField @"_fooName" @String v1 v0

copyField
    :: forall name t from to
     . ( HasField name to   to   t t
       , HasField name from from t t
       )
    => from
    -> to
    -> to
copyField f t =
  t & field @name @to @to @t @t .~ f ^. field @name @from @from @t @t

but if you turn Foo into a data family, it doesn't:

data family Foo (a :: Nat)

data instance Foo 0
  = FooV0
    { _fooId :: Int
    , _fooName :: String
    }
  deriving (Generic, Show, Eq)

data instance Foo 1
  = FooV1
  { _fooId        :: Int
  , _fooName      :: String
  , _fooHonorific :: String
  }
  deriving (Generic, Show, Eq)

with the following error message:

/home/sandy/prj/YoloSwag69/src/Lib.hs:39:7: error:
    • Couldn't match type ‘generic-lens-1.1.0.0:Data.Generics.Internal.Families.Changing.ReplaceArgs
                             (Foo 0)
                             (generic-lens-1.1.0.0:Data.Generics.Internal.Families.Changing.Unify
                                b'1 Int)’
                     with ‘Foo 0’
        arising from a use of ‘copyField’
      The type variable ‘b'1’ is ambiguous
    • In the expression: copyField @"_fooId" @Int v1
      In the expression:
        copyField @"_fooId" @Int v1 $ copyField @"_fooName" @String v1 v0
      In an equation for ‘v0'’:
          v0'
            = copyField @"_fooId" @Int v1 $ copyField @"_fooName" @String v1 v0
   |
39 | v0' = copyField @"_fooId" @Int v1 $ copyField @"_fooName" @String v1 v0
   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^

/home/sandy/prj/YoloSwag69/src/Lib.hs:39:37: error:
    • Couldn't match type ‘generic-lens-1.1.0.0:Data.Generics.Internal.Families.Changing.ReplaceArgs
                             (Foo 0)
                             (generic-lens-1.1.0.0:Data.Generics.Internal.Families.Changing.Unify
                                b'0 [Char])’
                     with ‘Foo 0’
        arising from a use of ‘copyField’
      The type variable ‘b'0’ is ambiguous
    • In the second argument of ‘($)’, namely
        ‘copyField @"_fooName" @String v1 v0’
      In the expression:
        copyField @"_fooId" @Int v1 $ copyField @"_fooName" @String v1 v0
      In an equation for ‘v0'’:
          v0'
            = copyField @"_fooId" @Int v1 $ copyField @"_fooName" @String v1 v0
   |
39 | v0' = copyField @"_fooId" @Int v1 $ copyField @"_fooName" @String v1 v0
   |                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

HELP PLEASE!!!!!

isovector commented 4 years ago

Using field' instead of field seems to work /shrug

kcsongor commented 4 years ago

Yes, unfortunately this is known to not work. In general, type parameters that have nominal roles are not supported well. field' is a possible workaround. I'm now thinking it would be good to stick this into the documentation somewhere?

neongreen commented 4 years ago

You can solve this on a case by case basis if you don't actually want polymorphic updates. (And.. maybe even if you do?)

Let's say you have:

data RefundT f = Refund { id :: B.C f String, ... }

Now, #id .~ "test" will not work on a Refund Identity. But it will if you add the following instance:

import qualified Data.Generics.Labels as GL
import qualified Data.Generics.Product.Fields as GL

instance
  {-# OVERLAPPING #-}
  (GL.HasField' name (RefundT f) a, f ~ g, a ~ b) =>
  GL.HasField name (RefundT f) (RefundT g) a b
  where
  field = GL.field' @name

This is only if you want to use the #field syntax.

michaelpj commented 1 year ago

Just wanted to add a use case where I hit this. I was thinking about using generic-lens for lsp-types in order to avoid a lens dependency on the types library. However, the lsp library features a bunch of types like this:

data Method = ...
type family MessageParams (m :: Method) where ...
data Message (m :: Method) = Message {
  params :: MessageParams m
  ...
}

Trying to use #params to access the field gives a very similar message to the OP.

Using field' works, and oddly it works if I split it out into separate let-bindings, i.e.

let t = msg ^. #params . #textDocument

doesn't work but

let p = msg ^. #params
      t = p ^. #textDocument

does.

It would be nice if the label syntax worked, but :shrug:

arybczak commented 1 year ago

For the record, built-in generic lenses (also usable via OverloadedLabels) in optics have full support for data families.

michaelpj commented 3 months ago

@arybczak I don't suppose you have any idea why it works in optics but not here? This is annoying enough I want to fix it :joy:

arybczak commented 3 months ago

@michaelpj no idea, implementation in here is quite different to what's in optics.