Skip to content

Add some docs #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,198 @@ bower install purescript-argonaut-core
## Documentation

Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-argonaut-core).

## Tutorial

Some of Argonaut's functions might seem a bit arcane at first, so it can help
to understand the underlying design decisions which make it the way it is.

One approach for modelling JSON values would be to define an algebraic data
type, like this:

```purescript
data Json
= JNull
| JString String
| JNumber Number
| JBoolean Boolean
| JArray (Array Json)
| JObject (StrMap Json)
```

And indeed, some might even say this is the obvious approach.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this opening - it sets the mindset the reader should have.

Alas I would name the data constructors differently as there a indeed types with names you are using here. Yes I know its only type synonyms yet it could easily confuse when you look into Pursuit and find exactly those types.
What about JSNull, JSBoolean ... Or even better NullWrapper, BooleanWrapper ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor names do correspond exactly to the real types which have those names, though, so I think it makes sense to use the same names. It does also say that Argonaut does not use this approach in the next paragraph.

Because Argonaut is written with the compilation target of JavaScript in mind,
it takes a slightly different approach, which is to reuse the existing data
types which JavaScript already provides. This way, the result of JavaScript's
`JSON.parse` function is already a `Json` value, and no extra processing is
needed before you can start operating on it. This ought to help your program
both in terms of speed and memory churn.

Much of the design of Argonaut follows naturally from this design decision.

### Types

The most important type in this library is, of course, `Json`, which is the
type of JSON data in its native JavaScript representation.

As the (hypothetical) algebraic data type declaration above indicates, there
are six possibilities for a JSON value: it can be `null`, a string, a number, a
boolean, an array of JSON values, or an object mapping string keys to JSON
values.

For convenience, and to ensure that values have the appropriate underlying
data representations, Argonaut also declares types for each of these individual
possibilities, whose names correspond to the data constructor names above.

Therefore, `JString`, `JNumber`, and `JBoolean` are synonyms for the primitive
PureScript types `String`, `Number`, and `Boolean` respectively; `JArray` is a
synonym for `Array Json`; and `JObject` is a synonym for `StrMap Json`.
Argonaut defines a type `JNull` as the type of the `null` value in JavaScript.

### Introducing Json values

(Or, where do `Json` values come from?)

If your program is receiving JSON data as a string, you probably want the
`parseJson` function in `Data.Argonaut.Parser`, which is a very simple wrapper
around JavaScript's `JSON.parse`.

Otherwise, `Json` values can be introduced into your program via the FFI or via
the construction functions in `Data.Argonaut.Core`. Here are some examples:

```javascript
// In an FFI module.
exports.someNumber = 23.6;
exports.someBoolean = false;
exports.someObject = {people: [{name: "john"}, {name: "jane"}], common_interests: []};
```

```purescript
foreign import someNumber :: Json
foreign import someBoolean :: Json
foreign import someObject :: Json
```

Generally, if a JavaScript value could be returned from a call to `JSON.parse`,
it's fine to import it from the FFI as `Json`. So, for example, objects,
booleans, numbers, strings, and arrays are all fine, but functions are not.

The construction functions (that is, `fromX`, or `jsonX`) can be used as
follows:

```purescript
import Data.Tuple (Tuple(..))
import Data.StrMap as StrMap
import Data.Argonaut.Core as A

someNumber = A.fromNumber 23.6
someBoolean = A.fromBoolean false
someObject = A.fromObject (StrMap.fromFoldable [
Tuple "people" (A.fromArray [
A.jsonSingletonObject "name" (A.fromString "john"),
A.jsonSingletonObject "name" (A.fromString "jane")
]),
Tuple "common_interests" A.jsonEmptyArray
])
```

### Eliminating/matching on `Json` values
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm - the following section I found a bit tricky.

It absolutely helped me (at least I think so) to understand why I would use foldJSONs but its not really clear from the text at the moment.

My suggestion is to restructure it as follows

  • explain the toXmethods and that its usually not enough as you only "convert" very superficially. for example toArray yields an Maybe (Array Json)

  • thats why you will need the foldJsonX functions

  • start with examples the single ones and examples
    fromJsonString = foldJsonString "" \x -> x (btw. I would the more elaborate syntax in examples. For example I had to look up that _ can be used for partial application. That was new to me)
    then show a more useful example of a fold like `isLolString``

  • finally move to the fullblown foldJson

  • and at last I would do complete example that shows how to fold a particular Json to a defined type.

    data Bar = Bar (Array Int)
    data SomeObject = SomeObject {foo:: Int, bar:: Bar}

    defaultSomeObject = SomeObject {foo: 1, Bar []}
    toSomeObject = foldJson defaultSomeObject \x -> ...

    so = toSomeObject someObjectFromFFI

I hope my thoughts are correct and make sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this makes sense. One thing though is that I don't think the toX functions are less powerful than foldJson; if you start with just the toX functions, I suspect that you would be able to define foldJson in terms of them. The toX functions are also no more superficial than foldJson; the argument to foldJson for when the JSON is an array also gives you an Array Json, so it only converts one level at a time too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's best to use idiomatic purescript code in examples where possible, because people will often base their own code around examples, so I think I'd prefer to leave the operator section _ == "lol" as is. If it prompts people to look it up and learn what it is, isn't that a good thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely agree re a bigger example. I'll also try writing this in a sort of reversed order like you suggested and see how it turns out. Thanks!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • concerning approach of defining X in terms of Y. I am not sure if I'd go that path. Lets see.
  • concerning the partial application via underscore ... wouldnt (=="lol") be even more idiomatic?
    but yea leave it in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not suggesting taking that path to explain it here, I'm just using it as an example to show that the toX functions, taken all together, can do anything that the foldJsonX functions or even foldJson can.

(=="lol") would previously have been more idiomatic, but we changed the syntax recently to the underscore version, and now the version without the underscore generates warnings and is being removed in 0.9.0.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh. yeah than that makes sense. can you point me to the discussion about why this has been changed (or will be in the near future)? thx

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and more code examples are always better anyways ;-)

Copy link
Member

@garyb garyb May 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robkuz the discussion was long and tortuous, so the short version: it makes (-1) unambiguous. That now refers to the literal value -1, whereas previously it was treated as an operator section - (_ - 1) is now the sectioned equivalent.

Underscores are used as they are a PureScript idiom for placeholders that can be used in a few places: record accessors, record constructors, record updates, and as of the latest compiler, in case _ of and if _ then _ else _ to generate "partially applied expressions".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a link to it in the compiler release notes from one of the 0.8.x releases, look for something like "operator sections".


We can perform case analysis for `Json` values using the `foldJson` function.
This function is necessary because `Json` is not an algebraic data type. If
`Json` were an algebraic data type, we would not have as much need for this
function, because we could perform pattern matching with a `case ... of`
expression instead.

The type of `foldJson` is:

```purescript
foldJson :: forall a.
(JNull -> a) -> (JBoolean -> a) -> (JNumber -> a) ->
(JString -> a) -> (JArray -> a) -> (JObject -> a) ->
Json -> a
```

That is, `foldJson` takes six functions, which all must return values of some
particular type `a`, together with one `Json` value. `foldJson` itself also
returns a value of the same type `a`.

A use of `foldJson` is very similar to a `case ... of` expression, as it allows
you to handle each of the six possibilities for the `Json` value you passed in.
Thinking of it this way, each of the six function arguments is like one of the
case alternatives. Just like in a `case ... of` expression, the final value
that the whole expression evaluates to comes from evaluating exactly one of the
'alternatives' (functions) that you pass in. In fact, you can tell that this
is the case just by looking at the type signature of `foldJson`, because of a
property called *parametricity* (although a deeper explanation of parametricity
is outside the scope of this tutorial).

For example, imagine we had the following values defined in JavaScript and
imported via the FFI:

```javascript
exports.anotherNumber = 0.0;
exports.anotherArray = [0.0, {foo: 'bar'}, false];
exports.anotherObject = {foo: 1, bar: [2,2]};
```

Then we can match on them in PureScript using `foldJson`:

```purescript
foreign import anotherNumber :: Json
foreign import anotherArray :: Json
foreign import anotherObject :: Json

basicInfo :: Json -> String
basicInfo = foldJson
(const "It was null")
(\b -> "Got a boolean: " <>
if b then "it was true!" else "It was false.")
(\x -> "Got a number: " <> show x)
(\s -> "Got a string, which was " <> Data.String.length s <>
" characters long.")
(\xs -> "Got an array, which had " <> Data.Array.length xs <>
" items.")
(\obj -> "Got an object, which had " <> Data.StrMap.size obj <>
" items.")
```

```purescript
basicInfo anotherNumber -- => "Got a number: 0.0"
basicInfo anotherArray -- => "Got an array, which had 3 items."
basicInfo anotherObject -- => "Got an object, which had 2 items."
```

`foldJson` is the fundamental function for pattern matching on `Json` values;
any kind of pattern matching you might want to do can be done with `foldJson`.

However, `foldJson` is not always comfortable to use, so Argonaut provides a
few other simpler versions for convenience. For example, the `foldJsonX`
functions can be used to match on a specific type. The first argument acts as a
default value, to be used if the `Json` value turned out not to be that type.
For example, we can write a function which tests whether a JSON value is the
string "lol" like this:

```purescript
foldJsonString :: forall a. a -> (JString -> a) -> Json -> a

isJsonLol = foldJsonString false (_ == "lol")
```

If the `Json` value is not a string, the default `false` is used. Otherwise,
we test whether the string is equal to "lol".

The `toX` functions also occupy a similar role: they attempt to convert `Json`
values into a specific type. If the json value you provide is of the right
type, you'll get a `Just` value. Otherwise, you'll get `Nothing`. For example,
we could have written `isJsonLol` like this, too:

```purescript
toString :: Json -> Maybe JString

isJsonLol json =
case toString json of
Just str -> str == "lol"
Nothing -> false
```
41 changes: 40 additions & 1 deletion src/Data/Argonaut/Core.purs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
-- | This module defines a data type and various functions for creating and
-- | manipulating JSON values. The README contains additional documentation
-- | for this module.
module Data.Argonaut.Core
( Json(..)
, JNull(..)
Expand Down Expand Up @@ -50,37 +53,73 @@ import Data.Function

import qualified Data.StrMap as M

-- | A Boolean value inside some JSON data. Note that this type is exactly the
-- | same as the primitive `Boolean` type; this synonym acts only to help
-- | indicate intent.
type JBoolean = Boolean

-- | A Number value inside some JSON data. Note that this type is exactly the
-- | same as the primitive `Number` type; this synonym acts only to help
-- | indicate intent.
type JNumber = Number

-- | A String value inside some JSON data. Note that this type is exactly the
-- | same as the primitive `String` type; this synonym acts only to help
-- | indicate intent.
type JString = String
type JAssoc = Tuple String Json

-- | A JSON array; an array containing `Json` values.
type JArray = Array Json

-- | A JSON object; a JavaScript object containing `Json` values.
type JObject = M.StrMap Json

type JAssoc = Tuple String Json

-- | The type of null values inside JSON data. There is exactly one value of
-- | this type: in JavaScript, it is written `null`. This module exports this
-- | value as `jsonNull`.
foreign import data JNull :: *

-- | The type of JSON data. The underlying representation is the same as what
-- | would be returned from JavaScript's `JSON.stringify` function; that is,
-- | ordinary JavaScript booleans, strings, arrays, objects, etc.
foreign import data Json :: *

-- | Case analysis for `Json` values. See the README for more information.
foldJson :: forall a.
(JNull -> a) -> (JBoolean -> a) -> (JNumber -> a) ->
(JString -> a) -> (JArray -> a) -> (JObject -> a) ->
Json -> a
foldJson a b c d e f json = runFn7 _foldJson a b c d e f json

-- | A simpler version of `foldJson` which accepts a callback for when the
-- | `Json` argument was null, and a default value for all other cases.
foldJsonNull :: forall a. a -> (JNull -> a) -> Json -> a
foldJsonNull d f j = runFn7 _foldJson f (const d) (const d) (const d) (const d) (const d) j

-- | A simpler version of `foldJson` which accepts a callback for when the
-- | `Json` argument was a `Boolean`, and a default value for all other cases.
foldJsonBoolean :: forall a. a -> (JBoolean -> a) -> Json -> a
foldJsonBoolean d f j = runFn7 _foldJson (const d) f (const d) (const d) (const d) (const d) j

-- | A simpler version of `foldJson` which accepts a callback for when the
-- | `Json` argument was a `Number`, and a default value for all other cases.
foldJsonNumber :: forall a. a -> (JNumber -> a) -> Json -> a
foldJsonNumber d f j = runFn7 _foldJson (const d) (const d) f (const d) (const d) (const d) j

-- | A simpler version of `foldJson` which accepts a callback for when the
-- | `Json` argument was a `String`, and a default value for all other cases.
foldJsonString :: forall a. a -> (JString -> a) -> Json -> a
foldJsonString d f j = runFn7 _foldJson (const d) (const d) (const d) f (const d) (const d) j

-- | A simpler version of `foldJson` which accepts a callback for when the
-- | `Json` argument was a `JArray`, and a default value for all other cases.
foldJsonArray :: forall a. a -> (JArray -> a) -> Json -> a
foldJsonArray d f j = runFn7 _foldJson (const d) (const d) (const d) (const d) f (const d) j

-- | A simpler version of `foldJson` which accepts a callback for when the
-- | `Json` argument was a `JObject`, and a default value for all other cases.
foldJsonObject :: forall a. a -> (JObject -> a) -> Json -> a
foldJsonObject d f j = runFn7 _foldJson (const d) (const d) (const d) (const d) (const d) f j

Expand Down