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 textNow, something like
showStudents :: Aff Unit
showStudents = do
s::(E (Array Student)) <- getFromFile "students.json"
case s of
Left error -> show error
Right students -> show studentswill – 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 fWe’ll use this below with a sum type
data SorI = String String
| Int IntTogether 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 result3Now 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