В чем разница между функциями декодирования и декодирования из пакета aeson?

Функции decode и decode' от aeson пакет почти идентичен. Но они имеют тонкую разницу, описанную в документации (размещая только интересную часть документов здесь):

-- This function parses immediately, but defers conversion.  See
-- 'json' for details.
decode :: (FromJSON a) => L.ByteString -> Maybe a
decode = decodeWith jsonEOF fromJSON

-- This function parses and performs conversion immediately.  See
-- 'json'' for details.
decode' :: (FromJSON a) => L.ByteString -> Maybe a
decode' = decodeWith jsonEOF' fromJSON

Я попытался прочитать описание json и json', но до сих пор не понимают, какой из них и когда я должен использовать, потому что документация недостаточно ясна. Может ли кто-нибудь более точно описать разницу между двумя функциями и дать пример с объяснением поведения, если это возможно?

UPDATE:

Есть также decodeStrict и decodeStrict' функций. Я не спрашиваю, в чем разница между decode' и decodeStrict, например, который, кстати, тоже интересный вопрос. Но то, что лениво и что здесь строго во всех этих функциях, вовсе не очевидно.

Ответ 1

Разница между этими двумя является тонкой. Есть разница, но это немного сложно. Мы можем начать с изучения типов.

Тип Value

Важно отметить, что тип Value, который предоставляет aeson, был строгим в течение очень долгого времени (в частности, начиная с версии 0.4. 0.0). Это означает, что между конструктором Value и его внутренним представлением не должно быть никаких трюков. Это немедленно означает, что Bool (и, конечно, Null) должен быть полностью оценен, как только a Value оценивается WHNF.

Затем рассмотрим String и Number. Конструктор String содержит значение типа строгое Text, поэтому там может быть любая лень. Аналогично, конструктор Number содержит значение Scientific, которое внутренне представлено двумя строгими значениями. И String, и Number также должны быть полностью оценены после того, как a Value оценивается как WHNF.

Теперь мы можем обратить внимание на Object и Array, единственные нетривиальные типы данных, которые предоставляет JSON. Это более интересно. Object представлены в aeson ленивым HashMap. Lazy HashMap оценивают только свои ключи от WHNF, а не их значения, поэтому значения могут очень сильно отличаться от неоцененных thunks. Аналогично, Array являются Vector s, которые также не являются строгими по своим значениям. Оба эти типа Value могут содержать thunks.

Учитывая это, мы знаем, что, когда мы имеем Value, единственные места, в которых decode и decode' могут отличаться, возникают при создании объектов и массивов.

Наблюдательные различия

Следующее, что мы можем попробовать, - это оценить некоторые вещи в GHCi и посмотреть, что произойдет. Хорошо начните с кучи импорта и определений:

:seti -XOverloadedStrings

import Control.Exception
import Control.Monad
import Data.Aeson
import Data.ByteString.Lazy (ByteString)
import Data.List (foldl')
import qualified Data.HashMap.Lazy as M
import qualified Data.Vector as V

:{
forceSpine :: [a] -> IO ()
forceSpine = evaluate . foldl' const ()
:}

Далее, давайте фактически проанализировать некоторые JSON:

let jsonDocument = "{ \"value\": [1, { \"value\": [2, 3] }] }" :: ByteString

let !parsed = decode jsonDocument :: Maybe Value
let !parsed' = decode' jsonDocument :: Maybe Value
force parsed
force parsed'

Теперь у нас есть две привязки, parsed и parsed', одна из которых анализируется с помощью decode, а другая с decode'. Они вынуждены использовать WHNF, чтобы мы могли хотя бы увидеть, что они есть, но мы можем использовать команду :sprint в GHCi, чтобы узнать, сколько из каждого значения действительно оценивается:

ghci> :sprint parsed
parsed = Just _
ghci> :sprint parsed'
parsed' = Just
            (Object
               (unordered-containers-0.2.8.0:Data.HashMap.Base.Leaf
                  15939318180211476069 (Data.Text.Internal.Text _ 0 5)
                  (Array (Data.Vector.Vector 0 2 _))))

Вы бы на это посмотрели! Версия, обработанная с помощью decode, по-прежнему не оценивается, но у одного, прошедшего синтаксический анализ с decode', есть некоторые данные. Это приводит нас к нашему первому значащему различию между ними: decode' заставляет его немедленный результат WHNF, но decode откладывает его до тех пор, пока он не понадобится.

Давайте рассмотрим эти значения, чтобы увидеть, не находят ли мы больше различий. Что происходит, когда мы оцениваем эти внешние объекты?

let (Just outerObjValue) = parsed
let (Just outerObjValue') = parsed'
force outerObjValue
force outerObjValue'

ghci> :sprint outerObjValue
outerObjValue = Object
                  (unordered-containers-0.2.8.0:Data.HashMap.Base.Leaf
                     15939318180211476069 (Data.Text.Internal.Text _ 0 5)
                     (Array (Data.Vector.Vector 0 2 _)))

ghci> :sprint outerObjValue'
outerObjValue' = Object
                   (unordered-containers-0.2.8.0:Data.HashMap.Base.Leaf
                      15939318180211476069 (Data.Text.Internal.Text _ 0 5)
                      (Array (Data.Vector.Vector 0 2 _)))

Это довольно очевидно. Мы явно вынудили оба объекта, поэтому теперь они оцениваются как хэш-карты. Реальный вопрос заключается в том, оцениваются ли их элементы.

let (Array outerArr) = outerObj M.! "value"
let (Array outerArr') = outerObj' M.! "value"
let outerArrLst = V.toList outerArr
let outerArrLst' = V.toList outerArr'

forceSpine outerArrLst
forceSpine outerArrLst'

ghci> :sprint outerArrLst
outerArrLst = [_,_]

ghci> :sprint outerArrLst'
outerArrLst' = [Number (Data.Scientific.Scientific 1 0),
                Object
                  (unordered-containers-0.2.8.0:Data.HashMap.Base.Leaf
                     15939318180211476069 (Data.Text.Internal.Text _ 0 5)
                     (Array (Data.Vector.Vector 0 2 _)))]

Еще одно отличие! Для массива, декодированного с помощью decode, значения не принудительно, но декодируются с помощью decode'. Как вы можете видеть, это означает, что decode фактически не выполняет преобразование значений Haskell до тех пор, пока они не понадобятся, что означает документация, когда говорится, что он "отменяет преобразование".

Влияние

Ясно, что эти две функции несколько отличаются, и, очевидно, decode' является более строгим, чем decode. Какая же значимая разница? Когда вы предпочитаете друг друга?

Хорошо, стоит упомянуть, что decode никогда не работает больше, чем decode', поэтому decode, вероятно, является правильным дефолтом. Конечно, decode' никогда не будет делать значительно больше работы, чем decode, так как весь документ JSON необходимо проанализировать до того, как будет создано какое-либо значение. Единственное существенное отличие состоит в том, что decode избегает выделения Value, если фактически используется только небольшая часть документа JSON.

Конечно, лень также не является бесплатным. Быть ленивым означает добавление гроз, которое может стоить пространства и времени. Если все тонкости будут оцениваться, так или иначе, то decode просто растрачивает память и время выполнения, добавляя бесполезную косвенность.

В этом смысле ситуации, когда вы, возможно, захотите использовать decode', - это ситуации, в которых вся структура Value будет вынуждена, во всяком случае, которая, вероятно, зависит от того, какой экземпляр вы используете FromJSON. В общем, я бы не стал беспокоиться о выборе между ними, если производительность не имеет особого значения, и вы расшифровываете много JSON или делаете JSON-декодирование в плотном цикле. В любом случае, вы должны ориентироваться. Выбор между decode и decode' - это очень конкретная ручная оптимизация, и я не был бы уверен в том, что либо на самом деле улучшит характеристики во время выполнения моей программы без тестов.

Ответ 2

Haskell - ленивый язык. Когда вы вызываете функцию, она фактически не выполняется правильно, но вместо этого информация о вызове "запоминается" и возвращается в стек (эта информация о запомненных вызовах упоминается как "thunk" в документах), а фактический вызов происходит только в том случае, если кто-то из стека действительно работает, чтобы что-то сделать с возвращенным значением.

Это поведение по умолчанию, и вот как работают json и decode. Но есть способ "обмануть" лень и сказать компилятору выполнить код и оценить значения прямо тогда и там. И это то, что делают json' и decode'.

Компромисс там очевиден: decode экономит время вычисления, если вы никогда ничего не делаете со значением, а decode' сохраняет необходимость "запоминать" информацию о вызове ( "thunk" ) за счет выполняя все на своем месте.