waargonaut icon indicating copy to clipboard operation
waargonaut copied to clipboard

Encoder mapLikeObj API thoughts

Open mankyKitty opened this issue 6 years ago • 1 comments

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.

mankyKitty avatar Feb 03 '19 00:02 mankyKitty

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
  : []

mankyKitty avatar Feb 07 '19 04:02 mankyKitty