Unfancy JSON transcoder
texastoland opened this issue ยท 15 comments
I'm struggling to transcode a record with native types and a single foreign Json entry for round trip to localStorage. My naive untested yet solution is:
-- I want to transcode this
newtype Event = Event { id: String, data: Json }
newtype UnsafeJson a = Unsafe Json a
instance encodeJsonUnsafe Json :: EncodeJson (Unsafe Json a) where
encodeJson = unsafeCoerce
instance decodeJsonUnsafe Json :: DecodeJson (Unsafe Json a) where
decodeJson = Left <<< unsafeCoerce- What would be preferable?
- Could instances be provided for some
newtypethat transcode without necessarily doing anythingsmart
? I know I can manuallystringify/parsebut it wouldn't work with clients of the codecs (specifically https://github.com/texastoland/purescript-localstorage/blob/refactor/src/DOM/WebStorage/JSON.purs).
Do you need to use Json at all for this, would Foreign not work equally well, given that encode is free, and decode is just a matter of checking it's the correct shape?
I'm not sure what you mean? It was previously Foreign but I need codec instances.
You can make anything Foreign with toForeign, so the only instance you'd need is IsForeign. You can still take advantage of Generic here too with https://github.com/paf31/purescript-foreign-generic.
Would that allow me to use a Foreign with Encode or Decode constraints? See link? Generic doesn't encode Foreign in either project (paf31/purescript-foreign-generic#12)?
No, you'd probably have to write some IsForeign instances by hand (those that have no Generic representation), but I meant you can make IsForeign instances easily by implementing them with readGeneric. Foreign definitely seems more suitable if you want to be able to store non-Generic values in localstorage, as there is no way of representing them in Json without resorting to unsafe hacks like your suggestion. ๐
Although I guess you'd probably need to rely on AsForeign (purescript/purescript-foreign#42 - I'll see about getting that merged later today perhaps) if you use purescript-foreign-generic as toForeignGeneric won't be the same as toForeign, since it's encoding the generic structure, not just coercing the value.
Wouldn't I need AsForeign a => EncodeJson a and IsForeign a => DecodeJson a instances to be able to encodeJson event from my example (assuming I wrapped eventData)?
Actually you can ignore everything I said. I was under the impression that webstorage dealt with values of javascript (therefore Foreign) types, not just String. ๐ข
So in that case, the encode you wrote above is fine for that particular type, as argonaut uses javascript types as its representation, so assuming all the types involved are Prim types (or newtype'd Prim types) it will work out.
The decode instance is not safe, and will always need to be written by hand, because if you ever load a value that is supposed to be of type T and instead is not, it won't fail here, it'll cause a runtime error in some other random place when a field that doesn't exist is accessed or there's a pattern failure, etc.
Examples of how that could happen:
- A value of type
Tis stored. You change the type and recompile/redeploy. The value is restored, but since the type ofThas changed, it's no longer correct, but there's no way of knowing. - You
store "foo" (someValue :: T)yourestore "foo"expecting a typeU: oops, no decode instance to check the type is as expected, so it's basically an unsafe coerceT -> U, which probably won't work unlessT = Ustructurally.
For the cases where there is a Generic instance for a type, you can rely on generic encode/decode, for the cases there isn't there is no choice but to accept total unsafety, or write a decode instance by hand.
I'd probably provide two versions of the functions in the webstorage API: Generic constrained versions, and those that use EncodeJson / DecodeJson. Without both, you can either only take advantage of Generic by providing Json codecs implemented with gEncodeJson / gDecodeJson, or you can only encode/decode types that are Generic, which as you know, isn't all of them. If I was only going to choose one, I'd use the EncodeJson / DecodeJson versions, as that is more flexible.
I'd probably provide two versions of the functions in the webstorage API
Did it in my last PR ๐ Thanks!
You could omit the Generic constrained versions actually and provide something like this:
newtype GJson a = GJson a
instance encodeJsonGJson :: Generic a => EncodeJson (GJson a) where
encodeJson = gEncodeJson
instance decodeJsonGJson :: Generic a => DecodeJson (GJson a) where
decodeJson = gDecodeJson
So then instead of having mutiple versions of the function, you can direct it with the newtype instead.
In fact, it might be useful to add something like that to argonaut-codecs even...
That's exactly what I did originally (named TranscodeG) but it required extra typing in user code @eskimor. We could eliminate an entire copy pasta file if added to Argonaut. Would there be any ambiguity when a user derives a Generic instance and provides codecs?
What do you mean by needing an extra type? In addition to the GJson wrapper?
And do you mean adding instances like this:
instance encodeJsonGeneric :: Generic a => EncodeJson a
That is a pretty dodgy instance, it means you can't define your own codecs if a type is also generic, as you'd end up with overlapping instances.
I agree it wouldn't be a good idea to add then. Basically I started with a newtype but users would have to do the wrapping/unwrapping for the more prominent use case (Generic). Then I added Generic versions like gGetItem but the key is a phantom type so MyKey (TranscodeG MyItem) still appeared in signatures. I opted to rip it out in eskimor/purescript-localstorage@1e3eb5a in favor of copy pasting the file. I suppose something like contraint kinds would fix that.
Ah I see. I wouldn't consider Generic the common use case, but that's just because we wrote all the codecs by hand in SlamData, but I guess it makes sense.
By the way: #20 ๐
I suppose I meant Don't wanna import extra stuff case
because I agree.
BTW (sorry such a long thread) I agree about UnsafeJson being unsafe. What I really wanted was an easy way to transcode a single Foreign field without bothering with a StrMap. I presume it's possible with foldJsonObject but not straightforward. Partial dupe of #5.