Implementing `ReadForeign` for existing types without having to handle newtype instances everywhere

Hi,

let me show you the following example:

module Main where

import Prelude

import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (log)
import Simple.JSON as JSON
import Data.DateTime (DateTime)
import Data.Tuple.Nested (type (/\), (/\))
import Foreign (ForeignError(..), fail)
import Data.RFC3339String (RFC3339String(..), toDateTime)

newtype JsonTuple a b = JsonTuple (a /\ b)

fromJsonTuple ∷ ∀ a b. JsonTuple a b → a /\ b
fromJsonTuple (JsonTuple x) = x

derive newtype instance jsonTupleShow ∷ (Show a, Show b) ⇒ Show (JsonTuple a b)

instance jsonTupleReadForeign ∷
  ( JSON.ReadForeign a
  , JSON.ReadForeign b
  ) ⇒
  JSON.ReadForeign (JsonTuple a b) where
  readImpl =
    JSON.readImpl
      >=> case _ of
        [ a, b ] → (\x y → JsonTuple $ x /\ y) <$> JSON.readImpl a
                                               <*> JSON.readImpl b
        _ → fail $ TypeMismatch "Expected 2-tuple" "not a 2-tuple"

newtype JsonDateTime = JsonDateTime DateTime

fromJsonDateTime :: JsonDateTime -> DateTime
fromJsonDateTime (JsonDateTime dt) = dt

instance jsonDateTimeShow ∷ Show JsonDateTime where
  show (JsonDateTime x) = show x

instance jsonDateTimeReadForeign ∷
  JSON.ReadForeign JsonDateTime where
  readImpl = JSON.readImpl
    >=> \x → case toDateTime $ RFC3339String x of
      Nothing → fail $ TypeMismatch "Expected RFC3339 String" "Not an RFC3339 String"
      Just dt → pure $ JsonDateTime dt

type Foo' =
  { a :: JsonDateTime
  , b :: JsonTuple Int Number
  }

type Foo =
  { a :: DateTime
  , b :: Int /\ Number
  }

input :: String
input = "{\"a\": \"2019-10-12T07:20:50.52Z\", \"b\": [1, 2.0]}"

readFoo :: String -> Maybe Foo
readFoo s = f <$> JSON.readJSON_ s
  where
    f :: Foo' -> Foo
    f x = { a: fromJsonDateTime x.a, b: fromJsonTuple x.b }

main :: Effect Unit
main = do
  log $ show $ readFoo input

This code implements the ReadForeign instance for two types:

  • Tuple (where the input shall be a json list of two items)
  • DateTime (where the input is an RFC3339 string)

Because orphan instances are forbidden in PureScript, i implemented these instances on newtypes.
To make JSON.readJSON_ work with my type Foo, I need to provide a type Foo' that works with the newtype instances, and then transform that to my real Foo type that i want to use.

Is it somehow possible to omit the last step? What is the right way to do this if i need ReadForeign instances for existing types?

There’s a few options here.

What you’ve done is one option. You can use coerce to make it more ergonomic, e.g. f :: Foo' -> Foo is just coerce and in fact readFoo can be written as coerce (JSON.readJSON_ :: String -> Maybe Foo').

This points to a general principle that you can define a type for encoding/decoding that has the instance you want, and coerce it to your application types. This is acting a bit more like explicit codecs, though (see @garyb’s blogpost Some thoughts on typeclass-based codecs and library purescript-codec-argonaut - Pursuit).

The other main option, that will provide the nicest interface, is making your own ReadForeign class. This is good if you really want to encode tuples as JSON arrays uniformly across your app. Unfortunately you need to copy some boilerplate and existing instances, but then you have something that works just the way you want.

In fact, Justin intended for the library to be used like that, see his note at the end of the README:

How should I actually use this library?

James Brock has informed me that people still do not understand that this library should be used not as a library. If you do not like any of the behavior in this library or would like to opt out of some behaviors, you should copy this library into your own codebase. Please see that this libraries does not actually contain many lines of code and you should be able to learn how to construct this library from scratch with a few days of reading.

I have run out of creativity to think of other ways to structure this atm, but a lot of people have thought about it so maybe they will chime in too.

1 Like

This newtype issue is partly why I wrote json-codecs

2 Likes

I agree with @monoidmusician , the cleanest interface you get if you (re-)define ReadForeign on your side. I don’t want to convince you to use a different Decoder, but just to demonstrate a principle:

In order to define your own class you have to copy/paste quite some code from the library. I’ve published the packages :bookmark: classless-encode-json and :bookmark:classless-decode-json that provide functions that make defining you own encoder and decoder class as boiler plate free as possible. However, they work on the Argonaut Json type.

You can have a look at the test suites. E.g. here is an example that defines a custom MyDecodeJson class. Types like Int and Array are trivial. For generic decoders like “Record” I’m using a pattern that I learned from the heterogeneous package. You have to provide a dummy data type that you pass to the generic record encoder. Then you provide an instance of a library provided type class for that type in which you refer to you own type class. By this the generic function can recur on you own type class method. This may sound complex, the usage of it is quite easy. What you get is a record instance without much copy/paste.
A similar thing exists for the generic ADTs.

Hope that’s useful for someone. Any feedback welcome!

1 Like