LINUX.ORG.RU

Maybe значения в toJSON

 


1

2

Допустим, есть тип данных с опциональным полем:

data Test = Test Int (Maybe String)
  deriving (Eq, Read, Show)
Инстанс FromJSON'а вроде как очевиден:
instance FromJSON Test where
  parseJSON (Object o) = do
    n ← o .:  "int"
    s ← o .:? "str"
    return $ Test n s
  parseJSON  _         = empty
С инстансом ToJSON очевидности меньше, так как опционального аналога `.=` нет.
instance ToJSON Test where
  toJSON (Test n s) = object
    [ "int" .= n
    , "str" .= s
    ]
Жсонируем тестовое значение:
> encode $ Test 1 Nothing
Chunk "{\"str\":null,\"int\":1}" Empty
it :: BSC.ByteString
Куда нажать, чтобы получить:
> encode $ Test 1 Nothing
Chunk "{\"int\":1}" Empty
it :: BSC.ByteString
?

Ну и да, ответы вида

instance ToJSON Test where
  toJSON (Test n (Just s)) = object
    [ "int" .= n
    , "str" .= s
    ]
  toJSON (Test n Nothing) = object
    [ "int" .= n
    ]
сразу мимо.

С инстансом ToJSON очевидности меньше, так как опционального аналога `.=` нет.

А если б был, то чем бы он тебе помог в списке?

Если он тебе так нужен, напиши сам что-то вроде

x .=? Nothing   = []
x .=? (Just y)  = [x .= y]

instance ToJSON Test where
  toJSON (Test n s) = object
    [ "int" .= n ] ++ ("str" .=? s)
korvin_ ★★★★★
()

Куда нажать

Сюда, потом

diff --git a/Data/Aeson.hs b/Data/Aeson.hs
index d3db4c4..c288b93 100644
--- a/Data/Aeson.hs
+++ b/Data/Aeson.hs
@@ -30,6 +30,7 @@ module Data.Aeson
     , ToJSON(..)
     -- * Constructors and accessors
     , (.=)
+    , (.=?)
     , (.:)
     , (.:?)
     , (.!=)
diff --git a/Data/Aeson/Types.hs b/Data/Aeson/Types.hs
index 178a69f..1a1b502 100644
--- a/Data/Aeson/Types.hs
+++ b/Data/Aeson/Types.hs
@@ -34,6 +34,7 @@ module Data.Aeson.Types
     , ToJSON(..)
     -- * Constructors and accessors
     , (.=)
+    , (.=?)
     , (.:)
     , (.:?)
     , (.!=)
diff --git a/Data/Aeson/Types/Class.hs b/Data/Aeson/Types/Class.hs
index 8ae9704..6a3e335 100644
--- a/Data/Aeson/Types/Class.hs
+++ b/Data/Aeson/Types/Class.hs
@@ -1,6 +1,6 @@
 {-# LANGUAGE CPP, DeriveDataTypeable, FlexibleContexts, FlexibleInstances,
     GeneralizedNewtypeDeriving, IncoherentInstances, OverlappingInstances,
-    OverloadedStrings, UndecidableInstances, ViewPatterns #-}
+    OverloadedStrings, UndecidableInstances, ViewPatterns, TupleSections #-}
 
 #ifdef GENERICS
 {-# LANGUAGE DefaultSignatures #-}
@@ -36,6 +36,7 @@ module Data.Aeson.Types.Class
     , (.:?)
     , (.!=)
     , (.=)
+    , (.=?)
     , typeMismatch
     ) where
 
@@ -713,9 +714,14 @@ instance FromJSON a => FromJSON (Last a) where
 
 -- | Construct a 'Pair' from a key and a value.
 (.=) :: ToJSON a => Text -> a -> Pair
-name .= value = (name, toJSON value)
+(.=) name = Just . (name,) . toJSON
 {-# INLINE (.=) #-}
 
+-- | U know engrish, right?
+(.=?) :: ToJSON a => Text -> Maybe a -> Pair
+(.=?) name = fmap ((name,) . toJSON)
+{-# INLINE (.=?) #-}
+
 -- | Convert a value from JSON, failing if the types do not match.
 fromJSON :: (FromJSON a) => Value -> Result a
 fromJSON = parse parseJSON
diff --git a/Data/Aeson/Types/Internal.hs b/Data/Aeson/Types/Internal.hs
index 974e1b5..cfadba6 100644
--- a/Data/Aeson/Types/Internal.hs
+++ b/Data/Aeson/Types/Internal.hs
@@ -43,6 +43,7 @@ import Data.Typeable (Typeable)
 import Data.Vector (Vector)
 import qualified Data.HashMap.Strict as H
 import qualified Data.Vector as V
+import Data.Maybe
 
 -- | The result of running a 'Parser'.
 data Result a = Error String
@@ -213,11 +214,11 @@ parseEither :: (a -> Parser b) -> a -> Either String b
 parseEither m v = runParser (m v) Left Right
 
 -- | A key\/value pair for an 'Object'.
-type Pair = (Text, Value)
+type Pair = Maybe (Text, Value)
 
 {-# INLINE parseEither #-}
 -- | Create a 'Value' from a list of name\/value 'Pair's.  If duplicate
 -- keys arise, earlier keys and their associated values win.
 object :: [Pair] -> Value
-object = Object . H.fromList
+object = Object . H.fromList . catMaybes
 {-# INLINE object #-}

Использовать так:

instance ToJSON Test where
  toJSON (Test n s) = object
    [ "int" .= n
    , "str" .=? s
    ]
quasimoto ★★★★
()
Ответ на: комментарий от dmitry_malikov

да туплю, но всё равно как-то печально это, тем более тогда для .=? надо бы сделать инстансы для всего, что может делать fail.

(..=)  = (Just . ) . (.=)
(..=?) = ...

object $ catMaybies [...]
qnikst ★★★★★
()
Последнее исправление: qnikst (всего исправлений: 1)
Ответ на: комментарий от qnikst

Pair - сумма нормальный пары и Nothing, это нужно, чтобы получить список из нормальных пар и Nothing и потом отсеять Nothing в object, сами нормальные пары получаются из (.=) и (.=?) когда второй аргумент последнего - Just, Nothing получается когда второй аргумент (.=?) - Nothing.

надо бы сделать инстансы для всего, что может делать fail.

Сначала надо бы в Prelude вытащить класс Failable из Monad.

quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

Pair - сумма нормальный пары и Nothing, это нужно, чтобы получить список из нормальных пар и Nothing и потом отсеять Nothing в object, сами нормальные пары получаются из (.=) и (.=?) когда второй аргумент последнего - Just, Nothing получается когда второй аргумент (.=?) - Nothing.

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

Сначала надо бы в Prelude вытащить класс Failable из Monad.

обычно 100500 инстансов для каждого типа решали эту проблему :)

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

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

Нормально? Или можно сделать лучше? Я как-то не придумал ничего что бы не сделало патч больше.

обычно 100500 инстансов для каждого типа решали эту проблему

fail ещё связан с do-нотацией, поэтому он в Monad. Другие вещи это, видимо, mempty, mzero и всё что можно использовать как FirstNullaryConstructorOfType. Только я не понял как тут можно это применить - Maybe тут нужен чисто технически, пользователь будет пользоваться теми инфиксными конструкторами и ему должно быть всё равно как оно там внутри работает.

quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

Нормально? Или можно сделать лучше? Я как-то не придумал ничего что бы не сделало патч больше.

чисто формально, всё хорошо и элегантно. Что лично мне не нравится: то, что в Pair добавилось значение, которое не используется нигде, кроме функции object. Единственное значение, которое оно несёт это данное значение не должно быть добавлено в объект, с этой точки зрения мне больше нравится подход

class toJSON a where ...
  (.=?)  :: Text -> a -> Maybe Pair
  name .=? value = Just (name, value)

instance (toJSON a) => toJSON (Just a) where ...
  name .=? Nothing = Nothing
  name .=? Just v    = Just (name .= v)

objectV :: [Just Pair] -> Object
objectV = object . catMaybies

+ это может быть лучше по причинам производительности (хотя тут лучше Сноймана спрашивать)

qnikst ★★★★★
()
Ответ на: комментарий от quasimoto

fail ещё связан с do-нотацией, поэтому он в Monad. Другие вещи это, видимо, mempty, mzero и всё что можно использовать как FirstNullaryConstructorOfType. Только я не понял как тут можно это применить - Maybe тут нужен чисто технически, пользователь будет пользоваться теми инфиксными конструкторами и ему должно быть всё равно как оно там внутри работает.

тут я некорректно сказал, но хорошему мне кажется, что .=? должно работать с разными типами данных, которые могут иметь значение, которое нужно не записывать в объект. (может тут я и не прав и Maybe покрывает всё множество таких объектов)

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

Да, уже понял. Если у нас есть

data Foo
  = Foo1 ...
  | Foo2 ...
  ...
  | FooN

и мы хотим часть конструкторов писать, часть - не писать.

Проще всего сделать

class MaybeLike f a | f -> a where toMaybe :: f -> Maybe a
instance MaybeLike (Maybe a) a where toMaybe = id

(.=?) ::  (ToJSON a, MaybeLike f a) => Text -> f -> Pair
(.=?) name = fmap ((name,) . toJSON) . toMaybe

(MultiParamTypeClasses + FunctionalDependencies - иначе придётся ограничится только (* -> *) типами) но в aeson уже принят Maybe для одного опционального конструктора, то есть такую фичу нужно уже вторым патчем добавлять - для всех опциональных конструкторов.

quasimoto ★★★★
()
Ответ на: комментарий от qnikst

Так не получится использовать (.=) и (.=?) в одном и том же списке - либо первый с object, либо второй с objectV, а нужны именно оба вперемежку.

quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

не получится, поэтому я предлагал сделать вариантом по умолчанию для .=? (Just .) . (.=), тогда в случае если нужно использовать объекты, которые не попадают в объект можно везде использовать .=?.

самое интересное, что у Maybe уже есть инстанс ToJSON и по каким-то причинам его реализация отличается от желаемой автором. В общем-то тут основная проблема в выборе подхода, в текущем варианте, все значения пишутся, Nothing -> null, если нужно не добавлять значение, то нужно не писать его в массив. Вариант вполне логичный и оправданный, учитывая, что в JSON null != undefined.

в варианте желаемом ТС Nothing -> Null, Data Int Nothing -> Object Int, т.е. исключение из общих правил. Вариант тоже логичный, т.к. это исключение, то и решение не обязано быть общим.

предложенный тобой вариант тоже не содержит исключений, но содержит оверхед имеющий смысл только на этапе сборки объекта.

оператор .=? это нечто, что делает возможным добавлять Undefined элементы в массив, которые потом естественным образом фильтруются. Как делать undefined или суммой Just (JSON значение) + Nothing, или ещё одним вариантом Value, и вносить ли их в тип Pair является открытым и не очевидным вопросом.

имхо идеальный вариант будет создать issue на github, и дав туда линк на возможные решения.

qnikst ★★★★★
()
Последнее исправление: qnikst (всего исправлений: 1)
Ответ на: комментарий от qnikst

самое интересное, что у Maybe уже есть инстанс ToJSON и по каким-то причинам его реализация отличается от желаемой автором

Я это тоже видел, но решил что «так надо», то есть _ .= Nothing должен писать null, и чтобы не писать вообще ничего нужно определить новый .=?.

Другое дело, что сейчас API такое, что эти комбинаторы ничего не знают про список в который возвращают значения, но возвращают значения в один список, поэтому должны иметь совпадающие типы. Проще всего заставить их возвращать значения с флагом писать-не-писать, то есть обвернуть в Maybe - будет тайптег в Pair, его проверка в рантайме, лишний проход по списку с помощью catMaybes.

quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

Другое дело, что сейчас API такое, что эти комбинаторы ничего не знают про список в который возвращают значения, но возвращают значения в один список, поэтому должны иметь совпадающие типы. Проще всего заставить их возвращать значения с флагом писать-не-писать, то есть обвернуть в Maybe - будет тайптег в Pair, его проверка в рантайме, лишний проход по списку с помощью catMaybes.

да это самое простое. и возможно и правильное решение. повторюсь, что меня напрягает, что эта информация нужна только на этапе создания объекта, а дальше, является формально лишней.

qnikst ★★★★★
()
Ответ на: комментарий от qnikst

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

По крайней мере, она не выживает после object, а Pair вообще определён в Internals (хотя и используется извне). Бенчмарков по составлению джсона я там не нашёл, так что трудно судить как это скажется на производительности, но issue открыл.

quasimoto ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.