Индекс Обход с помощью объектива
У меня есть объектив, указывающий на документ JSON, например
doc ^? ((key "body").values)
Теперь я хотел бы проиндексировать значения в теле с ключом "ключ", потому что 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 ответа
Новый, отвратительно полный ответ
Я наконец вернулся на настоящий компьютер с 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)]
При этом обход по-прежнему поражает все свои обычные цели, даже если индекс отсутствует. Это потенциально интересная точка в пространстве решений. Есть, конечно, варианты использования, для которых это будет лучшим выбором. Несмотря на это, я подумал, что это не совсем то, что вы хотели. Я подумал, что вы, вероятно, действительно хотели поведение обхода - если нет цели для обхода индекса, просто пропустите все дочерние элементы. К сожалению, линзы немного аскетичны при манипуляциях с индексами. Я в конечном итоге закончил с 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 - я тестировал, общаясь с lambdabot на freenode.
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
создать индексированную оптику с правильным значением индекса. Тогда вы должны быть осторожны с (<.)
и аналогичные операторы для поддержания правильного индекса по составам различной индексированной оптики.
Наконец, я перешел на использование (^@..)
извлечь список (index, target) пар вместо использования 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
newtype с полезными экземплярами. В частности, Applicative
Экземпляр выполняет декартово произведение складок.
Мы можем использовать это декартово произведение, чтобы получить "ключ" с одной стороны и "данные" с другой. Как это:
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
Там нет комбинаторного взрыва, потому что "ключевая" часть нацелена не более чем на одно значение.