Add Encode.mapping and Decode.mapping?
njlr opened this issue · comments
I was wondering if the library should have built-in support for maps, similar to lists and sequences?
(Decode.map
is already taken of course)
Perhaps...
#r "nuget: Thoth.Json.Net"
open Thoth.Json.Net
module Encode =
let mapping keyEncoder valueEncoder : Encoder<Map<'k, 'v>> =
(fun m ->
m
|> Map.toSeq
|> Seq.map (Encode.tuple2 keyEncoder valueEncoder)
|> Encode.seq)
module Decode =
let mapping keyDecoder valueDecoder : Decoder<Map<'k, 'v>> =
Decode.list (Decode.tuple2 keyDecoder valueDecoder)
|> Decode.andThen
(fun kvps ->
let duplicateKeys =
kvps
|> Seq.map fst
|> Seq.groupBy id
|> Seq.filter (fun (_, vs) -> Seq.length vs > 1)
|> Seq.map fst
if Seq.isEmpty duplicateKeys then
Decode.succeed (Map.ofSeq kvps)
else
let duplicateKeysList =
duplicateKeys
|> Seq.map string
|> String.concat ", "
Decode.fail $"Found duplicate key(s): {duplicateKeysList}")
// Usage...
let m =
Map.ofSeq
[
123, "abc"
456, "def"
]
let json =
m
|> Encode.mapping Encode.int Encode.string
|> Encode.toString 2
printfn "%A" json
let decoded =
json
|> Decode.unsafeFromString (Decode.mapping Decode.int Decode.string)
printfn "%A" decoded
There the dict
decoder/encoder which support Map<string, 'T>
.
Lines 690 to 691 in a1fe5bb
I am unsure why I didn't make the key generics at the time too.
I see two potential reason:
dict
was implemented like that in the Elm library and I didn't think more about it- Could it be that we can't support any type of keys in the JSON representation? For example, can we work with a key represented as a Record? I am taking the record as an example because I think this is probably the most extreme case possible.
Depending on the answer, there are 2 solutions:
- Make a breaking change and make
dict
accept generics for both the keys and the values - Provide a new decoder (new name) as you proposed
Ah, I think I skipped over dict
assuming it was for IDictionary
. I think the ability to have non-string keys is quite important; this is a key advantage of Fable over JavaScript for me.
Happy to send a PR with some direction 👍
Ah, I think I skipped over dict assuming it was for IDictionary.
Yes, you are not the first one. That's the problem when you have a type Map
and the function map
to support. 😅
Happy to send a PR with some direction 👍
The first thing is to decide how we are going to represent the type as JSON. I think you proposed to use an array representation with the first value being the key and the second the value. I think this is indeed the only way we have to do it.
Then for the actual code changes:
Rewrite keyValuesPairs
to accept a keyDecoder
and valueDecoder
.
Lines 496 to 508 in a1fe5bb
This is because it allows to have a really simple implementation for the dict
decoder.
Lines 690 to 691 in a1fe5bb
Which would also need to be changed to accept a keyDecoder
and valueDecoder
.
There is also the Auto
API to cover:
Lines 1153 to 1159 in a1fe5bb
I think the only changes required is to convert typedefof< Map<string, obj> >.FullName
to typedefof< Map<obj, obj> >.FullName
because the keyDecoder
is already auto generating its type.
The same things needs to be done for the encoders and the tests needs to be adapt :). Then we need to port the change to Thoth.Json.Net
but in general this is just a matter of copy/pasting the code.
Because, we are going to make a breaking change to the API by adding an arguments to dict
, I would like to take this opportunity to change dict
to returns the F# dictionary type (is it IDictionary
? Is it supported by Fable?).
And then find a good name for the Map
type.
Some proposition I have are:
Decode.Map
, which a capital letter to not conflict withmap
--> too subtle ?Decode.asMap
-> Doesn't follow the same convention as the other decoderDecode.toMap
-> Doesn't follow the same convention as the other decoderDecode.mapping
looks a bit strange to meDecode.map'
not perfect but "ok" ?
Personally, my preference goes to Decode.map'
Decode.keyValueListMap
is verbose, but descriptive. It also gives a clue as to how the map will be encoded as a JSON array.
If breaking changes are on the table, another option might be:
Decode.map
Function to create aDecoder<Map<'k, 'v>>
Decoder.map
Function to map the result of a successful decoding (currentlyDecode.map
)
I like Decoder.map
because Decoder
is a module of functions over decoders, which mirrors List.map
, Seq.map
, etc.
Although this could be extremely confusing for existing users!
It is also a further departure from Elm.
Decode.keyValueListMap
is verbose, but descriptive. It also gives a clue as to how the map will be encoded as a JSON array.
Indeed, it is verbose but if I was to go this way I would probably use Decode.keyValueListAsMap
or Decode.keyValueListToMap
.
I like
Decoder.map
because Decoder is a module of functions over decoders, which mirrorsList.map
,Seq.map
, etc.
For now, I think this is too much of a breaking change I think.
I still have the hope to rewrite Thoth.Json from scratch in the future and perhaps it will be a better time to have such a big breaking changes.
It is also a further departure from Elm.
This is not something holding me personally. I mean we already have some specific syntax thanks to classes and reflection support (even if I still think reflection support is an error ^^)
PR for the most minimal version of this feature: #136
Decode.dict
is unchangedDecode.map'
is addedDecode.Auto
supports maps with non-string keys- A few tests
Will be included in the next release