Simple.JSON and Lists (rather than Arrays)

Posted on 2021-09-17

This post concerns the Purescript package Simple.JSON. As far as I can tell, it is one of two main packages for purescript that enable encoding/decoding of JSON data (the other being argonaut).

I’m just going to focus on Simple.JSON here. Here is the simplest way of using the package. Given some type

type Student = { name :: String
               , address :: String
               , id :: Integer
               , email :: String 
               }

and some serialized JSON containing a JSON array of such students in, say, a data file students.json like so:

[ { "name": "Alice",
    "address": "Penny Lane",
    "id": 1,
    "email": "alice@alice.org"
  },
  { "name": "Bob",
    "address": "Desolation Row",
    "id": 2,
    "email": "bob@bob.com"
  }
]

one can write code for deserializing such data as follows:

import Simple.JSON (readJSON)

type E a = Either (NonEmptyList ForeignError) a

getFromFile :: forall a. ReadForeign a => FilePath -> Aff (E a)
getFromFile fp = do
  text <- readTextFile UTF8 fp
  pure $ readJSON text

Now, something like

showStudents :: Aff Unit
showStudents = do
  s::(E (Array Student)) <- getFromFile "students.json"
  case s of
    Left error -> show error
    Right students -> show students

will – without further code – “parse” the JSON data as a purescript Array of Students, and display this array (or display an error should something go awry…)

We might hope to replace the call to getFromFile with something like

  s::(E (List Student)) <- getFromFile "students.json"

in order to parse the data into a purescript List. But this won’t work… As a newcomer to purescript, I was baffled by this at first (and a learned a lot from trying to understand what was happening).

The module Simple.JSON defines a typeclass called ReadForeign, and as you can see in the type signature for get above (or really: the type signature for readJSON), we can use get to parse any type a which has a ReadForeign instance.

Now, the type Student gets – “for free” – a ReadForeign instance, because String and Integer have such instances, as do records (“row types”) of types with ReadForeign instances.

Now, Simple.JSON defines also a ReadForeign instance for Array a whenever a has such an instance; i.e.

instance readArray  :: ReadForeign a => readForeign (Array a) ...

But it does not define one for the data type List.

Now, purescript does not allow orphan instances, so we are actually never going to be able to define a ReadForeign instance for List. But, we can define ReadForeign instance for a newtype for List, and that is what I’m going to do here.

First of all, it is useful to look at the definition of the readArray instance from the Simple.JSON source. Here it is:

instance readArray :: ReadForeign a => ReadForeign (Array a) where
  readImpl = traverseWithIndex readAtIdx <=< readArray
    where
      readAtIdx i f = withExcept (map (ErrorAtIndex i)) (readImpl f)

So if we define a newtype for List:

newtype JList a = JList (List a)

We can try to copy the above code. Now, the Foreign library is being used under-the-hood here, namely the function readArray, which tries to read an array of Foreign (“javascript”) values from its input.

We want to do basically the same thing, and then apply the function Data.List.fromFoldable to produce a list from a Foldable data type (in this case, an Array).

For my first effort, I wasn’t able to write this instance in point-free style, and this is what I came up with:

instance readJList' :: ReadForeign a => ReadForeign (JList a) where
  readImpl f = do
    ar <- traverseWithIndex readAtIdx =<< readArray f
    pure $ JList $ fromFoldable  ar
      where
       readAtIdx :: Int ->
                    Foreign ->
                    ExceptT (NonEmptyList ForeignError) Identity a
       readAtIdx i g = withExcept (map (ErrorAtIndex i)) (readImpl g)

But then –inspired by the above success, and after a little head-scratching – I managed to find a point-free formulation:

instance readJList :: ReadForeign a => ReadForeign (JList a) where
  readImpl = 
    pure <<< JList <<< fromFoldable <=< traverseWithIndex readAtIdx <=< readArray 
      where
       readAtIdx :: Int -> 
                    Foreign ->
                    ExceptT (NonEmptyList ForeignError) Identity a
       readAtIdx i g = withExcept (map (ErrorAtIndex i)) (readImpl g)

For what it is worth <<< is the composition operator, which I guess would be . in Haskell. And <=< is a Kliesli arrow.

Let’s make some modules that permit us to use this instance, as well as some other features of Simple.JSON.

The following uses ideas found in the simple.JSON quickstart for creation of a typeclass UntaggedSumRep which will allow us to read certain JSON data into a sum type.

module Reps
       (class UntaggedSumRep
       , untaggedSumRep)
where

import Foreign (F, Foreign)
import Prelude ((<$>))

import Control.Alt ((<|>))
import Data.Generic.Rep (Argument(..), Constructor(..), Sum(..))
import Simple.JSON (class ReadForeign, readImpl)
  
class UntaggedSumRep rep where
  untaggedSumRep :: Foreign -> F rep

instance untaggedSumRepSum ::
  ( UntaggedSumRep a
  , UntaggedSumRep b
  ) => UntaggedSumRep (Sum a b) where
  untaggedSumRep f
      = Inl <$> untaggedSumRep f
    <|> Inr <$> untaggedSumRep f

instance untaggedSumRepConstructor ::
  ( UntaggedSumRep a
  ) => UntaggedSumRep (Constructor name a) where
  untaggedSumRep f = Constructor <$> untaggedSumRep f

instance untaggedSumRepArgument ::
  ( ReadForeign a
  ) => UntaggedSumRep (Argument a) where
  untaggedSumRep f = Argument <$> readImpl f

We’ll use this below with a sum type

data SorI = String String
          | Int Int

Together with generics, the code above permits us to define a ReadForeign instance for SorI as follows:

derive instance genericSorI :: Generic SorI _ 

instance showSorI :: Show SorI where
  show  = genericShow

instance readForeignImageSource :: ReadForeign SorI where
  readImpl f = to <$> untaggedSumRep f            

Now, here is a module which defines the ReadForeign instance for JList we described earlier.

module JList
       (JList(..))
where

import Prelude

import Control.Monad.Except (ExceptT, withExcept)
import Data.Identity (Identity)
import Data.List (List, fromFoldable)
import Data.List.NonEmpty (NonEmptyList)
import Data.Traversable (class Traversable)
import Data.Foldable (class Foldable)
import Data.TraversableWithIndex (traverseWithIndex)
import Foreign (Foreign, ForeignError(..), readArray)
import Simple.JSON (class ReadForeign, readImpl)

newtype JList a = JList (List a)

derive newtype instance showJList :: Show a => Show (JList a)

derive newtype instance foldableJlist :: Foldable JList

derive newtype instance traverseJlist :: Traversable JList

instance readJList :: ReadForeign a => ReadForeign (JList a) where
  readImpl = pure <<< JList <<< fromFoldable <=< traverseWithIndex readAtIdx <=< readArray
    where
    readAtIdx ::
      Int ->
      Foreign ->
      ExceptT (NonEmptyList ForeignError) Identity a
    readAtIdx i g = withExcept (map (ErrorAtIndex i)) (readImpl g)

And we can use the above modules as imports in this program:

module Main where

import Prelude

import Data.Either (Either(..))
import Data.Foldable (class Foldable, traverse_)
import Data.Generic.Rep (class Generic, to)
import Data.List.NonEmpty (NonEmptyList)
import Data.Show.Generic (genericShow)
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Class.Console (log)
import Foreign (ForeignError)
import JList (JList)
import Node.Encoding (Encoding(..))
import Node.FS.Aff (readTextFile)
import Node.Path (FilePath)
import Reps (untaggedSumRep)
import Simple.JSON (class ReadForeign, readJSON)


data SorI = String String
          | Int Int

derive instance genericSorI :: Generic SorI _ 

instance showSorI :: Show SorI where
  show  = genericShow

instance readForeignSorI :: ReadForeign SorI where
  readImpl f = to <$> untaggedSumRep f            

type E a = Either (NonEmptyList ForeignError) a

getFromFile :: forall a. ReadForeign a => FilePath -> Aff (E a)
getFromFile fp = do
  text <- readTextFile UTF8 fp
  pure $ readJSON text

display :: forall t a. Foldable t => Show a => Show (t a) => E (t a) -> Aff Unit
display x =
  case x of
    Left error -> 
      log $ show error
    Right xx ->  do
      log "\n  - traversal: \n"
      traverse_ (log <<< show)  xx
      log "\n  - all-at-once: \n"
      (log <<< show)  xx
  

main :: Effect Unit
main = do
  launchAff_ do
    result1 :: E (JList (Array SorI)) <- getFromFile "foobar.json"
    log "\n# List of Arrays" 
    display result1
    
    result2 :: E (JList (JList SorI)) <- getFromFile "foobar.json"
    log "\n# List of Lists"
    display result2

    result3 :: E (Array (JList SorI)) <- getFromFile "foobar.json"
    log "\n# Array of Lists"
    display result3

Now if foobar.json has contents as follows:

[
  [
    1,
    2,
    "3",
    4
  ],
  [ "foo",
    "bar",
    78
  ]
 ]

The resulting output displays the contents in various mixes of List and Array’s.

george@valhalla:~$ spago run
spago run
[info] Build succeeded.

# List of Arrays

  - traversal: 

[(Int 1),(Int 2),(String "3"),(Int 4)]
[(String "foo"),(String "bar"),(Int 78)]

  - all-at-once: 

([(Int 1),(Int 2),(String "3"),(Int 4)] : [(String "foo"),(String "bar"),(Int 78)] : Nil)

# List of Lists

  - traversal: 

((Int 1) : (Int 2) : (String "3") : (Int 4) : Nil)
((String "foo") : (String "bar") : (Int 78) : Nil)

  - all-at-once: 

(((Int 1) : (Int 2) : (String "3") : (Int 4) : Nil) : ((String "foo") : (String "bar") : (Int 78) : Nil) : Nil)

# Array of Lists

  - traversal: 

((Int 1) : (Int 2) : (String "3") : (Int 4) : Nil)
((String "foo") : (String "bar") : (Int 78) : Nil)

  - all-at-once: 

[((Int 1) : (Int 2) : (String "3") : (Int 4) : Nil),((String "foo") : (String "bar") : (Int 78) : Nil)]

Compilation finished at Fri Sep 17 17:30:04