如何同时将镜头(或任何其他光学器件)视为 getter 和 setter?
How to treat a lens (or any other optic) as a getter and setter simultaneously?
我正在尝试编写一个通用的记录更新程序,它将允许人们轻松地更新 existing
记录中的字段,以及具有类似形状的 incoming
记录中的字段。这是我到目前为止所拥有的:
applyUpdater fields existing incoming =
let getters = DL.map (^.) fields
setters = DL.map set fields
updaters = DL.zipWith (,) getters setters
in DL.foldl' (\updated (getter, setter) -> setter (getter incoming) updated) existing updaters
我希望按以下方式使用它:
applyUpdater
[email, notificationEnabled] -- the fields to be copied from incoming => existing (this obviously assumed that `name` and `email` lenses have already been setup
User{name="saurabh", email="blah@blah.com", notificationEnabled=True}
User{name="saurabh", email="foo@bar.com", notificationEnabled=False}
这不起作用,可能是因为 Haskell 为 applyUpdater
推断出一个非常奇怪的类型签名,这意味着它没有做我期望的事情:
applyUpdater :: [ASetter t1 t1 a t] -> t1 -> Getting t (ASetter t1 t1 a t) t -> t1
这是一个代码示例和编译错误:
module TryUpdater where
import Control.Lens
import GHC.Generics
import Data.List as DL
data User = User {_name::String, _email::String, _notificationEnabled::Bool} deriving (Eq, Show, Generic)
makeLensesWith classUnderscoreNoPrefixFields ''User
-- applyUpdater :: [ASetter t1 t1 a t] -> t1 -> Getting t (ASetter t1 t1 a t) t -> t1
applyUpdater fields existing incoming =
let getters = DL.map (^.) fields
setters = DL.map set fields
updaters = DL.zipWith (,) getters setters
in DL.foldl' (\updated (getter, setter) -> setter (getter incoming) updated) existing updaters
testUpdater :: User -> User -> User
testUpdater existingUser incomingUser = applyUpdater [email, notificationEnabled] existingUser incomingUser
编译错误:
18 62 error error:
• Couldn't match type ‘Bool’ with ‘[Char]’
arising from a functional dependency between:
constraint ‘HasNotificationEnabled User String’
arising from a use of ‘notificationEnabled’
instance ‘HasNotificationEnabled User Bool’
at /Users/saurabhnanda/projects/vl-haskell/.stack-work/intero/intero54587Sfx.hs:8:1-51
• In the expression: notificationEnabled
In the first argument of ‘applyUpdater’, namely
‘[email, notificationEnabled]’
In the expression:
applyUpdater [email, notificationEnabled] existingUser incomingUser (intero)
18 96 error error:
• Couldn't match type ‘User’
with ‘(String -> Const String String)
-> ASetter User User String String
-> Const String (ASetter User User String String)’
Expected type: Getting
String (ASetter User User String String) String
Actual type: User
• In the third argument of ‘applyUpdater’, namely ‘incomingUser’
In the expression:
applyUpdater [email, notificationEnabled] existingUser incomingUser
In an equation for ‘testUpdater’:
testUpdater existingUser incomingUser
= applyUpdater
[email, notificationEnabled] existingUser incomingUser (intero)
首先,请注意 (^.)
将镜头作为其 正确的 参数,因此您真正想要的实际上是 getters = DL.map (flip (^.)) fields
,也就是 DL.map view field
.
但这里更有趣的问题是:光学需要更高阶的多态性,因此 GHC 只能猜测 类型。出于这个原因,总是从开始使用类型签名!
天真地,你可能会写
applyUpdater :: [Lens' s a] -> s -> s -> s
好吧,这实际上行不通,因为 Lens'
包含一个 ∀
量词,因此将其放在列表 would require impredicative polymorphism, which GHC isn't really capable of 中。常见问题,因此镜头库有两种解决方法:
ALens
只是 Functor
约束的特定实例,选择它是为了保留完整的通用性。但是,您需要使用不同的组合器来应用它。
applyUpdater :: [ALens' s a] -> s -> s -> s
applyUpdater fields existing incoming =
let getters = DL.map (flip (^#)) fields
setters = DL.map storing fields
updaters = DL.zipWith (,) getters setters
in DL.foldl' (\upd (γ, σ) -> σ (γ incoming) upd) existing updaters
因为 ALens
严格来说是 Lens
的实例化,您可以完全按照您的预期使用它。
ReifiedLens
保留原始多态性,但将其包装在新类型中,以便镜头可以存储在例如一个列表。然后可以像往常一样使用包裹的镜头,但是您需要明确包裹它们才能传递给您的函数;这可能不值得为您的应用程序带来麻烦。当您想以不太直接的方式重新使用存储的镜头时,此方法更有用。 (这也可以用 ALens
完成,但它需要 cloneLens
,我认为这对性能不利。)
applyUpdater
现在将按照我在 ALens'
中解释的方式工作,但它只能与镜头列表一起使用 相同类型的所有焦点。将镜头聚焦在列表中 不同 类型的字段显然是类型错误。要做到这一点,你 必须 将镜头包裹在一些新类型中以隐藏类型参数——没有办法绕过它,根本不可能统一 email
和 [= 的类型30=] 到你可以在一个列表中填充的东西。
但在经历那个麻烦之前,我会强烈考虑 不 将任何镜头存储在列表中:基本上只是组合所有访问共享引用的更新函数.好吧,直接这样做——“所有访问共享引用”很方便,正是 monad 为您提供的功能,所以写起来很简单
applyUpdater :: [s -> r -> s] -> s -> r -> s
applyUpdater = foldr (>=>) pure
要将镜头转换为单独的更新程序函数,请编写
mkUpd :: ALens' s a -> s -> s -> s
mkUpd l exi inc = storing l (inc^#l) exi
像
一样使用
applyUpdater
[mkUpd email, mkUpd notificationEnabled]
User{name="saurabh", email="blah@blah.com", notificationEnabled=True}
User{name="saurabh", email="foo@bar.com", notificationEnabled=False}
根据@leftaroundabout 的回答,还有一种基于元组的方法:
applyUpdater2 (f1, f2) existing incoming = (storing f2 (incoming ^# f2)) $ (storing f1 (incoming ^# f1) existing)
applyUpdater3 (f1, f2, f3) existing incoming = (storing f3 (incoming ^# f3)) $ (applyUpdater2 (f1, f2) existing incoming)
applyUpdater4 (f1, f2, f3, f4) existing incoming = (storing f4 (incoming ^# f4)) $ (applyUpdater3 (f1, f2, f3) existing incoming)
-- and so on
可以通过以下方式使用:
testUpdater :: User -> User -> User
testUpdater existingUser incomingUser = applyUpdater2 (email, notificationEnabled) existingUser incomingUser
设置 applyUpdaterN
直到,比如 32 元组,应该很容易。之后,一切都归结为个人喜好和实际用例。您 可能 不想在每个调用站点都用 mkUpd
包装您的更新程序。另一方面,如果您想动态生成 updaters
列表,那么使用列表比使用元组更容易。
我正在尝试编写一个通用的记录更新程序,它将允许人们轻松地更新 existing
记录中的字段,以及具有类似形状的 incoming
记录中的字段。这是我到目前为止所拥有的:
applyUpdater fields existing incoming =
let getters = DL.map (^.) fields
setters = DL.map set fields
updaters = DL.zipWith (,) getters setters
in DL.foldl' (\updated (getter, setter) -> setter (getter incoming) updated) existing updaters
我希望按以下方式使用它:
applyUpdater
[email, notificationEnabled] -- the fields to be copied from incoming => existing (this obviously assumed that `name` and `email` lenses have already been setup
User{name="saurabh", email="blah@blah.com", notificationEnabled=True}
User{name="saurabh", email="foo@bar.com", notificationEnabled=False}
这不起作用,可能是因为 Haskell 为 applyUpdater
推断出一个非常奇怪的类型签名,这意味着它没有做我期望的事情:
applyUpdater :: [ASetter t1 t1 a t] -> t1 -> Getting t (ASetter t1 t1 a t) t -> t1
这是一个代码示例和编译错误:
module TryUpdater where
import Control.Lens
import GHC.Generics
import Data.List as DL
data User = User {_name::String, _email::String, _notificationEnabled::Bool} deriving (Eq, Show, Generic)
makeLensesWith classUnderscoreNoPrefixFields ''User
-- applyUpdater :: [ASetter t1 t1 a t] -> t1 -> Getting t (ASetter t1 t1 a t) t -> t1
applyUpdater fields existing incoming =
let getters = DL.map (^.) fields
setters = DL.map set fields
updaters = DL.zipWith (,) getters setters
in DL.foldl' (\updated (getter, setter) -> setter (getter incoming) updated) existing updaters
testUpdater :: User -> User -> User
testUpdater existingUser incomingUser = applyUpdater [email, notificationEnabled] existingUser incomingUser
编译错误:
18 62 error error:
• Couldn't match type ‘Bool’ with ‘[Char]’
arising from a functional dependency between:
constraint ‘HasNotificationEnabled User String’
arising from a use of ‘notificationEnabled’
instance ‘HasNotificationEnabled User Bool’
at /Users/saurabhnanda/projects/vl-haskell/.stack-work/intero/intero54587Sfx.hs:8:1-51
• In the expression: notificationEnabled
In the first argument of ‘applyUpdater’, namely
‘[email, notificationEnabled]’
In the expression:
applyUpdater [email, notificationEnabled] existingUser incomingUser (intero)
18 96 error error:
• Couldn't match type ‘User’
with ‘(String -> Const String String)
-> ASetter User User String String
-> Const String (ASetter User User String String)’
Expected type: Getting
String (ASetter User User String String) String
Actual type: User
• In the third argument of ‘applyUpdater’, namely ‘incomingUser’
In the expression:
applyUpdater [email, notificationEnabled] existingUser incomingUser
In an equation for ‘testUpdater’:
testUpdater existingUser incomingUser
= applyUpdater
[email, notificationEnabled] existingUser incomingUser (intero)
首先,请注意 (^.)
将镜头作为其 正确的 参数,因此您真正想要的实际上是 getters = DL.map (flip (^.)) fields
,也就是 DL.map view field
.
但这里更有趣的问题是:光学需要更高阶的多态性,因此 GHC 只能猜测 类型。出于这个原因,总是从开始使用类型签名!
天真地,你可能会写
applyUpdater :: [Lens' s a] -> s -> s -> s
好吧,这实际上行不通,因为 Lens'
包含一个 ∀
量词,因此将其放在列表 would require impredicative polymorphism, which GHC isn't really capable of 中。常见问题,因此镜头库有两种解决方法:
ALens
只是Functor
约束的特定实例,选择它是为了保留完整的通用性。但是,您需要使用不同的组合器来应用它。applyUpdater :: [ALens' s a] -> s -> s -> s applyUpdater fields existing incoming = let getters = DL.map (flip (^#)) fields setters = DL.map storing fields updaters = DL.zipWith (,) getters setters in DL.foldl' (\upd (γ, σ) -> σ (γ incoming) upd) existing updaters
因为
ALens
严格来说是Lens
的实例化,您可以完全按照您的预期使用它。ReifiedLens
保留原始多态性,但将其包装在新类型中,以便镜头可以存储在例如一个列表。然后可以像往常一样使用包裹的镜头,但是您需要明确包裹它们才能传递给您的函数;这可能不值得为您的应用程序带来麻烦。当您想以不太直接的方式重新使用存储的镜头时,此方法更有用。 (这也可以用ALens
完成,但它需要cloneLens
,我认为这对性能不利。)
applyUpdater
现在将按照我在 ALens'
中解释的方式工作,但它只能与镜头列表一起使用 相同类型的所有焦点。将镜头聚焦在列表中 不同 类型的字段显然是类型错误。要做到这一点,你 必须 将镜头包裹在一些新类型中以隐藏类型参数——没有办法绕过它,根本不可能统一 email
和 [= 的类型30=] 到你可以在一个列表中填充的东西。
但在经历那个麻烦之前,我会强烈考虑 不 将任何镜头存储在列表中:基本上只是组合所有访问共享引用的更新函数.好吧,直接这样做——“所有访问共享引用”很方便,正是 monad 为您提供的功能,所以写起来很简单
applyUpdater :: [s -> r -> s] -> s -> r -> s
applyUpdater = foldr (>=>) pure
要将镜头转换为单独的更新程序函数,请编写
mkUpd :: ALens' s a -> s -> s -> s
mkUpd l exi inc = storing l (inc^#l) exi
像
一样使用applyUpdater
[mkUpd email, mkUpd notificationEnabled]
User{name="saurabh", email="blah@blah.com", notificationEnabled=True}
User{name="saurabh", email="foo@bar.com", notificationEnabled=False}
根据@leftaroundabout 的回答,还有一种基于元组的方法:
applyUpdater2 (f1, f2) existing incoming = (storing f2 (incoming ^# f2)) $ (storing f1 (incoming ^# f1) existing)
applyUpdater3 (f1, f2, f3) existing incoming = (storing f3 (incoming ^# f3)) $ (applyUpdater2 (f1, f2) existing incoming)
applyUpdater4 (f1, f2, f3, f4) existing incoming = (storing f4 (incoming ^# f4)) $ (applyUpdater3 (f1, f2, f3) existing incoming)
-- and so on
可以通过以下方式使用:
testUpdater :: User -> User -> User
testUpdater existingUser incomingUser = applyUpdater2 (email, notificationEnabled) existingUser incomingUser
设置 applyUpdaterN
直到,比如 32 元组,应该很容易。之后,一切都归结为个人喜好和实际用例。您 可能 不想在每个调用站点都用 mkUpd
包装您的更新程序。另一方面,如果您想动态生成 updaters
列表,那么使用列表比使用元组更容易。