使用镜头索引遍历
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)]
我认为这是您正在寻找的基本思路,但我还没有将其准确地应用到您的数据中。我将它应用到一个结构相似的值以证明模式,至少。基本思想是使用 selfIndex
和 reindexed
的组合来创建具有正确折射率值的折射率光学元件。然后,您必须小心使用 (<.)
和类似的运算符,以在各种索引光学元件的组成中保持正确的索引。
最后,我改用 (^@..)
来提取(索引,目标)对列表,而不是使用 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" 部分最多针对一个值。
我有一个 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)]
我认为这是您正在寻找的基本思路,但我还没有将其准确地应用到您的数据中。我将它应用到一个结构相似的值以证明模式,至少。基本思想是使用 selfIndex
和 reindexed
的组合来创建具有正确折射率值的折射率光学元件。然后,您必须小心使用 (<.)
和类似的运算符,以在各种索引光学元件的组成中保持正确的索引。
最后,我改用 (^@..)
来提取(索引,目标)对列表,而不是使用 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" 部分最多针对一个值。