如何将镜片(或任何其他镜片)同时作为吸气剂和固定剂处理?

Sau*_*nda 1 haskell haskell-lens

我正在尝试编写一个通用记录更新程序,它允许用户轻松更新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
Run Code Online (Sandbox Code Playgroud)

我希望以下列方式使用它:

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}
Run Code Online (Sandbox Code Playgroud)

这不起作用,可能是因为Haskell推断出一个非常奇怪的类型签名,applyUpdater这意味着它没有做我期望它做的事情:

applyUpdater :: [ASetter t1 t1 a t] -> t1 -> Getting t (ASetter t1 t1 a t) t -> t1
Run Code Online (Sandbox Code Playgroud)

这是一个代码示例和编译错误:

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
Run Code Online (Sandbox Code Playgroud)

编译错误:

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)
Run Code Online (Sandbox Code Playgroud)

lef*_*out 5

首先,请注意(^.)将镜头作为正确的参数,所以你真正想要的是,实际上getters = DL.map (flip (^.)) fields,也就是说DL.map view field.

但这里更有趣的问题是:光学需要更高级别的多态性,因此GHC只能猜测类型.因此,始终类型签名开始!

天真地,你可能会写

applyUpdater :: [Lens' s a] -> s -> s -> s
Run Code Online (Sandbox Code Playgroud)

嗯,这实际上并不起作用,因为Lens'包含一个?量词,所以把它放在一个列表中需要不可预测的多态性,而GHC实际上并不具备这种多态性.常见问题,所以镜头库有两种方法可以解决这个问题:

  • 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
    
    Run Code Online (Sandbox Code Playgroud)

    因为ALens严格来说是实例化Lens,所以你可以按照预期的方式使用它.

  • ReifiedLens保持原始多态性,但将其包装在新类型中,以便镜头可以存储在例如列表中.然后可以像往常一样使用包装好的镜头,但是你需要明确地将它们包装起来以传递给你的功能; 这可能不值得你的申请麻烦.当您想以较不直接的方式重复使用存储的镜头时,此方法更有用.(这也可以用ALens,但cloneLens我认为这对性能有害.)

applyUpdater现在将以我解释的方式工作ALens',但它只能用于所有相同类型的场聚焦的镜头列表.将镜头聚焦在列表中的不同类型的字段上显然是类型错误.要做到这一点,你必须将镜头包装成一些新类型以隐藏类型参数 - 没有办法绕过它,根本不可能将你可以填充的类型emailnotificationEnabled东西统一在一个列表中.

但在经历这个麻烦之前,我会强烈考虑不要将任何镜头存储在列表中:基本上只是组成更新函数才能访问共享引用.那么,直接这样做 - "所有访问共享引用"都很方便,monad为你提供的功能,所以编写它是微不足道的

applyUpdater :: [s -> r -> s] -> s -> r -> s
applyUpdater = foldr (>=>) pure
Run Code Online (Sandbox Code Playgroud)

要将镜头转换为单个更新器功能,请写入

mkUpd :: ALens' s a -> s -> s -> s
mkUpd l exi inc = storing l (inc^#l) exi
Run Code Online (Sandbox Code Playgroud)

用得像

applyUpdater 
  [mkUpd email, mkUpd notificationEnabled]
  User{name="saurabh", email="blah@blah.com", notificationEnabled=True}
  User{name="saurabh", email="foo@bar.com", notificationEnabled=False}
Run Code Online (Sandbox Code Playgroud)