-->

Replace record projection function with lenses

2019-05-04 02:29发布

问题:

Almost every time I make a record, I find myself adding makeLenses ''Record (from lens) immediately afterwards, and I never end up actually using the projection functions that the record gives me. In fact, looking at what makeLenses produces (with the GHC -ddump-splices flag), it looks like not even it uses those projection functions, except to choose a name for the lenses it produces.

Is there some way, either through TemplateHaskell, or through a preprocessor, or frankly any other magic, that I could get record projection functions to be straight up Van Laarhoven lenses instead?

To be clear, that would mean that

data Record a b = Record { x :: a, y :: b }

Would generate (with type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t)

x :: forall a b c. Lens (Record a b) (Record c b) a c
x f (Record _x _y) = fmap (\x' -> Record x' _y) (f _x)

y :: forall a b c. Lens (Record a b) (Record a c) b c
y f (Record _x _y) = fmap (\ y' -> Record _x y') (f _y)

Instead of

x :: forall a b. Record a b -> a
x (Record _x _y) = _x

y :: forall a b. Record a b -> b
y (Record _x _y) = _y

It would not only get rid of the boilerplate makeLenses but also free up the namespace (since the projection functions wouldn't ever be defined).

This is such a minor thing, but since it's attached to all my records, and records aren't such a rare thing, it is really starting to get on my nerves...

回答1:

There is a GHC extensions proposal called OverloadedRecordFields/MagicClasses. Adam Gundry is working on an active pull request. It, combined with OverloadedRecordLabels, are intended to address this very problem!

data Foo = Foo { x :: Int, y :: Int }

class IsLabel (x :: Symbol) a where
  fromLabel :: Proxy# x -> a

With an example datatype like Foo, the subexpression #x in the expression #x (foo :: Foo) will be magically expanded by the compiler to fromLabel @"x" @Foo proxy#. That @ symbol, the type application symbol, is another GHC 8-ism.

Unlike x, the behavior of #x can be modified to suit your needs. You might have it be just a regular projection function. With OverloadedLabels enabled, we already have access to a polymorphic projection function getField:

instance HasField name big small => IsLabel name (big -> small) where
  fromLabel proxy big = getField proxy big

Or we could satisfy the constraint with stab-style lenses:

instance ( Functor f
         , HasField name big small
         , UpdateField name big big' small') =>
         IsLabel name ((small -> f small') -> (big -> big')) where
  fromLabel proxy f big =
    setField proxy big <$> f (getField proxy big)

With such an instance you could immediately start using #x as a lens:

over #x (* 2) (Foo 1008 0) -- evaluates to Foo 2016 0