waargonaut
waargonaut copied to clipboard
Encoder mapLikeObj API thoughts
In my opinion the current API for creating a 'map-like' JSON object using the
mapLikeObj
and atKey
functions is awkward and not as straight-forward as I would like.
In the current form, it looks like this:
personEncoder :: Applicative f => Encoder f Person
personEncoder = E.mapLikeObj $ \p ->
E.atKey' "name" E.text (_personName p) .
E.atKey' "age" E.int (_personAge p) .
E.atKey' "address" E.text (_personAddress p) .
E.atKey' "numbers" (E.list E.int) (_personFavouriteLotteryNumbers p)
With the atKey'
functions being composed together to create the final object.
However the available generalisation over f
complicates things a bit when both
the mapLikeObj
function AND the atKey
functions both have general and
Identity
implementations. But neither mapLikeObj
function works when used
with the generalised over f
atKey
function.
Also, the fact that these functions are composed isn't always obvious to newer users of the library.
Also the general type of the atKey
functions can produce errors that are difficult to untangle:
atKey :: (At t, IxValue t ~ Json, Applicative f) => Index t -> Encoder f a -> a -> t -> f t
atKey' :: (At t, IxValue t ~ Json) => Index t -> Encoder' a -> a -> t -> t
Contrast this to the glorious ease of decoding the equivalent object:
personDecoder2 :: Monad f => Decoder f Person
personDecoder2 = Person
<$> D.atKey "name" D.text
<*> D.atKey "age" D.int
<*> D.atKey "address" D.text
<*> D.atKey "numbers" (D.list D.int)
I would like to have something for creating 'map-like' objects that has a more obvious interface as well as being more difficult to use incorrectly.
Maybe something like:
data ObjKV f a = OKV
{ _objkvKey :: Text
, _objkvEncoder :: Encoder f v
, _objkvGetter :: a -> (v ????)
}
mkObj :: (Applicative f, Foldable g) => g (OKV f a) -> a -> Encoder f a
-- Then encoding a map-like object is more obviously:
mapLike :: Applicative f => Encoder f a
mapLike = E.encodeA . mkObj
[ OKV "name" E.text _personName
, OKV "age" E.int _personAge
, OKV "address" E.text _personAddress
, OKV "numbers" (E.list E.int) _personFavouriteLotteryNumbers
]
I'm not sure, I hacked that out as an idea...
Try to keep the suggestions reasonable. Whilst I'm not against burning the
Encoder
structure to the ground in the name of a far superior alternative, I
would expect a sufficiently compelling justification. You need more than
"I/we/this package/that package/bob/susan does it this way and I like it, so you
should do that."
Likewise, nerdsnipes like "Build a better representation in ATS2 with linear types and just use the FFI" are :heart: , but no.
Hacked out a thing...
-- Don't have to care what 'f' is here
newtype ObjKV f a = ObjKV
{ envKV :: a -> JObject WS Json -> f (JObject WS Json)
}
-- 'onObj' requires 'Applicative'
keyVal :: Applicative f => Text -> Encoder f b -> (a -> b) -> ObjKV f a
keyVal k e f = ObjKV $ \a -> onObj k (f a) e
-- 'foldrM' requires 'Monad', used to try to keep the thrashing of 'JObject' values
-- down as only one should need to be threaded through.
mkObj :: (Foldable g, Monad f) => g (ObjKV f a) -> Encoder f a
mkObj kvs = encodeA $ \a -> review _JObj . (, mempty) <$> foldrM (($ a) . envKV) mempty kvs
data Foo = Foo
{ _fooA :: Text
, _fooB :: [Int]
}
-- This seems nice
encFoo :: Monad f => Encoder f Foo
encFoo = mkObj
[ keyVal "a" text _fooA
, keyVal "b" (list int) _fooB
]
-- Everyone else has cool custom operators... :(
encFooBecauseOperatorsAreCool :: Monad f => Encoder f Foo
encFooBecauseOperatorsAreCool = mkObj $
keyVal "a" text _fooA
: keyVal "b" (list int) _fooB
: []