组合镜头

17

使用一个镜头库,我可以对单个目标应用修改函数,示例如下:

Prelude Control.Lens> (1, 'a', 2) & _1 %~ (*3)
(3,'a',2)
Prelude Control.Lens> (1, 'a', 2) & _3 %~ (*3)
(1,'a',6)

我该如何将这些单独的镜头(_1_3)结合起来,以便能够同时对两个目标执行此更新? 我期望以下类似的方法:

Prelude Control.Lens> (1, 'a', 2) & ??? %~ (*3)
(3,'a',6)

我不确定是否有一个合理的实现这样的操作。假设将两个镜头组合的运算符是 (&&&)。它必须具有类似于 (&&&) :: Lens a b -> Lens a b -> Lens a b 的类型,以便您可以像使用组合它的两个镜头一样使用它。鉴于 view _1 (1,2) = 1view _2 (1,2) = 2,您期望 view (_1 &&& _2) (1,2) 的结果是什么? - Chris Taylor
@ChrisTaylor 我并不真正需要“getter”功能。虽然据我所知,在这个库中,通常使用单子来处理这种情况,例如Traversal - Nikita Volkov
2
我在下面的评论中已经放了链接,但是万一有人错过了,这里有一个关于结合遍历和相关问题的镜头问题 - shachaf
通常情况下,您不能安全地组合镜头或设置器,因为没有办法强制执行两个镜头或设置器不重叠。 - Russell O'Connor
2个回答

19
使用 Control.Lens.Internal.Setter 中的 Settable 类型类中的 untainted,可以将两个 setter 结合起来,但结果仍然只是一个 setter 而不是 getter。
import Control.Lens.Internal.Setter

-- (&&&) is already taken by Control.Arrow
(~&~) :: (Settable f) => (c -> d -> f a) -> (c -> a -> t) -> c -> d -> t
(~&~) a b f = b f . untainted . a f

您可以进行测试:
>>> import Control.Lens
>>> (1, 'a', 2) & (_1 ~&~ _3) %~ (*3)
(3,'a',6)

编辑

实际上您不需要使用内部函数。您可以利用Mutator是一个单子的事实:

{-# LANGUAGE NoMonomorphismRestriction #-}

import Control.Monad
import Control.Applicative

(~&~) = liftA2 (>=>)

-- This works too, and is maybe easier to understand: 
(~&~) a b f x = a f x >>= b f

谢谢!也许有一种方法可以构建Traversal吗? - Nikita Volkov
4
抱歉,通常它并不能正常工作。 - shachaf
1
(_1 & _1) 不满足Setter(也称为SEC)法则。 - Russell O'Connor
1
((_1 & _1) %~ ('y':)) . ((_1 & _1) %~ ('z':)) $ ("",()) === ("yyzz",())当(_1 & _1) %~ (('y':) . ('z':)) $ ("",()) === ("yzyz",()) - Russell O'Connor
@RussellO'Connor 那么换句话说,只有当 setter1setter2 是 "正交的" 时,setter1 ~&~ setter2 才是有效的 setter? - Dan Burton

7

你所询问的问题有一个更为普遍的变体:

(/\)
    :: (Functor f)
    => ((a -> (a, a)) -> (c -> (a, c)))
    -- ^ Lens' c a
    -> ((b -> (b, b)) -> (c -> (b, c)))
    -- ^ Lens' c b
    -> (((a, b) -> f (a, b)) -> (c -> f c))
    -- ^ Lens' c (a, b)
(lens1 /\ lens2) f c0 =
    let (a, _) = lens1 (\a_ -> (a_, a_)) c0
        (b, _) = lens2 (\b_ -> (b_, b_)) c0
        fab = f (a, b)
    in fmap (\(a, b) ->
            let (_, c1) = lens1 (\a_ -> (a_, a)) c0
                (_, c2) = lens2 (\b_ -> (b_, b)) c1
            in c2
            ) fab

infixl 7 /\

只需要关注带有透镜类型同义词签名的类型签名:

Lens' c a -> Lens' c b -> Lens' c (a, b)

它需要两个镜头,并将它们合并成一对字段的镜头。这更为通用,适用于将指向不同类型字段的镜头组合在一起。然而,那么你就必须分别改变这两个字段。

我只是想提供这个解决方案,以防有人正在寻找类似的东西。


1
一种更简洁的类型,适用于像这样的人 (/\) :: LensLike' ((,) a) c a -> LensLike' ((,) b) c b -> Lens' c (a, b)。此外,还有明显的限制,(/|\) :: LensLike' ((,) a) c a -> LensLike' ((,) a) c a -> Traversal' c a,其中 a /|\ b = (a /\ b) . both。为什么它们不在 lens 中呢? - J. Abrahamson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接