From caf69a334e8acfa963f753a263a2fd37926da6f5 Mon Sep 17 00:00:00 2001 From: Jeremie Gillet <jie.gillet@gmail.com> Date: Fri, 13 Dec 2024 09:19:05 +0900 Subject: [PATCH] make and generate introduction --- concepts/json/introduction.md | 447 +++++++++++++++++- .../concept/githup-api/.docs/introduction.md | 446 +++++++++++++++++ .../monster-attack/.docs/introduction.md | 2 +- 3 files changed, 887 insertions(+), 8 deletions(-) create mode 100644 exercises/concept/githup-api/.docs/introduction.md diff --git a/concepts/json/introduction.md b/concepts/json/introduction.md index 9ebd32ab..f72fdf2f 100644 --- a/concepts/json/introduction.md +++ b/concepts/json/introduction.md @@ -1,11 +1,444 @@ # Introduction -TODO +## JSON -The plan is: +JSON (JavaScript Object Notation) is a human readable data and file format commonly used to exchange data, in particular for web applications. +As such, it is no surprise that is holds a special place in the Elm language. -- get about.md reviewed -- copy about.md to introduction.md -- remove all links -- remove the "What else?" section -- remove other things suggested during review if any +Elm provides the core modules `Json.Decode` and `Json.Encode` to parse and write JSON data. +JSON Decoders are a way to _declare_ what type of data is expected and which data structure the data should map to. + +Let's import some aliases and types from the modules: + +```elm +import Json.Decode as Decode exposing (Decoder, Error) +import Json.Encode as Encode exposing (Value) +``` + +Using `Decode.decodeString : Decoder a -> String -> Result Error a` with a decoder and a JSON string will either succeed if the string confirms to the decoder specifications, or fail otherwise. +There is no way of inspecting the raw JSON data when defining the decoder, which could feel unusual coming from imperative languages. + +## Basic data type decoders + +JSON has a small number of core data types: + +- String: encoded with double quotes `"hello world"` +- Boolean: either `true` or `false` +- Number: such as `34` or `0.12` +- Null: the absence of data `null` + +Each of these is associated with a `Decode` function: + +```elm +Decode.decodeString Decode.string """\"hello\"""" + --> Ok "hello" + +Decode.decodeString Decode.string """null""" + --> Err ... + +Decode.decodeString Decode.bool """true""" + --> Ok True + +Decode.decodeString Decode.bool """0""" + --> Err ... + +Decode.decodeString Decode.int """12""" + --> Ok 12 + +Decode.decodeString Decode.float """3.14""" + --> Ok 3.14 + +Decode.decodeString (Decode.null AnyValue) """null""" + --> Ok AnyValue + +Decode.decodeString (Decode.null AnyValue) """true""" + --> Err ... +``` + +Note that `Decode.null` lets you decide how to model a `null` in your program by requiring an arbitrary value as argument (maybe `()`, `Nothing`, or anything appropriate to your program). + +## Combining decoders + +JSON also defines two data structures to collect core data types: + +- Array: a collection of other data types `[0, null, "none", []]` +- Object: a collection of key-value pairs `{"id": 1, "is_admin": false, "more_info": {}}` + +`Decode` also offers functions to decode these: + +```elm +Decode.decodeString (Decode.list Decode.int) """[1, 2, 3]""" + --> Ok [1, 2, 3] + +Decode.decodeString (Decode.list Decode.int) """[1, 2, 3, "not an int"]""" + --> Err ... + +Decode.decodeString (Decode.dict Decode.int) """{ "key1": 17, "key2": 71 }""" + --> Ok (Dict.fromList [("key1", 17), ("key2", 71)]) + +Decode.decodeString (Decode.dict Decode.int) """{ "key1": 17, "key2": "seventy-one" }""" + --> Err ... +``` + +These functions `Decode.list : Decoder a -> Decoder (List a)` and `Decode.dict : Decoder a -> Decoder (Dict String a)` each expect a decoder as argument, which is expected to decode every single element elements of the data structure. +If the argument decoder fails to decode any element of an array or object value, the whole decoder fails, which is a common pattern. + +Combining simple decoders into more complex ones is the core idea of the technique. +We will introduce several more functions that can be used to build arbitrarily complex real-world data decoders through means of an example. + +## GeoJSON + +For practicing combining basic decoders to parse complex JSON data, let's build decoders for parsing (a subset of) GeoJSON data. +GeoJSON is a specialized JSON-based data format used to represent geographic features, such as locations, paths or regions, specified with 2D or 3D geographic coordinates, along with other properties. + +### Geometry Object + +A GeoJSON Geometry Object is an object that contains a `type` field with a string value (there are 7 possible type values) and a `coordinates` field with geographic coordinates (collected in a structure that depends on the type). +For example, + +```json +{ + "type": "Point", + "coordinates": [127.831, 26.461] +} +``` + +or + +```json +{ + "type": "LineString", + "coordinates": [ + [127.831, 26.461], + [127.829, 26.465], + [127.829, 26.469], + [127.83, 26.47] + ] +} +``` + +A Geometry Object could be represented in Elm with the following type + +```elm +type Geometry + = Point (List Float) + | LineString (List (List Float)) + | ... +``` + +To probe the value of a certain field, we can use `Decode.field : String -> Decoder a -> Decoder a` which decodes the value of a specific key of an object. + +```elm +decodePointCoordinates : Decoder (List Float) +decodePointCoordinates = + Decode.field "coordinates" (Decode.list Decode.float) + +decodeLineStringCoordinates : Decoder (List (List Float)) +decodeLineStringCoordinates = + Decode.field "coordinates" (Decode.list (Decode.list Decode.float)) +``` + +To transform the value successfully parsed by a decoder, we can use `Decode.map : (a -> value) -> Decoder a -> Decoder value`, or any of its siblings, from `Decode.map2 (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value` to `Decode.map8`. + +In this case, we want to decide which coordinate decoder to use depending on the value in the `type` field, which is a perfect job for `Decode.andThen : (a -> Decoder b) -> Decoder a -> Decoder b` and its friends `Decode.succeed : a -> Decoder a` and `Decode.fail : String -> Decoder a`. + +```elm +decodeGeometry : Decoder Geometry +decodeGeometry = + Decode.field "type" Decode.string + |> Decode.andThen + (\value -> + case value of + "Point" -> + decodePointCoordinates |> Decode.map Point + + "LineString" -> + decodeLineStringCoordinates |> Decode.map LineString + + _ -> + Decode.fail "Geometry not implemented yet, or invalid type value" + ) +``` + +Note at this point that we could use `Decode.andThen` to validate the coordinates (checking that the coordinates come in pairs or triplets and checking that the coordinate values make geographic sense). + +```elm +Decode.decodeString decodeGeometry """{"type": "Point", "coordinates": [127.831, 26.461]}""" + --> Ok (Point [127.831, 26.461]) + +Decode.decodeString decodeGeometry """{"type": "LineString", "coordinates": [127.831, 26.461]}""" + --> Err ... +``` + +### Feature Object + +A GeoJSON Feature Object represents something spatially bounded: it has a `type` field with the `"Feature"` value, a `geometry` field with a Geometry Object or a `null`, a `properties` field with an arbitrary JSON Object or a `null`, and an optional `id` field with a JSON string or number value. + +```json +{ + "type": "Feature", + "geometry": null, + "properties": null +} +``` + +or + +```json +{ + "type": "Feature", + "id": "0157", + "geometry": { + "type": "Point", + "coordinates": [127.831, 26.461] + }, + "properties": { + "country": "Japan" + } +} +``` + +Let's tackle the `id` field first. +If it exists, it must accommodate strings or numbers, so we could either introduce a custom type for the value, or always save is as a string. + +To choose between two possible decoders, we can use `Decode.oneOf : List (Decoder a) -> Decoder a`. + +```elm +decodeFeatureId : Decoder String +decodeFeatureId = + Decode.oneOf + [ Decode.string + , Decode.int |> Decode.map String.fromInt + , Decode.float |> Decode.map String.fromFloat + ] + |> Decode.field "id" + +Decode.decodeString decodeFeatureId """{"id": "seventeen"}""" + --> Ok "seventeen" + +Decode.decodeString decodeFeatureId """{"id": 17}""" + --> Ok "17" + +Decode.decodeString decodeFeatureId """{"type": "Point"}""" + --> Er ... +``` + +To express that the field is optional, we could use the same technique + +```elm +decodeMaybeFeatureId : Decoder (Maybe String) +decodeMaybeFeatureId = + Decode.oneOf + [ decodeFeatureId |> Decode.map Just + , Decode.succeed Nothing + ] +``` + +But we could instead use `Decode.maybe : Decoder a -> Decoder (Maybe a)` + +```elm +decodeMaybeFeatureId : Decoder (Maybe String) +decodeMaybeFeatureId = + Decode.maybe decodeFeatureId + +Decode.decodeString decodeMaybeFeatureId """{"id": 17}""" + --> Ok (Just "17") + +Decode.decodeString decodeMaybeFeatureId """{"type": "Point"}""" + --> Ok Nothing +``` + +Note that `decodeMaybeFeatureId` (or any decoder wrapped with `Decode.maybe`) will always succeed since it can always fallback on `Ok Nothing`, although a decoder that uses it might still fail + +```elm +Decode.decodeString (Decode.list decodeMaybeFeatureId) """{"id": 17}""" + --> Err ... +``` + +Next, let's focus on the `geometry` field, which can either be a Geometry Object or be `null`. +We could use `Decode.oneOf` once again, but there is a better option: `Decode.nullable : Decoder a -> Decoder (Maybe a)`. + +```elm +decodeFeatureGeometry : Decoder (Maybe Geometry) +decodeFeatureGeometry = + Decode.nullable decodeGeometry + |> Decode.field "geometry" + +Decode.decodeString decodeFeatureGeometry """{"geometry": {"type": "Point", "coordinates": [127.831, 26.461]}}""" + --> Ok (Just (Point [127.831, 26.461])) + +Decode.decodeString decodeFeatureGeometry """{"geometry": null}""" + --> Ok Nothing + +Decode.decodeString decodeFeatureGeometry """{"geometry": {}}""" + --> Err ... +``` + +`Decode.maybe` and `Decode.nullable` have the same signature, but their behavior is different, since the `OK Nothing` result of a `Decode.nullable` will only come from the specific JSON value `null`. + +Finally, the `properties` field can contain an arbitrary JSON Object or a `null`, which is a job for `Decode.value : Decoder Value`. + +```elm +decodeProperties : Decoder (Maybe (Dict String Value)) +decodeProperties = + Decode.nullable (Decode.dict Decode.value) + |> Decode.field "properties" + + +Decode.decodeString decodeProperties """{"properties": null}""" + --> Ok Nothing + +Decode.decodeString decodeProperties """{"properties": {"country": "Japan"}}""" + --> Ok (Just (Dict.fromList [("country", <internals>)])) + +Decode.decodeString decodeProperties """{"properties": 17}""" + --> Err ... +``` + +Note that `Value` is an opaque type, which means you cannot easily probe its content. +To use it, you can either write a decoder for it and run it with `Decode.decodeValue : Decoder a -> Value -> Result Error a`, or send it to JavaScript directly via a port. + +We are ready to parse Feature Objects: + +```elm +type alias Feature = + { id : Maybe String + , geometry : Maybe Geometry + , properties : Maybe (Dict String Value) + } + +decodeFeature : Decoder Feature +decodeFeature = + Decode.field "type" Decode.string + |> Decode.andThen + (\value -> + if value /= "Feature" then + Decode.fail "not a Feature" + + else + Decode.map3 Feature + decodeMaybeFeatureId + decodeFeatureGeometry + decodeProperties + ) + +Decode.decodeString decodeFeature """{"type": "Feature", "geometry": null, "properties": null}""" + --> Ok { geometry = Nothing, id = Nothing, properties = Nothing } + +""" +{ + "type": "Feature", + "id": "0157", + "geometry": { + "type": "Point", + "coordinates": [127.831, 26.461] + }, + "properties": { + "country": "Japan" + } +} +""" + |> Decode.decodeString decodeFeature + --> Ok { geometry = Just (Point [127.831,26.461]), id = Just "0157", properties = Just (Dict.fromList [("country",<internals>)]) } +``` + +## JSON Encoders + +Encoders allow to write valid JSON from Elm values using the `Encode.encode : Int -> Value -> String` function. +The first argument specifies the amount of indentation in the final result, and the second argument is the JSON value to write. + +A `Value` can either be obtained from `Decode.value` or be produced from one of the encoders: + +```elm +Encode.encode 0 (Encode.string "hello") + --> "hello" + +Encode.encode 0 (Encode.bool True) + --> "true" + +Encode.encode 0 (Encode.int 12) + --> "12" + +Encode.encode 0 (Encode.float 3.14) + --> "3.14" + +Encode.encode 0 Encode.null + --> "null" + +Encode.encode 0 (Encode.list Encode.int [1, 2, 3]) + --> "[1,2,3]" + +Encode.encode 4 (Encode.list Encode.int [1, 2, 3]) + --> "[\n 1,\n 2,\n 3\n]" + +Encode.encode 0 (Encode.dict String.toLower Encode.int (Dict.fromList [("KEY1", 17), ("KEY2", 71)])) + --> "{\"key1\":17,\"key2\":71}" + +Encode.encode 4 (Encode.dict String.toLower Encode.int (Dict.fromList [("KEY1", 17), ("KEY2", 71)])) + --> "{\n \"key1\": 17,\n \"key2\": 71\n}" +``` + +as well as with `Encode.object : List ( String, Value ) -> Value` + +```elm +Encode.object + [ ( "key1", Encode.int 17 ) + , ( "key2", Encode.int 71 ) + ] + |> Encode.encode 0 + --> "{\"key1\":17,\"key2\":71}" +``` + +Le's define encoders for the GeoJSON decoders defined earlier + +```elm +encodeGeometry : Geometry -> Value +encodeGeometry geometry = + case geometry of + Point coord -> + Encode.object + [ ( "type", Encode.string "Point" ) + , ( "coordinates", Encode.list Encode.float coord ) + ] + + LineString coord -> + Encode.object + [ ( "type", Encode.string "LineString" ) + , ( "coordinates", Encode.list (Encode.list Encode.float) coord ) + ] + +encodeFeature : Feature -> Value +encodeFeature feature = + let + maybeId = + case feature.id of + Nothing -> + [] + + Just id -> + [ ( "id", Encode.string id ) ] + in + Encode.object + (maybeId + ++ [ ( "type", Encode.string "Feature" ) + , ( "geometry" + , case feature.geometry of + Nothing -> + Encode.null + + Just geometry -> + encodeGeometry geometry + ) + , ( "properties" + , case feature.properties of + Nothing -> + Encode.null + + Just dict -> + Encode.dict identity identity dict + ) + ] + ) +``` + +In general, a good test for your decoder-encoder pairs is checking that `elmValue |> encoder |> decoder == elmValue`. diff --git a/exercises/concept/githup-api/.docs/introduction.md b/exercises/concept/githup-api/.docs/introduction.md new file mode 100644 index 00000000..cb89ed21 --- /dev/null +++ b/exercises/concept/githup-api/.docs/introduction.md @@ -0,0 +1,446 @@ +# Introduction + +## JSON + +### JSON + +JSON (JavaScript Object Notation) is a human readable data and file format commonly used to exchange data, in particular for web applications. +As such, it is no surprise that is holds a special place in the Elm language. + +Elm provides the core modules `Json.Decode` and `Json.Encode` to parse and write JSON data. +JSON Decoders are a way to _declare_ what type of data is expected and which data structure the data should map to. + +Let's import some aliases and types from the modules: + +```elm +import Json.Decode as Decode exposing (Decoder, Error) +import Json.Encode as Encode exposing (Value) +``` + +Using `Decode.decodeString : Decoder a -> String -> Result Error a` with a decoder and a JSON string will either succeed if the string confirms to the decoder specifications, or fail otherwise. +There is no way of inspecting the raw JSON data when defining the decoder, which could feel unusual coming from imperative languages. + +### Basic data type decoders + +JSON has a small number of core data types: + +- String: encoded with double quotes `"hello world"` +- Boolean: either `true` or `false` +- Number: such as `34` or `0.12` +- Null: the absence of data `null` + +Each of these is associated with a `Decode` function: + +```elm +Decode.decodeString Decode.string """\"hello\"""" + --> Ok "hello" + +Decode.decodeString Decode.string """null""" + --> Err ... + +Decode.decodeString Decode.bool """true""" + --> Ok True + +Decode.decodeString Decode.bool """0""" + --> Err ... + +Decode.decodeString Decode.int """12""" + --> Ok 12 + +Decode.decodeString Decode.float """3.14""" + --> Ok 3.14 + +Decode.decodeString (Decode.null AnyValue) """null""" + --> Ok AnyValue + +Decode.decodeString (Decode.null AnyValue) """true""" + --> Err ... +``` + +Note that `Decode.null` lets you decide how to model a `null` in your program by requiring an arbitrary value as argument (maybe `()`, `Nothing`, or anything appropriate to your program). + +### Combining decoders + +JSON also defines two data structures to collect core data types: + +- Array: a collection of other data types `[0, null, "none", []]` +- Object: a collection of key-value pairs `{"id": 1, "is_admin": false, "more_info": {}}` + +`Decode` also offers functions to decode these: + +```elm +Decode.decodeString (Decode.list Decode.int) """[1, 2, 3]""" + --> Ok [1, 2, 3] + +Decode.decodeString (Decode.list Decode.int) """[1, 2, 3, "not an int"]""" + --> Err ... + +Decode.decodeString (Decode.dict Decode.int) """{ "key1": 17, "key2": 71 }""" + --> Ok (Dict.fromList [("key1", 17), ("key2", 71)]) + +Decode.decodeString (Decode.dict Decode.int) """{ "key1": 17, "key2": "seventy-one" }""" + --> Err ... +``` + +These functions `Decode.list : Decoder a -> Decoder (List a)` and `Decode.dict : Decoder a -> Decoder (Dict String a)` each expect a decoder as argument, which is expected to decode every single element elements of the data structure. +If the argument decoder fails to decode any element of an array or object value, the whole decoder fails, which is a common pattern. + +Combining simple decoders into more complex ones is the core idea of the technique. +We will introduce several more functions that can be used to build arbitrarily complex real-world data decoders through means of an example. + +### GeoJSON + +For practicing combining basic decoders to parse complex JSON data, let's build decoders for parsing (a subset of) GeoJSON data. +GeoJSON is a specialized JSON-based data format used to represent geographic features, such as locations, paths or regions, specified with 2D or 3D geographic coordinates, along with other properties. + +#### Geometry Object + +A GeoJSON Geometry Object is an object that contains a `type` field with a string value (there are 7 possible type values) and a `coordinates` field with geographic coordinates (collected in a structure that depends on the type). +For example, + +```json +{ + "type": "Point", + "coordinates": [127.831, 26.461] +} +``` + +or + +```json +{ + "type": "LineString", + "coordinates": [ + [127.831, 26.461], + [127.829, 26.465], + [127.829, 26.469], + [127.83, 26.47] + ] +} +``` + +A Geometry Object could be represented in Elm with the following type + +```elm +type Geometry + = Point (List Float) + | LineString (List (List Float)) + | ... +``` + +To probe the value of a certain field, we can use `Decode.field : String -> Decoder a -> Decoder a` which decodes the value of a specific key of an object. + +```elm +decodePointCoordinates : Decoder (List Float) +decodePointCoordinates = + Decode.field "coordinates" (Decode.list Decode.float) + +decodeLineStringCoordinates : Decoder (List (List Float)) +decodeLineStringCoordinates = + Decode.field "coordinates" (Decode.list (Decode.list Decode.float)) +``` + +To transform the value successfully parsed by a decoder, we can use `Decode.map : (a -> value) -> Decoder a -> Decoder value`, or any of its siblings, from `Decode.map2 (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value` to `Decode.map8`. + +In this case, we want to decide which coordinate decoder to use depending on the value in the `type` field, which is a perfect job for `Decode.andThen : (a -> Decoder b) -> Decoder a -> Decoder b` and its friends `Decode.succeed : a -> Decoder a` and `Decode.fail : String -> Decoder a`. + +```elm +decodeGeometry : Decoder Geometry +decodeGeometry = + Decode.field "type" Decode.string + |> Decode.andThen + (\value -> + case value of + "Point" -> + decodePointCoordinates |> Decode.map Point + + "LineString" -> + decodeLineStringCoordinates |> Decode.map LineString + + _ -> + Decode.fail "Geometry not implemented yet, or invalid type value" + ) +``` + +Note at this point that we could use `Decode.andThen` to validate the coordinates (checking that the coordinates come in pairs or triplets and checking that the coordinate values make geographic sense). + +```elm +Decode.decodeString decodeGeometry """{"type": "Point", "coordinates": [127.831, 26.461]}""" + --> Ok (Point [127.831, 26.461]) + +Decode.decodeString decodeGeometry """{"type": "LineString", "coordinates": [127.831, 26.461]}""" + --> Err ... +``` + +#### Feature Object + +A GeoJSON Feature Object represents something spatially bounded: it has a `type` field with the `"Feature"` value, a `geometry` field with a Geometry Object or a `null`, a `properties` field with an arbitrary JSON Object or a `null`, and an optional `id` field with a JSON string or number value. + +```json +{ + "type": "Feature", + "geometry": null, + "properties": null +} +``` + +or + +```json +{ + "type": "Feature", + "id": "0157", + "geometry": { + "type": "Point", + "coordinates": [127.831, 26.461] + }, + "properties": { + "country": "Japan" + } +} +``` + +Let's tackle the `id` field first. +If it exists, it must accommodate strings or numbers, so we could either introduce a custom type for the value, or always save is as a string. + +To choose between two possible decoders, we can use `Decode.oneOf : List (Decoder a) -> Decoder a`. + +```elm +decodeFeatureId : Decoder String +decodeFeatureId = + Decode.oneOf + [ Decode.string + , Decode.int |> Decode.map String.fromInt + , Decode.float |> Decode.map String.fromFloat + ] + |> Decode.field "id" + +Decode.decodeString decodeFeatureId """{"id": "seventeen"}""" + --> Ok "seventeen" + +Decode.decodeString decodeFeatureId """{"id": 17}""" + --> Ok "17" + +Decode.decodeString decodeFeatureId """{"type": "Point"}""" + --> Er ... +``` + +To express that the field is optional, we could use the same technique + +```elm +decodeMaybeFeatureId : Decoder (Maybe String) +decodeMaybeFeatureId = + Decode.oneOf + [ decodeFeatureId |> Decode.map Just + , Decode.succeed Nothing + ] +``` + +But we could instead use `Decode.maybe : Decoder a -> Decoder (Maybe a)` + +```elm +decodeMaybeFeatureId : Decoder (Maybe String) +decodeMaybeFeatureId = + Decode.maybe decodeFeatureId + +Decode.decodeString decodeMaybeFeatureId """{"id": 17}""" + --> Ok (Just "17") + +Decode.decodeString decodeMaybeFeatureId """{"type": "Point"}""" + --> Ok Nothing +``` + +Note that `decodeMaybeFeatureId` (or any decoder wrapped with `Decode.maybe`) will always succeed since it can always fallback on `Ok Nothing`, although a decoder that uses it might still fail + +```elm +Decode.decodeString (Decode.list decodeMaybeFeatureId) """{"id": 17}""" + --> Err ... +``` + +Next, let's focus on the `geometry` field, which can either be a Geometry Object or be `null`. +We could use `Decode.oneOf` once again, but there is a better option: `Decode.nullable : Decoder a -> Decoder (Maybe a)`. + +```elm +decodeFeatureGeometry : Decoder (Maybe Geometry) +decodeFeatureGeometry = + Decode.nullable decodeGeometry + |> Decode.field "geometry" + +Decode.decodeString decodeFeatureGeometry """{"geometry": {"type": "Point", "coordinates": [127.831, 26.461]}}""" + --> Ok (Just (Point [127.831, 26.461])) + +Decode.decodeString decodeFeatureGeometry """{"geometry": null}""" + --> Ok Nothing + +Decode.decodeString decodeFeatureGeometry """{"geometry": {}}""" + --> Err ... +``` + +`Decode.maybe` and `Decode.nullable` have the same signature, but their behavior is different, since the `OK Nothing` result of a `Decode.nullable` will only come from the specific JSON value `null`. + +Finally, the `properties` field can contain an arbitrary JSON Object or a `null`, which is a job for `Decode.value : Decoder Value`. + +```elm +decodeProperties : Decoder (Maybe (Dict String Value)) +decodeProperties = + Decode.nullable (Decode.dict Decode.value) + |> Decode.field "properties" + + +Decode.decodeString decodeProperties """{"properties": null}""" + --> Ok Nothing + +Decode.decodeString decodeProperties """{"properties": {"country": "Japan"}}""" + --> Ok (Just (Dict.fromList [("country", <internals>)])) + +Decode.decodeString decodeProperties """{"properties": 17}""" + --> Err ... +``` + +Note that `Value` is an opaque type, which means you cannot easily probe its content. +To use it, you can either write a decoder for it and run it with `Decode.decodeValue : Decoder a -> Value -> Result Error a`, or send it to JavaScript directly via a port. + +We are ready to parse Feature Objects: + +```elm +type alias Feature = + { id : Maybe String + , geometry : Maybe Geometry + , properties : Maybe (Dict String Value) + } + +decodeFeature : Decoder Feature +decodeFeature = + Decode.field "type" Decode.string + |> Decode.andThen + (\value -> + if value /= "Feature" then + Decode.fail "not a Feature" + + else + Decode.map3 Feature + decodeMaybeFeatureId + decodeFeatureGeometry + decodeProperties + ) + +Decode.decodeString decodeFeature """{"type": "Feature", "geometry": null, "properties": null}""" + --> Ok { geometry = Nothing, id = Nothing, properties = Nothing } + +""" +{ + "type": "Feature", + "id": "0157", + "geometry": { + "type": "Point", + "coordinates": [127.831, 26.461] + }, + "properties": { + "country": "Japan" + } +} +""" + |> Decode.decodeString decodeFeature + --> Ok { geometry = Just (Point [127.831,26.461]), id = Just "0157", properties = Just (Dict.fromList [("country",<internals>)]) } +``` + +### JSON Encoders + +Encoders allow to write valid JSON from Elm values using the `Encode.encode : Int -> Value -> String` function. +The first argument specifies the amount of indentation in the final result, and the second argument is the JSON value to write. + +A `Value` can either be obtained from `Decode.value` or be produced from one of the encoders: + +```elm +Encode.encode 0 (Encode.string "hello") + --> "hello" + +Encode.encode 0 (Encode.bool True) + --> "true" + +Encode.encode 0 (Encode.int 12) + --> "12" + +Encode.encode 0 (Encode.float 3.14) + --> "3.14" + +Encode.encode 0 Encode.null + --> "null" + +Encode.encode 0 (Encode.list Encode.int [1, 2, 3]) + --> "[1,2,3]" + +Encode.encode 4 (Encode.list Encode.int [1, 2, 3]) + --> "[\n 1,\n 2,\n 3\n]" + +Encode.encode 0 (Encode.dict String.toLower Encode.int (Dict.fromList [("KEY1", 17), ("KEY2", 71)])) + --> "{\"key1\":17,\"key2\":71}" + +Encode.encode 4 (Encode.dict String.toLower Encode.int (Dict.fromList [("KEY1", 17), ("KEY2", 71)])) + --> "{\n \"key1\": 17,\n \"key2\": 71\n}" +``` + +as well as with `Encode.object : List ( String, Value ) -> Value` + +```elm +Encode.object + [ ( "key1", Encode.int 17 ) + , ( "key2", Encode.int 71 ) + ] + |> Encode.encode 0 + --> "{\"key1\":17,\"key2\":71}" +``` + +Le's define encoders for the GeoJSON decoders defined earlier + +```elm +encodeGeometry : Geometry -> Value +encodeGeometry geometry = + case geometry of + Point coord -> + Encode.object + [ ( "type", Encode.string "Point" ) + , ( "coordinates", Encode.list Encode.float coord ) + ] + + LineString coord -> + Encode.object + [ ( "type", Encode.string "LineString" ) + , ( "coordinates", Encode.list (Encode.list Encode.float) coord ) + ] + +encodeFeature : Feature -> Value +encodeFeature feature = + let + maybeId = + case feature.id of + Nothing -> + [] + + Just id -> + [ ( "id", Encode.string id ) ] + in + Encode.object + (maybeId + ++ [ ( "type", Encode.string "Feature" ) + , ( "geometry" + , case feature.geometry of + Nothing -> + Encode.null + + Just geometry -> + encodeGeometry geometry + ) + , ( "properties" + , case feature.properties of + Nothing -> + Encode.null + + Just dict -> + Encode.dict identity identity dict + ) + ] + ) +``` + +In general, a good test for your decoder-encoder pairs is checking that `elmValue |> encoder |> decoder == elmValue`. diff --git a/exercises/concept/monster-attack/.docs/introduction.md b/exercises/concept/monster-attack/.docs/introduction.md index d057b897..fe4a7d2c 100644 --- a/exercises/concept/monster-attack/.docs/introduction.md +++ b/exercises/concept/monster-attack/.docs/introduction.md @@ -1,6 +1,6 @@ # Introduction -## Partial application and function composition +## Partial Application and Function Composition ### Partial application