使用镜头索引遍历

时间:2018-05-03 13:41:18

标签: haskell lens

我有lens指向json文档,例如

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

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

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

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

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

应返回

[(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

2 个答案:

答案 0 :(得分:3)

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

我终于用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)]

有了这个,即使索引丢失,遍历仍然会击中所有常规目标。这可能是解决方案领域的一个有趣点。肯定有用例,它将是最佳选择。尽管如此,我认为它并不完全是你想要的。我想你可能真的想要Traversal-ish行为 - 如果没有索引遍历的目标,只需跳过所有孩子。不幸的是,镜头对指数的这种操纵有点严峻。我最终得到了subIndex3,它使用了map fromJust . filter isJust模式的索引级变体。它本身非常安全,但在重构方面它有点脆弱。

但它有效:

> 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)]

答案 1 :(得分:2)

仅仅Fold - 不是一个完整的Traversal - 是否足够?

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

我们可以使用该笛卡尔积来获得&#34;键&#34;一方面,&#34;数据&#34;在另一。像这样:

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

没有组合爆炸,因为&#34;键&#34;部分目标最多只有一个值。