使用镜头索引遍历

Index a Traversal with a Lens

我有一个 lens 指向一个 json 文档,例如

doc ^? ((key "body").values)

现在我想用键 "key" 索引正文中的值,因为 json 看起来像

{"body": [{"key": 23, "data": [{"foo": 1}, {"foo": 2}]}]}

所以我正在寻找可以让我用另一个镜头索引的东西:

doc ^? key "body" . values
      . indexWith (key "key")
      . key "data" . values
      . key "foo" . withIndex

哪个应该return

[(23, 1), (23, 2)]

MVCE:

#!/usr/bin/env stack
-- stack --resolver lts-11.7 script
-- --package lens
-- --package text
-- --package lens-aeson
{-# LANGUAGE OverloadedStrings #-}
import Control.Lens
import Data.Aeson.Lens
import Data.Text

doc :: Text
doc = "{\"body\": [{\"key\": 23, \"data\": [{\"foo\": 1}, {\"foo\": 2}]}]}"

-- Something akin to Lens -> Traversal -> IndexedTraversal
indexWith :: _
indexWith = undefined

-- should produce [(23, 1), (23, 2)]
indexedBody :: [(Int, Int)]
indexedBody = doc ^? key "body" . values
                   . indexWith (key "key")
                   . key "data" . values
                   . key "foo" . withIndex

main = print indexedBody

新的、令人作呕的完整答案

我终于回到了装有 GHC 的真实计算机上,并做了一些更彻底的测试。我发现两件事:1)我的基本想法有效。 2) 按照您想要的方式使用它有 很多 的微妙之处。

这里有一些扩展定义来开始实验:

{-# Language OverloadedStrings, FlexibleContexts #-}

import Control.Lens
import Data.Aeson
import Data.Aeson.Lens
import Data.Text
import Data.Monoid (First)
import Data.Maybe (isJust, fromJust)

doc :: Text
doc = "{\"body\": [ {\"key\": 23, \"data\": [{\"foo\": 1}, {\"foo\": 2}]}, {\"key\": 29, \"data\": [{\"foo\": 11}, {\"bar\": 12}]} ]}"

doc2 :: Text
doc2 = "{\"body\": [ {\"data\": [{\"foo\": 21}, {\"foo\": 22}]}, {\"key\": 23, \"data\": [{\"foo\": 1}, {\"foo\": 2}]}, {\"key\": 29, \"data\": [{\"foo\": 11}, {\"bar\": 12}]} ]}"

subIndex :: Indexable i p => Getting i s i -> p s fb -> s -> fb
subIndex f = reindexed (view f) selfIndex

subIndex2 :: Indexable (Maybe a) p => Getting (First a) s a -> p s fb -> s -> fb
subIndex2 f = reindexed (preview f) selfIndex

subIndex3 :: (Applicative f, Indexable i p) => Getting (First i) s i -> p s (f s) -> s -> f s
subIndex3 f = reindexed fromJust (subIndex2 f . indices isJust)

我已经定义了 3 个不同的函数变体来执行您想要的操作。第一个 subIndex 正是您在问题标题中要求的。它需要一个镜头,而不是一个遍历。这会阻止它完全按照您想要的方式使用。

> doc ^@.. key "body" . values . subIndex (key "key" . _Integer) <. key "data" . values . key "foo" . _Integer

<interactive>:61:42: error:
    • No instance for (Monoid Integer) arising from a use of ‘key’
    • In the first argument of ‘(.)’, namely ‘key "key"’
      In the first argument of ‘subIndex’, namely
        ‘(key "key" . _Integer)’
      In the first argument of ‘(<.)’, namely
        ‘subIndex (key "key" . _Integer)’

这里的问题是密钥可能实际上并不存在。类型系统携带足够的信息来检测这个问题,并拒绝编译。您可以通过稍作修改来解决它:

> doc ^@.. key "body" . values . subIndex (singular $ key "key" . _Integer) <. key "data" . values . key "foo" . _Integer
[(23,1),(23,2),(29,11)]

但是singular是对编译器的承诺。如果你错了,事情就会出错:

> doc2 ^@.. key "body" . values . subIndex (singular $ key "key" . _Integer) <. key "data" . values . key "foo" . _Integer
[(*** Exception: singular: empty traversal
CallStack (from HasCallStack):
  error, called at src/Control/Lens/Traversal.hs:667:46 in lens-4.16-f58XaBDme4ClErcSwBN5e:Control.Lens.Traversal
  singular, called at <interactive>:63:43 in interactive:Ghci4

所以,我的下一个想法是使用 preview 而不是 view,结果是 subIndex2

> doc ^@.. key "body" . values . subIndex2 (key "key" . _Integer) <. key "data" . values . key "foo" . _Integer
[(Just 23,1),(Just 23,2),(Just 29,11)]

那里有那些 Just 构造函数有点难看,但它有其优点:

> doc2 ^@.. key "body" . values . subIndex2 (key "key" . _Integer) <. key "data" . values . key "foo" . _Integer
[(Nothing,21),(Nothing,22),(Just 23,1),(Just 23,2),(Just 29,11)]

有了这个,即使索引丢失,遍历仍然会命中所有常规目标。这可能是解决方案 space 中的一个有趣点。对于某些用例,它肯定是最佳选择。尽管如此,我认为这并不是您想要的。我想你可能真的想要 Traversal-ish 行为——如果没有索引遍历的目标,就跳过所有 children。不幸的是,镜头在进行这种指数操作时有点简朴。我最终得到了 subIndex3,它使用 map fromJust . filter isJust 模式的 index-level 变体。它是完全安全的,但在重构面前它有点脆弱。

虽然有效:

> doc ^@.. key "body" . values . subIndex3 (key "key" . _Integer) <. key "data" . values . key "foo" . _Integer
[(23,1),(23,2),(29,11)]

而且,当索引遍历找不到任何目标时,它的工作方式可能与您预期的一样:

> doc2 ^@.. key "body" . values . subIndex3 (key "key" . _Integer) <. key "data" . values . key "foo" . _Integer
[(23,1),(23,2),(29,11)]

缺少 "key" 字段的字典将被忽略,即使遍历的其余部分将包含目标。

至此 - 三个相关选项,每个选项都有正面和负面。第三个在实现方面相当粗糙,我怀疑它也不会有最好的性能。但我估计这很可能是你真正想要的。

旧的、不完整的答案

这不是一个完整的答案,因为我身边没有装有 ghc 的计算机 - 我一直在通过在 freenode 上与 lambdabot 聊天来进行测试。

09:34 <me> > let setIndex f = reindexed (view f) selfIndex in Just (1, [3..6]) ^@.. _Just . setIndex _1 <. _2 . traverse
09:34 <lambdabot>  [(1,3),(1,4),(1,5),(1,6)]

我认为这是您正在寻找的基本思路,但我还没有将其准确地应用到您的数据中。我将它应用到一个结构相似的值以证明模式,至少。基本思想是使用 selfIndexreindexed 的组合来创建具有正确折射率值的折射率光学元件。然后,您必须小心使用 (<.) 和类似的运算符,以在各种索引光学元件的组成中保持正确的索引。

最后,我改用 (^@..) 来提取(索引,目标)对列表,而不是使用 withIndex。后者会起作用,但是你需要更加小心如何将各种组合组合在一起。

使用 withIndex 的示例,请注意,它需要覆盖组合运算符的默认关联才能工作:

12:21 <me> > let setIndex f = reindexed (view f) selfIndex in Just (1, [3..6]) ^.. (_Just . setIndex _1 <. _2 . traverse) . withIndex
12:21 <lambdabot>  [(1,3),(1,4),(1,5),(1,6)]

仅仅 Fold——不是完整的 Traversal——就够了吗?

Control.Lens.Reified 提供了一个具有有用实例的 ReifiedFold 新类型。特别是,Applicative 实例执行折叠的笛卡尔积。

我们可以使用该笛卡尔积在一侧获得 "key",在另一侧获得 "data"。像这样:

indexedBody :: Fold Value (Int,Int)
indexedBody =
    let k :: Fold Value Int
        k = key "key"._Integral
        d :: Fold Value Int
        d = key "data".values.key "foo"._Integral
        Fold kd = (,) <$> Fold k <*> Fold d
     in key "body" . values . kd

没有组合爆炸,因为 "key" 部分最多针对一个值。