Skip to content

Instantly share code, notes, and snippets.

@KaneTW
Created December 6, 2017 06:46
Show Gist options
  • Save KaneTW/370ca9f04a01bb5c4c3060ee0272fd15 to your computer and use it in GitHub Desktop.
Save KaneTW/370ca9f04a01bb5c4c3060ee0272fd15 to your computer and use it in GitHub Desktop.
makeFields is evil

Consider the following example

A.hs

module A where
import Control.Lens

data A = A { _aFoo :: Int, _aBar :: String }
makeFields ''A

B.hs

module B where

import Control.Lens
import A

data B 
  = B1 { _bFoo :: Int, _bBar :: Bool }
  | B2 { _bFoo :: Int }

makeFields ''B

Whoops.

lens-bug/B.hs:10:1: error:
    • Could not deduce (Applicative f) arising from a use of ‘pure’
      from the context: Functor f
        bound by the type signature for:
                   bar :: Functor f => (Bool -> f Bool) -> B -> f B
        at B.hs:10:1-14
      Possible fix:
        add (Applicative f) to the context of
          the type signature for:
            bar :: Functor f => (Bool -> f Bool) -> B -> f B
    • In the expression: pure (B2 x1_a6s2)
      In an equation for ‘bar’: bar _ (B2 x1_a6s2) = pure (B2 x1_a6s2)
      In the instance declaration for ‘HasBar B Bool’

No satisfactory resolution

  • Swapping import A in B with import B in A makes bar a Traversal everywhere, i.e. you can't use it as a Lens anymore (no view without a Monoid instance, ...)
  • Associated type family don't support polymorphic types (i.e. you can't do type LensChoice A Int = Lens' A Int), so you have to use ReifiedLens etc, which is a headache
  • Even if they did, you'd run into ambiguous types that require proxies or type application; view (runLens foo) (A 1 "b") fails to typecheck

Conclusion

Don't use makeFields, it's a red herring.

@mpickering
Copy link

generic-lens might work better for you. After renaming the fields suitably and deriving Generic.

> view (field @"bar") (A 5 "a")
"a"
> view (field @"bar") (B1 5 True)
<type error as not total>
> view (field @"foo") (B2 5)
5
> view (field @"foo") (B1 5 True)
5

@KaneTW
Copy link
Author

KaneTW commented Dec 6, 2017

That's interesting! I was thinking for splitting my sums into separate data types, using prisms for the different constructors and fields for the separate types, something like

data Channel = TextChannel TextChannel | VoiceChannel VoiceChannel
data TextChannel = TextChannel' { ... }
data VoiceChannel = VoiceChannel' { ... }

but generic-lens might be a better solution. I'll have to see what else I encounter when implementing this API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment