From fea201ba1e9bfcd9d1a6fbb8f44052237210cfcd Mon Sep 17 00:00:00 2001 From: Harry Garrood Date: Sun, 1 May 2016 15:26:59 +0100 Subject: [PATCH 1/2] Docs --- README.md | 139 ++++++++++++++++++++++++++++++++++++ src/Data/Argonaut/Core.purs | 41 ++++++++++- 2 files changed, 179 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3c6c57..59b0cc4 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,142 @@ 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 ADT, like this: + +```purescript +data Json + = JNull + | JBoolean Boolean + | JArray (Array Json) + | JObject (StrMap Json) + [...] +``` + +And indeed, some might even say this is the obvious approach. + +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.stringify` 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. + +### Introducing Json values + +(Or, where do `Json` values come from?) + +Usually, a `Json` value will be introduced into your program via either 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"}], +``` + +```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.stringify`, 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") + ]) + ]) +``` + +### Eliminating/matching on `Json` values + +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. + +For example, imagine we had the following values defined in JavaScript and +imported via the FFI: + +```javascript +exports.someNumber = 0.0; +exports.someArray = [0.0, {foo: 'bar'}, false]; +exports.someObject = {foo: 1, bar: [2,2]}; +``` + +Then we can match on them in PureScript using `foldJson`: + +```purescript +foreign import someNumber :: Json +foreign import someArray :: Json +foreign import someObject :: 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 someNumber -- => "Got a number: 0.0" +basicInfo someArray -- => "Got an array, which had 3 items." +basicInfo someObject -- => "Got an object, which had 2 items." +``` + +All the other functions for matching on `Json` values can be expressed in terms +of `foldJson`, but a few others are provided 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 +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. We could have written +`isJsonLol` like this, too: + +```purescript +isJsonLol json = + case toString json of + Just str -> str == "lol" + Nothing -> false +``` diff --git a/src/Data/Argonaut/Core.purs b/src/Data/Argonaut/Core.purs index 91ac0ae..8ace9cc 100644 --- a/src/Data/Argonaut/Core.purs +++ b/src/Data/Argonaut/Core.purs @@ -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(..) @@ -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 From 565c7e650c51c45570663cf1838ec9cfa307a9c7 Mon Sep 17 00:00:00 2001 From: Harry Garrood Date: Tue, 3 May 2016 02:08:29 +0100 Subject: [PATCH 2/2] Improve docs --- README.md | 114 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 59b0cc4..fd962c8 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,17 @@ Module documentation is [published on Pursuit](http://pursuit.purescript.org/pac 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 ADT, like this: +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. @@ -38,25 +40,47 @@ And indeed, some might even say this is the obvious approach. 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.stringify` function is already a `Json` value, and no extra processing is +`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?) -Usually, a `Json` value will be introduced into your program via either the FFI -or via the construction functions in `Data.Argonaut.Core`. Here are some -examples: +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"}], +exports.someObject = {people: [{name: "john"}, {name: "jane"}], common_interests: []}; ``` ```purescript @@ -65,10 +89,9 @@ foreign import someBoolean :: Json foreign import someObject :: Json ``` -Generally, if a JavaScript value could be returned from a call to -`JSON.stringify`, 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. +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: @@ -84,7 +107,8 @@ 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 ]) ``` @@ -96,21 +120,44 @@ This function is necessary because `Json` is not an algebraic data type. If 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.someNumber = 0.0; -exports.someArray = [0.0, {foo: 'bar'}, false]; -exports.someObject = {foo: 1, bar: [2,2]}; +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 someNumber :: Json -foreign import someArray :: Json -foreign import someObject :: Json +foreign import anotherNumber :: Json +foreign import anotherArray :: Json +foreign import anotherObject :: Json basicInfo :: Json -> String basicInfo = foldJson @@ -127,29 +174,38 @@ basicInfo = foldJson ``` ```purescript -basicInfo someNumber -- => "Got a number: 0.0" -basicInfo someArray -- => "Got an array, which had 3 items." -basicInfo someObject -- => "Got an object, which had 2 items." +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." ``` -All the other functions for matching on `Json` values can be expressed in terms -of `foldJson`, but a few others are provided 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: +`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. We could have written -`isJsonLol` like this, too: +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"