Haskell 使用一级镜头制作复杂镜头

Haskell use first level lenses to create complex lens

比方说,我有一个包含两个字段的对象:

data Example = Example { _position :: Int
                       , _storage  :: [Int]}

我如何构建聚焦于 storage 内的 position 元素的镜头?

此外,是否可以将通过镜头修改的 position 值限制在基于 storage 大小的范围内?

似乎 alongside 可以以某种方式使用,因为 Example 与元组同构,但我无法理解这样做的方法。

我不确定如何表述这个问题,所以找不到太多相关信息。

我认为棱镜应该更适合这种情况,因为 pos 可能是一个不大于列表长度的整数,或者是负数。

我想你可以使用类似 docu for prisms provide

的东西
nat :: Prism' Integer Natural
nat = prism toInteger $ \ i ->
   if i < 0
   then Left i
   else Right (fromInteger i)
storageAtPos Prism' Example Int
storageAtPos = prism $ aux
  where aux (Example p s) | p < 0 || p >= length s = Nothing
                          | otherwise = Just (s !! p)

注意:我没有 运行 这段代码 - 只是模拟了文档(现在没有 ghc)

更新

可能是

<s>storageAtPos = \p -> (p^.storage)^?(ix $ p^.pos)</s>

有效,但同样 - 我现在没有 ghc 来测试 - 正如@Gurkenglas 指出的那样,这不是 Prism

编辑:我误解了问题,原答案在最后。

我不知道有什么组合器可以满足您的需求,所以我写了一个。

(^>>=) :: Lens' s a -> (a -> Lens' s b) -> Lens' s b
--        Lens' s a -> (a -> (b -> f b) -> s -> f s) -> (b -> f b) -> s -> f s
-- (That previous line disregards a forall and the Functor constraints)
(x ^>>= f) btofb s = f (s ^. x) btofb s

省略类型签名并询问 ghci 应该会给我们最通用的类​​型签名,所以这里是:

:t (^>>=)
Getting a s a -> (a -> t1 -> s -> t) -> t1 -> s -> t

Getting 的文档:"When you see this in a type signature it indicates that you can pass the function a Lens, Getter, Traversal, Fold, Prism, Iso, or one of the indexed variants, and it will just "做正确的事"。"

右侧同样通用,允许Traversals/Prisms/etc..

请注意,如果指针不指向自身,这只会产生合法的 lenslikes。

现在应用这个组合器 - 你想要的组合是:

position ^>>= \p -> storage . ix p

这样算出来是个遍历,看原回答

或者,使用另一个我喜欢的组合器:

let (f .: g) x = f . g x in position ^>>= (storage .: ix)

任何带有一些中缀声明的,你甚至可以去掉那些括号。


(这个原始答案假设 position :: Int 局部遮挡位置镜头。)

我们不知道列表在那个位置是否有值,所以这不是Lens',而是Traversal',它代表"traversing over any number of values"而不是"lensing onto one value"。

storage . ix position :: Traversal' Example Int

(^?) 将 return 遍历的第一个值(如果有的话),因此如果该位置有效,则该术语将为您提供 Int,否则为 Nothing。

(^? storage . ix position) :: Example -> Maybe Int

这个部分版本将假定该位置有效,如果无效则崩溃。

(^?! storage . ix position) :: Example -> Int

(%~),将右边的函数应用到左边遍历的所有东西,不仅适用于 Lenses,也适用于所有 Traversals。 (每个镜头都是聪明的 ekmett-trickery 的遍历,并且可以插入遍历可以到达的任何地方。)

storage . ix position %~ (+1) :: Example -> Example

如果您绝对必须使用 Lens,那么如果您尝试在无效位置应用这些部分术语,它们中的任何一个都会崩溃。

singular $ storage . ix position :: Lens' Example Int
storage . singular (ix position) :: Lens' Example Int

PS:您的记录看起来可能需要拉链:如果您只是逐步移动 forward/backward,那么如果您跟踪当前位置左侧的值列表、当前位置的值和当前位置右侧的值列表,而不是所有值的列表和您在其中的位置。如需更多乐趣,请查看 Control.Lens.Zipper,但这些都经过优化以优雅地嵌套多层拉链。

看来,实现这个最简单的方法是使用镜头编写getter和setter,然后组成一个镜头:

at_position :: Functor f => (Int -> f Int) -> Example -> f Example
at_position = lens get set
    where
        get :: Example -> Int
        get e = fromJust $ e ^? storage . ix (e^.position)

        set :: Example -> Int -> Example
        set e v = e & storage . ix (e^.position) .~ v

虽然这可能会有所改进,但代码足够清晰,并且不局限于对象结构。