diff --git a/README.md b/README.md index a3c6c57..fd962c8 100644 --- a/README.md +++ b/README.md @@ -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. + +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 + +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 +``` 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