用镜头代替记录投影功能
Replace record projection function with lenses
几乎每次我做记录时,我发现自己在之后立即添加 makeLenses ''Record
(来自 lens),而且我从来没有真正使用记录给我的投影函数。事实上,看看 makeLenses
生成的内容(使用 GHC -ddump-splices
标志),看起来它甚至没有使用那些投影函数,只是为它生成的镜头选择一个名称。
有没有什么方法,通过 TemplateHaskell
,或者通过预处理器,或者坦率地说,任何其他魔法,我可以让记录投影功能直接变成 Van Laarhoven 镜头?
明确地说,这意味着
data Record a b = Record { x :: a, y :: b }
将生成(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)
而不是
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
它不仅会摆脱样板 makeLenses
,还会释放命名空间(因为永远不会定义投影函数)。
这是一件小事,但是因为它附在我所有的记录上,而且记录又不是什么稀有的东西,我真的开始心烦了...
有一个名为 OverloadedRecordFields/MagicClasses. Adam Gundry is working on an active pull request. It, combined with OverloadedRecordLabels 的 GHC 扩展提案,旨在解决这个问题!
data Foo = Foo { x :: Int, y :: Int }
class IsLabel (x :: Symbol) a where
fromLabel :: Proxy# x -> a
对于像 Foo
这样的示例数据类型,表达式 #x (foo :: Foo)
中的子表达式 #x
将被编译器神奇地扩展为 fromLabel @"x" @Foo proxy#
。 @ 符号,类型应用程序符号,是另一个 GHC 8-ism。
与 x
不同,#x
的行为可以根据您的需要进行修改。你可能认为它只是一个常规的投影函数。启用 OverloadedLabels
后,我们已经可以访问多态投影函数 getField
:
instance HasField name big small => IsLabel name (big -> small) where
fromLabel proxy big = getField proxy big
或者我们可以用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)
有了这样的实例,您可以立即开始使用 #x
作为镜头:
over #x (* 2) (Foo 1008 0) -- evaluates to Foo 2016 0
几乎每次我做记录时,我发现自己在之后立即添加 makeLenses ''Record
(来自 lens),而且我从来没有真正使用记录给我的投影函数。事实上,看看 makeLenses
生成的内容(使用 GHC -ddump-splices
标志),看起来它甚至没有使用那些投影函数,只是为它生成的镜头选择一个名称。
有没有什么方法,通过 TemplateHaskell
,或者通过预处理器,或者坦率地说,任何其他魔法,我可以让记录投影功能直接变成 Van Laarhoven 镜头?
明确地说,这意味着
data Record a b = Record { x :: a, y :: b }
将生成(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)
而不是
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
它不仅会摆脱样板 makeLenses
,还会释放命名空间(因为永远不会定义投影函数)。
这是一件小事,但是因为它附在我所有的记录上,而且记录又不是什么稀有的东西,我真的开始心烦了...
有一个名为 OverloadedRecordFields/MagicClasses. Adam Gundry is working on an active pull request. It, combined with OverloadedRecordLabels 的 GHC 扩展提案,旨在解决这个问题!
data Foo = Foo { x :: Int, y :: Int }
class IsLabel (x :: Symbol) a where
fromLabel :: Proxy# x -> a
对于像 Foo
这样的示例数据类型,表达式 #x (foo :: Foo)
中的子表达式 #x
将被编译器神奇地扩展为 fromLabel @"x" @Foo proxy#
。 @ 符号,类型应用程序符号,是另一个 GHC 8-ism。
与 x
不同,#x
的行为可以根据您的需要进行修改。你可能认为它只是一个常规的投影函数。启用 OverloadedLabels
后,我们已经可以访问多态投影函数 getField
:
instance HasField name big small => IsLabel name (big -> small) where
fromLabel proxy big = getField proxy big
或者我们可以用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)
有了这样的实例,您可以立即开始使用 #x
作为镜头:
over #x (* 2) (Foo 1008 0) -- evaluates to Foo 2016 0