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)
= do
getFromFile fp <- readTextFile UTF8 fp
text pure $ readJSON text
Now, something like
showStudents :: Aff Unit
= do
showStudents 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
= traverseWithIndex readAtIdx <=< readArray
readImpl where
= withExcept (map (ErrorAtIndex i)) (readImpl f) readAtIdx i 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
= do
readImpl f <- traverseWithIndex readAtIdx =<< readArray f
ar pure $ JList $ fromFoldable ar
where
readAtIdx :: Int ->
Foreign ->
ExceptT (NonEmptyList ForeignError) Identity a
= withExcept (map (ErrorAtIndex i)) (readImpl g) readAtIdx i 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
= withExcept (map (ErrorAtIndex i)) (readImpl g) readAtIdx i 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
) = Constructor <$> untaggedSumRep f
untaggedSumRep f
instance untaggedSumRepArgument ::
ReadForeign a
( => UntaggedSumRep (Argument a) where
) = Argument <$> readImpl f untaggedSumRep 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:
instance genericSorI :: Generic SorI _
derive
instance showSorI :: Show SorI where
show = genericShow
instance readForeignImageSource :: ReadForeign SorI where
= to <$> untaggedSumRep f readImpl 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)
newtype instance showJList :: Show a => Show (JList a)
derive
newtype instance foldableJlist :: Foldable JList
derive
newtype instance traverseJlist :: Traversable JList
derive
instance readJList :: ReadForeign a => ReadForeign (JList a) where
= pure <<< JList <<< fromFoldable <=< traverseWithIndex readAtIdx <=< readArray
readImpl where
readAtIdx ::
Int ->
Foreign ->
ExceptT (NonEmptyList ForeignError) Identity a
= withExcept (map (ErrorAtIndex i)) (readImpl g) readAtIdx i 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
instance genericSorI :: Generic SorI _
derive
instance showSorI :: Show SorI where
show = genericShow
instance readForeignSorI :: ReadForeign SorI where
= to <$> untaggedSumRep f
readImpl f
type E a = Either (NonEmptyList ForeignError) a
getFromFile :: forall a. ReadForeign a => FilePath -> Aff (E a)
= do
getFromFile fp <- readTextFile UTF8 fp
text 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"
log <<< show) xx
traverse_ (log "\n - all-at-once: \n"
log <<< show) xx
(
main :: Effect Unit
= do
main do
launchAff_ 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