При синтаксическом анализе JSON с помощью Aeson почему Maybe обрабатывается по-разному, если он входит в параметр типа? - PullRequest
5 голосов
/ 11 июня 2019

Предположим, у нас есть несколько классов данных

{-# LANGUAGE DeriveGeneric, DuplicateRecordFields #-}

import Data.Aeson
import Data.ByteString.Lazy.Char8
import GHC.Generics

data Foo a = Foo { payload :: a }
    deriving (Show, Generic)

instance ToJSON a => ToJSON (Foo a)
instance FromJSON a => FromJSON (Foo a)

data Bar a = Bar { payload :: Maybe a }
    deriving (Show, Generic)

instance ToJSON a => ToJSON (Bar a)
instance FromJSON a => FromJSON (Bar a)

Затем мы пытаемся расшифровать следующим образом:

*Main > decode $ pack "{}" :: Maybe (Bar String)
Just (Foo {payload = Nothing})
*Main > decode $ pack "{}" :: Maybe (Foo (Maybe String))
Nothing

Так почему же мы не можем декодировать JSON с последней попытки? Классы данных кажутся одинаковыми, и они оба работают одинаково с toJSON:

*Main > toJSON $ Foo (Nothing :: Maybe String)
Object (fromList [("payload",Null)])
*Main > toJSON $ Bar (Nothing :: Maybe String)
Object (fromList [("payload",Null)])

1 Ответ

5 голосов
/ 12 июня 2019

Обновлено: с простым решением внизу.

Это сбивает с толку, но работает более или менее так, как задумано.Вы можете попытаться отправить его как проблему aeson, но я подозреваю, что она будет закрыта как "не будет исправлено".

То, что происходит, заключается в том, что общий экземпляр, сгенерированный для FromJSON (Bar a), эквивалентен:

instance FromJSON a => FromJSON (Bar a) where
  parseJSON = withObject "Bar" $ \v -> Bar
    <$> v .:? "payload"

Обратите внимание на использование оператора (.:?), сгенерированного из-за поля Maybe a в Bar.В структуре со смесью полей Maybe и не Maybe будет соответствующая смесь операторов (.:?) и (.:).

Обратите внимание, что этот экземпляр генерируется раз и навсегдаза все возможные a.Причина его полиморфности в том, что реализация (.:?) может отправлять методу parseJSON в словаре FromJSON a, предоставленном ограничением экземпляра.Также обратите внимание, что единственная причина, по которой мы можем использовать (.:?), заключается в том, что во время компиляции известно, что для всех возможных типов a поле payload в объекте Bar имеет тип Maybe a, поэтому использование *Оператор 1030 * проведет проверку типов.

Теперь рассмотрим экземпляр, сгенерированный для FromJSON (Foo a).Это будет эквивалентно:

instance FromJSON a => FromJSON (Foo a) where
  parseJSON = withObject "Foo" $ \v -> Foo
    <$> v .: "payload"

Это точно аналогично приведенному выше экземпляру Bar a, за исключением того, что используется оператор (.:).Опять же, он имеет единственную реализацию во время компиляции, которая работает для всех возможных a путем отправки в parseJSON в словаре FromJSON a.Нет никакого способа, чтобы этот экземпляр мог использовать оператор (.:?), так как общие a и Maybe t не могут объединяться, и нет никакого способа, которым он может каким-то образом "проверять" тип a, будь то во время компиляции иливремя выполнения, чтобы увидеть, является ли это Maybe, по той же причине, по которой вы не можете написать полную полиморфную функцию с типом a -> a, отличную от идентификатора.

Следовательно, это Foo aэкземпляр не может сделать поле payload необязательным!Вместо этого он должен рассматривать payload как обязательный и - когда используется для анализа Foo (Maybe String) - отправку в экземпляр FromJSON t => FromJSON (Maybe t) (что позволяет null, но в противном случае отправку в экземпляр FromJSON String).

Теперь, почему это выглядит нормально для ToJSON?Итак, экземпляры для ToJSON (Foo a) и ToJSON (Bar a) генерируют один и тот же вид (мономорфного) представления Value:

> toJSON (Foo (Nothing :: Maybe String))
Object (fromList [("payload",Null)])
> toJSON (Bar (Nothing :: Maybe String))
Object (fromList [("payload",Null)])

, и удаление пустых полей происходит равномерно, когда это значение кодируется вJSON.

Это приводит к неудачной асимметрии в экземплярах FromJSON и ToJSON, но это именно то, что происходит.

И я только что понял, что забыл поделиться простым решением для исправленияЭто.Просто определите два общих экземпляра для Foo, один перекрывающийся экземпляр для обработки Maybes, а другой для других типов:

instance {-# OVERLAPPING #-} FromJSON a => FromJSON (Foo (Maybe a))
instance FromJSON a => FromJSON (Foo a)
...