Giter Site home page Giter Site logo

purescript-option's Introduction

purescript-option

A data type for optional values.

Table of Contents

Explanation: Motivation for Option _

There are a few different data types that encapsulate ideas in programming.

Records capture the idea of a collection of key/value pairs where every key and value exist. E.g. Record (foo :: Boolean, bar :: Int) means that both foo and bar exist and with values all of the time.

Variants capture the idea of a collection of key/value pairs where exactly one of the key/value pairs exist. E.g. Data.Variant.Variant (foo :: Boolean, bar :: Int) means that either only foo exists with a value or only bar exists with a value, but not both at the same time.

Options capture the idea of a collection of key/value pairs where any key and value may or may not exist. E.g. Option.Option (foo :: Boolean, bar :: Int) means that either only foo exists with a value, only bar exists with a value, both foo and bar exist with values, or neither foo nor bar exist.

The distinction between these data types means that we can describe problems more accurately. Options are typically what you find in dynamic languages or in weakly-typed static languages. Their use cases range from making APIs more flexible to interfacing with serialization formats to providing better ergonomics around data types.

These data types are all specific to the PureScript language. Different data types exist in other languages that combine some of these ideas. In many languages records are a combination of both PureScript-style records and PureScript-style options. E.g. Option.Record (foo :: Boolean) (bar :: Int) means that foo exists with a value all of the time, and either bar exists with a value or bar doesn't exist with a value.

Other languages might signify optional fields with a question mark. E.g. In TypeScript, the previous example would be { foo: boolean; bar?: number }

This is different from a required field with an optional value. In PureScript, we might signify that by using: Record (foo :: Boolean, bar :: Data.Maybe.Maybe Int). In TypeScript, we might signify that by using: { foo: boolean; bar: number | null }

How To: Make a function with optional values

Let's say we want to make a greeting function where people can pass in an Option ( name :: String, title :: String ) to override the default behavior. I.e. we want something like: greeting :: Option.Option ( name :: String, title :: String ) -> String. The implementation should be fairly straight forward:

greeting :: Option.Option ( name :: String, title :: String ) -> String
greeting option = "Hello, " <> title' <> name'
  where
  name' :: String
  name' = case Option.get (Data.Symbol.SProxy :: _ "name") option of
    Data.Maybe.Just name -> name
    Data.Maybe.Nothing -> "World"

  title' :: String
  title' = case Option.get (Data.Symbol.SProxy :: _ "title") option of
    Data.Maybe.Just title -> title <> " "
    Data.Maybe.Nothing -> ""

We look up each key in the given Option _, and decide what to do with it. In the case of the "title", we append a space so the output is still legible. With the greeting function, we can pass in an option and alter the behavior:

> greeting (Option.fromRecord {})
"Hello, World"

> greeting (Option.fromRecord { title: "wonderful" })
"Hello, wonderful World"

> greeting (Option.fromRecord { name: "Pat" })
"Hello, Pat"

> greeting (Option.fromRecord { name: "Pat", title: "Dr." })
"Hello, Dr. Pat"

We've allowed people to override the behavior of the function with optional values!

It might be instructive to compare how we might write a similar function using a Record _ instead of Option _:

greeting' ::
  Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) ->
  String
greeting' option = "Hello, " <> title' <> name'
  where
  name' :: String
  name' = case option.name of
    Data.Maybe.Just name -> name
    Data.Maybe.Nothing -> "World"

  title' :: String
  title' = case option.title of
    Data.Maybe.Just title -> title <> " "
    Data.Maybe.Nothing -> ""

To implement greeting', nothing really changed. We used the built-in dot operator to fetch the keys out of the record, but we could have just as easily used Record.get (which would have highlighted the similarlities even more).

To use greeting', we force the users of greeting' to do always give us a value in the record:

> User.greeting' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing }
"Hello, World"

> User.greeting' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" }
"Hello, wonderful World"

> User.greeting' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing }
"Hello, Pat"

> User.greeting' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." }
"Hello, Dr. Pat"

How To: Make a function with optional values from a record

Let's say we want to solve a similar problem as before, but we don't want to force people to create the Option _ themselves. We want to allow people to pass in a record that may be missing some fields. To write this version of greeting, we'll need to use the FromRecord typeclass (see the section on FromRecord for more information). I.e. we want something like: greeting :: forall record. Option.FromRecord record ( name :: String, title :: String ) => Record record -> String.

The implementation moves the work of constructing the Option _, but should also be straight forward:

greeting ::
  forall record.
  Option.FromRecord record () ( name :: String, title :: String ) =>
  Record record ->
  String
greeting record = "Hello, " <> title' <> name'
  where
  name' :: String
  name' = case Option.get (Data.Symbol.SProxy :: _ "name") option of
    Data.Maybe.Just name -> name
    Data.Maybe.Nothing -> "World"

  option :: Option.Option ( name :: String, title :: String )
  option = Option.fromRecord record

  title' :: String
  title' = case Option.get (Data.Symbol.SProxy :: _ "title") option of
    Data.Maybe.Just title -> title <> " "
    Data.Maybe.Nothing -> ""

We can use this similar to how we used the previous implementation of greeting. Instead of needing to construct an Option _ and pass it in, we give the Record _ directly:

> greeting {}
"Hello, World"

> greeting { title: "wonderful" }
"Hello, wonderful World"

> greeting { name: "Pat" }
"Hello, Pat"

> greeting { name: "Pat", title: "Dr." }
"Hello, Dr. Pat"

We've allowed people to override the behavior of the function with optional values using a record!

How To: Make a function with required and optional values

Let's say we want to make a greeting function where people can pass in an Option.Record ( name :: String ) ( title :: String ). The "name" is required, but a "title" can be given to override the default behavior. I.e. we want something like: greeting :: Option.Record ( name :: String ) ( title :: String ) -> String. The implementation should be fairly straight forward:

import Prelude
import Data.Maybe as Data.Maybe
import Option as Option

greeting ::
  Option.Record ( name :: String ) ( title :: String ) ->
  String
greeting record' = "Hello, " <> title' <> record.name
  where
  record :: Record ( name :: String, title :: Data.Maybe.Maybe String )
  record = Option.recordToRecord record'

  title' :: String
  title' = case record.title of
    Data.Maybe.Just title -> title <> " "
    Data.Maybe.Nothing -> ""

We look up the "title" in the given Option.Record _ _, and decide what to do with it. In the case of the "title", we append a space so the output is still legible. With the greeting function, we can pass in an option and alter the behavior:

> greeting (Option.recordFromRecord { name: "Pat" })
"Hello, Pat"

> greeting (Option.recordFromRecord { name: "Pat", title: "Dr." })
"Hello, Dr. Pat"

We've allowed people to override the behavior of the function with optional values!

It might be instructive to compare how we might write a similar function using a Record _ instead of Option _:

greeting' ::
  Record ( name :: String, title :: Data.Maybe.Maybe String ) ->
  String
greeting' record = "Hello, " <> title' <> record.name
  where
  title' :: String
  title' = case record.title of
    Data.Maybe.Just title -> title <> " "
    Data.Maybe.Nothing -> ""

To implement greeting', nothing really changed. We don't have to convert down to a language-level Record because the argument is already a language-level Record. That's the only difference as far as implementing greeting'.

To use greeting', we force the users of greeting' to do always give us a value in the record:

> User.greeting' { name: "Pat", title: Data.Maybe.Nothing }
"Hello, Pat"

> User.greeting' { name: "Pat", title: Data.Maybe.Just "Dr." }
"Hello, Dr. Pat"

How To: Make a function with required and optional values from a record

Let's say we want to solve a similar problem as before, but we don't want to force people to create the Option.Record _ _ themselves. We want to allow people to pass in a record that may be missing some fields. To write this version of greeting, we'll need to use the FromRecord typeclass (see the section on FromRecord for more information). I.e. we want something like: greeting :: forall record. Option.FromRecord record ( name :: String ) ( title :: String ) => Record record -> String.

The implementation moves the work of constructing the Option.Record _ _, but should also be straight forward:

import Prelude
import Data.Maybe as Data.Maybe
import Option as Option

greeting ::
  forall record.
  Option.FromRecord record ( name :: String ) ( title :: String ) =>
  Record record ->
  String
greeting record'' = "Hello, " <> title' <> record.name
  where
  record :: Record ( name :: String, title :: Data.Maybe.Maybe String )
  record = Option.recordToRecord record'

  record' :: Option.Record ( name :: String ) ( title :: String )
  record' = Option.recordFromRecord record''

  title' :: String
  title' = case record.title of
    Data.Maybe.Just title -> title <> " "
    Data.Maybe.Nothing -> ""

We can use this similar to how we used the previous implementation of greeting. Instead of needing to construct an Option.Record _ _ and pass it in, we give the Record _ directly:

> greeting { name: "Pat" }
"Hello, Pat"

> greeting { name: "Pat", title: "Dr." }
"Hello, Dr. Pat"

We've allowed people to override the behavior of the function with optional values using a record!

How To: Decode and Encode JSON with optional values in purescript-argonaut

A common pattern with JSON objects is that keys do not always have to be present. Some APIs make the distinction between a JSON object like { "name": "Pat" } and one like { "name": "Pat", "title": null }. In the first case, it might recognize that the "title" key does not exist, and behave in a different way from the "title" key having a value of null. In the second case, it might notice that the "title" key exists and work with the value assuming it's good to go; the null might eventually cause a failure later.

In many cases, what we want is to not generate any fields that do not exist. Using purescript-argonaut, Option _ can help with that idea:

decode ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either String (Option.Option ( name :: String, title :: String ))
decode = Data.Argonaut.Decode.Class.decodeJson

encode ::
  Option.Option ( name :: String, title :: String ) ->
  Data.Argonaut.Core.Json
encode = Data.Argonaut.Encode.Class.encodeJson

parse ::
  String ->
  Data.Either.Either String (Option.Option (name :: String, title :: String))
parse string = case Data.Argonaut.Parser.jsonParser string of
  Data.Either.Left error -> Data.Either.Left error
  Data.Either.Right json -> case decode json of
    Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error)
    Data.Either.Right option -> Data.Either.Right option

We can give that a spin with some different JSON values:

> parse """{}"""
(Right (Option.fromRecord {}))

> parse """{"title": "wonderful"}"""
(Right (Option.fromRecord { title: "wonderful" }))

> parse """{"name": "Pat"}"""
(Right (Option.fromRecord { name: "Pat" }))

> parse """{"name": "Pat", "title": "Dr."}"""
(Right (Option.fromRecord { name: "Pat", title: "Dr." }))

> parse """{ "name": null }"""
Right (Option.fromRecord {})

> parse """{ "title": null }"""
Right (Option.fromRecord {})

> parse """{ "name": null, "title": null }"""
Right (Option.fromRecord {})

We can also produce some different JSON values:

> Data.Argonaut.Core.stringify (encode (Option.fromRecord {}))
"{}"

> Data.Argonaut.Core.stringify (encode (Option.fromRecord { title: "wonderful" }))
"{\"title\":\"wonderful\"}"

> Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat" }))
"{\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat", title: "Dr." }))
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

Notice that we don't end up with a "title" field in the JSON output unless we have a title field in our record.

It might be instructive to compare how we might write a similar functions using a Record _ instead of Option _: With purescript-argonaut, the instances for decoding and encoding on records expect the field to always exist no matter its value. If we attempt to go directly to Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ):

decode' ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
decode' = Data.Argonaut.Decode.Class.decodeJson

encode' ::
  Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) ->
  Data.Argonaut.Core.Json
encode' = Data.Argonaut.Encode.Class.encodeJson

parse' ::
  String ->
  Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
parse' string = case Data.Argonaut.Parser.jsonParser string of
  Data.Either.Left error -> Data.Either.Left error
  Data.Either.Right json -> case decode json of
    Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error)
    Data.Either.Right record -> Data.Either.Right record

We won't get the behavior we expect:

> parse' """{}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'title':\n  No value was found.")

> parse' """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'name':\n  No value was found.")

> parse' """{"name": "Pat"}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'title':\n  No value was found.")

> parse' """{"name": "Pat", "title": "Dr."}"""
(Right { name: (Just "Pat"), title: (Just "Dr.") })

> Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing })
"{\"title\":null,\"name\":null}"

> Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" })
"{\"title\":\"wonderful\",\"name\":null}"

> Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing })
"{\"title\":null,\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." })
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

Unless both fields exist, we cannot decode the JSON object. Similarly, no matter what the values are, we always encode them into a JSON object.

In order to emulate the behavior of an optional field, we have to name the record, and write our own instances:

newtype Greeting
  = Greeting
  ( Record
      ( name :: Data.Maybe.Maybe String
      , title :: Data.Maybe.Maybe String
      )
  )

derive instance genericGreeting :: Data.Generic.Rep.Generic Greeting _

instance showGreeting :: Show Greeting where
  show = Data.Generic.Rep.Show.genericShow

instance decodeJsonGreeting :: Data.Argonaut.Decode.Class.DecodeJson Greeting where
  decodeJson json = do
    object <- Data.Argonaut.Decode.Class.decodeJson json
    name <- Data.Argonaut.Decode.Combinators.getFieldOptional object "name"
    title <- Data.Argonaut.Decode.Combinators.getFieldOptional object "title"
    pure (Greeting { name, title })

instance encodeJsonGreeting :: Data.Argonaut.Encode.Class.EncodeJson Greeting where
  encodeJson (Greeting { name, title }) =
    Data.Argonaut.Encode.Combinators.extendOptional
      (Data.Argonaut.Encode.Combinators.assocOptional "name" name)
      ( Data.Argonaut.Encode.Combinators.extendOptional
          (Data.Argonaut.Encode.Combinators.assocOptional "title" title)
          (Data.Argonaut.Core.jsonEmptyObject)
      )

parse'' ::
  String ->
  Data.Either.Either String Greeting
parse'' string = case Data.Argonaut.Parser.jsonParser string of
  Data.Either.Left error -> Data.Either.Left error
  Data.Either.Right json -> case decode json of
    Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error)
    Data.Either.Right greeting -> Data.Either.Right greeting

If we try decoding and encoding now, we get something closer to what we wanted:

> parse'' """{}"""
(Right (Greeting { name: Nothing, title: Nothing }))

> parse'' """{"title": "wonderful"}"""
(Right (Greeting { name: Nothing, title: (Just "wonderful") }))

> parse'' """{"name": "Pat"}"""
(Right (Greeting { name: (Just "Pat"), title: Nothing }))

> parse'' """{"name": "Pat", "title": "Dr."}"""
(Right (Greeting { name: (Just "Pat"), title: (Just "Dr.") }))

> Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing }))
"{}"

> Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" }))
"{\"title\":\"wonderful\"}"

> Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing }))
"{\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." }))
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

How To: Decode and Encode JSON with optional values in purescript-codec-argonaut

A common pattern with JSON objects is that keys do not always have to be present. Some APIs make the distinction between a JSON object like { "name": "Pat" } and one like { "name": "Pat", "title": null }. In the first case, it might recognize that the "title" key does not exist, and behave in a different way from the "title" key having a value of null. In the second case, it might notice that the "title" key exists and work with the value assuming it's good to go; the null might eventually cause a failure later.

In many cases, what we want is to not generate any fields that do not exist. Using purescript-codec-argonaut, Option _ can help with that idea:

jsonCodec :: Data.Codec.Argonaut.JsonCodec (Option.Option ( name :: String, title :: String ))
jsonCodec =
  Option.jsonCodec
    { name: Data.Codec.Argonaut.string
    , title: Data.Codec.Argonaut.string
    }

We can add a couple of helpers to make decoding/encoding easier in the REPL:

decode ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Option.Option ( name :: String, title :: String ))
decode = Data.Codec.Argonaut.decode jsonCodec

encode ::
  Option.Option ( name :: String, title :: String ) ->
  Data.Argonaut.Core.Json
encode = Data.Codec.Argonaut.encode jsonCodec

parse ::
  String ->
  Data.Either.Either String (Option.Option ( name :: String, title :: String ))
parse string = do
  json <- Data.Argonaut.Parser.jsonParser string
  case decode json of
    Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err)
    Data.Either.Right option -> Data.Either.Right option

We can give that a spin with some different JSON values:

> parse """{}"""
(Right (Option.fromRecord {}))

> parse """{"title": "wonderful"}"""
(Right (Option.fromRecord { title: "wonderful" }))

> parse """{"name": "Pat"}"""
(Right (Option.fromRecord { name: "Pat" }))

> parse """{"name": "Pat", "title": "Dr."}"""
(Right (Option.fromRecord { name: "Pat", title: "Dr." }))

> parse """{ "name": null }"""
Right (Option.fromRecord {})

> parse """{ "title": null }"""
Right (Option.fromRecord {})

> parse """{ "name": null, "title": null }"""
Right (Option.fromRecord {})

We can also produce some different JSON values:

> Data.Argonaut.Core.stringify (encode (Option.fromRecord {}))
"{}"

> Data.Argonaut.Core.stringify (encode (Option.fromRecord { title: "wonderful" }))
"{\"title\":\"wonderful\"}"

> Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat" }))
"{\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (encode (Option.fromRecord { name: "Pat", title: "Dr." }))
"{\"name\":\"Pat\",\"title\":\"Dr.\"}"

Notice that we don't end up with a "title" field in the JSON output unless we have a title field in our record.

It might be instructive to compare how we might write a similar functions using a Record _ instead of Option _: With purescript-codec-argonaut, there are a couple of codecs that ship with the package for records: Data.Codec.Argonaut.recordProp and Data.Codec.Argonaut.Record.record. Each of those codecs expect the field to always exist no matter its value. The difference between those codecs is not very relevant except to say that the latter requires less characters to use. There are also a couple of codecs that ship with the package for Data.Maybe.Maybe _: Data.Codec.Argonaut.Common.maybe and Data.Codec.Argonaut.Compat.maybe. The former decodes/encodes with tagged values, the latter with nulls. If we attempt to go directly to Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) and Data.Codec.Argonaut.Common.maybe:

decode' ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
decode' = Data.Codec.Argonaut.decode jsonCodec'

encode' ::
  Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) ->
  Data.Argonaut.Core.Json
encode' = Data.Codec.Argonaut.encode jsonCodec'

jsonCodec' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
jsonCodec' =
  Data.Codec.Argonaut.Record.object
    "Greeting"
    { name: Data.Codec.Argonaut.Common.maybe Data.Codec.Argonaut.string
    , title: Data.Codec.Argonaut.Common.maybe Data.Codec.Argonaut.string
    }

parse' ::
  String ->
  Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
parse' string = do
  json <- Data.Argonaut.Parser.jsonParser string
  case decode' json of
    Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err)
    Data.Either.Right option -> Data.Either.Right option

We won't get the behavior we expect:

> parse' """{}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  No value was found.")

> parse' """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  Under 'Maybe':\n  Expected value of type 'Object'.")

> parse' """{"name": "Pat"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  No value was found.")

> parse' """{"name": "Pat", "title": "Dr."}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  Under 'Maybe':\n  Expected value of type 'Object'.")

> parse' """{"name": {"tag": "Just", "value": "Pat"}, "title": {"tag": "Just", "value": "Dr."}}"""
(Right { name: (Just "Pat"), title: (Just "Dr.") })

> Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing })
"{\"name\":{\"tag\":\"Nothing\"},\"title\":{\"tag\":\"Nothing\"}}"

> Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" })
"{\"name\":{\"tag\":\"Nothing\"},\"title\":{\"tag\":\"Just\",\"value\":\"wonderful\"}}"

> Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing })
"{\"name\":{\"tag\":\"Just\",\"value\":\"Pat\"},\"title\":{\"tag\":\"Nothing\"}}"

> Data.Argonaut.Core.stringify (encode' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." })
"{\"name\":{\"tag\":\"Just\",\"value\":\"Pat\"},\"title\":{\"tag\":\"Just\",\"value\":\"Dr.\"}}"

Unless both fields exist, we cannot decode the JSON object. Not only is every field required, they're serialized as tagged values. Similarly, no matter what the values are, we always encode them into a JSON object.

If we try with Data.Codec.Argonaut.Compat.maybe:

decode'' ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
decode'' = Data.Codec.Argonaut.decode jsonCodec''

encode'' ::
  Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) ->
  Data.Argonaut.Core.Json
encode'' = Data.Codec.Argonaut.encode jsonCodec''

jsonCodec'' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
jsonCodec'' =
  Data.Codec.Argonaut.Record.object
    "Greeting"
    { name: Data.Codec.Argonaut.Compat.maybe Data.Codec.Argonaut.string
    , title: Data.Codec.Argonaut.Compat.maybe Data.Codec.Argonaut.string
    }

parse'' ::
  String ->
  Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
parse'' string = do
  json <- Data.Argonaut.Parser.jsonParser string
  case decode'' json of
    Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err)
    Data.Either.Right option -> Data.Either.Right option

We also don't get the behavior we expect:

> parse'' """{}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  No value was found.")

> parse'' """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key name:\n  No value was found.")

> parse'' """{"name": "Pat"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  No value was found.")

> parse'' """{"name": "Pat", "title": "Dr."}"""
(Right { name: (Just "Pat"), title: (Just "Dr.") })

> Data.Argonaut.Core.stringify (encode'' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing })
"{\"name\":null,\"title\":null}"

> Data.Argonaut.Core.stringify (encode'' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" })
"{\"name\":null,\"title\":\"wonderful\"}"

> Data.Argonaut.Core.stringify (encode'' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing })
"{\"name\":\"Pat\",\"title\":null}"

> Data.Argonaut.Core.stringify (encode'' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." })
"{\"name\":\"Pat\",\"title\":\"Dr.\"}"

Unless both fields exist, we cannot decode the JSON object. Similarly, no matter what the values are, we always encode them into a JSON object.

In order to emulate the behavior of an optional field, we have to use a different codec:

optionalField ::
  forall label record record' value.
  Data.Symbol.IsSymbol label =>
  Prim.Row.Cons label (Data.Maybe.Maybe value) record' record =>
  Prim.Row.Lacks label record' =>
  Data.Symbol.SProxy label ->
  Data.Codec.Argonaut.JsonCodec value ->
  Data.Codec.Argonaut.JPropCodec (Record record') ->
  Data.Codec.Argonaut.JPropCodec (Record record)
optionalField label codecValue codecRecord =
  Data.Codec.GCodec
    (Control.Monad.Reader.Trans.ReaderT decodeField)
    (Data.Profunctor.Star.Star encodeField)
  where
  decodeField ::
    Foreign.Object.Object Data.Argonaut.Core.Json ->
    Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record record)
  decodeField object' = do
    record <- Data.Codec.Argonaut.decode codecRecord object'
    case Foreign.Object.lookup key object' of
      Data.Maybe.Just json -> case Data.Codec.Argonaut.decode codecValue json of
        Data.Either.Left error -> Data.Either.Left (Data.Codec.Argonaut.AtKey key error)
        Data.Either.Right value -> Data.Either.Right (Record.insert label (Data.Maybe.Just value) record)
      Data.Maybe.Nothing -> Data.Either.Right (Record.insert label Data.Maybe.Nothing record)

  encodeField ::
    Record record ->
    Control.Monad.Writer.Writer (Data.List.List (Data.Tuple.Tuple String Data.Argonaut.Core.Json)) (Record record)
  encodeField record = do
    case Record.get label record of
      Data.Maybe.Just value ->
        Control.Monad.Writer.Class.tell
          ( Data.List.Cons
              (Data.Tuple.Tuple key (Data.Codec.Argonaut.encode codecValue value))
              Data.List.Nil
          )
      Data.Maybe.Nothing -> pure unit
    Control.Monad.Writer.Class.tell
      (Data.Codec.Argonaut.encode codecRecord (Record.delete label record))
    pure record

  key :: String
  key = Data.Symbol.reflectSymbol label

With this codec defined, we can implement a codec for the record with optional fields:

decode''' ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
decode''' = Data.Codec.Argonaut.decode jsonCodec'''

encode''' ::
  Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) ->
  Data.Argonaut.Core.Json
encode''' = Data.Codec.Argonaut.encode jsonCodec'''

jsonCodec''' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
jsonCodec''' =
  Data.Codec.Argonaut.object
    "Greeting"
    ( optionalField (Data.Symbol.SProxy :: _ "name") Data.Codec.Argonaut.string
        $ optionalField (Data.Symbol.SProxy :: _ "title") Data.Codec.Argonaut.string
        $ Data.Codec.Argonaut.record
    )

parse''' ::
  String ->
  Data.Either.Either String (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
parse''' string = do
  json <- Data.Argonaut.Parser.jsonParser string
  case decode''' json of
    Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err)
    Data.Either.Right option -> Data.Either.Right option

If we try decoding and encoding now, we get something closer to what we wanted:

> parse''' """{}"""
(Right { name: Nothing, title: Nothing })

> parse''' """{"title": "wonderful"}"""
(Right { name: Nothing, title: (Just "wonderful") })

> parse''' """{"name": "Pat"}"""
(Right { name: (Just "Pat"), title: Nothing })

> parse''' """{"name": "Pat", "title": "wonderful"}"""
(Right { name: (Just "Pat"), title: (Just "wonderful") })

> Data.Argonaut.Core.stringify (encode''' { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing })
"{}"

> Data.Argonaut.Core.stringify (encode''' { name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful" })
"{\"title\":\"wonderful\"}"

> Data.Argonaut.Core.stringify (encode''' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing })
"{\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (encode''' { name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr." })
"{\"name\":\"Pat\",\"title\":\"Dr.\"}"

How To: Decode and Encode JSON with optional values in purescript-simple-json

A common pattern with JSON objects is that keys do not always have to be present. Some APIs make the distinction between a JSON object like { "name": "Pat" } and one like { "name": "Pat", "title": null }. In the first case, it might recognize that the "title" key does not exist, and behave in a different way from the "title" key having a value of null. In the second case, it might notice that the "title" key exists and work with the value assuming it's good to go; the null might eventually cause a failure later.

In many cases, what we want is to not generate any fields that do not exist. Using purescript-simple-json, Option _ can help with that idea:

readJSON ::
  String ->
  Simple.JSON.E (Option.Option ( name :: String, title :: String ))
readJSON = Simple.JSON.readJSON

writeJSON ::
  Option.Option ( name :: String, title :: String ) ->
  String
writeJSON = Simple.JSON.writeJSON

We can give that a spin with some different JSON values:

> readJSON """{}"""
(Right (Option.fromRecord {}))

> readJSON """{"title": "wonderful"}"""
(Right (Option.fromRecord { title: "wonderful" }))

> readJSON """{"name": "Pat"}"""
(Right (Option.fromRecord { name: "Pat" }))

> readJSON """{"name": "Pat", "title": "Dr."}"""
(Right (Option.fromRecord { name: "Pat", title: "Dr." }))

> readJSON """{ "name": null }"""
Right (Option.fromRecord {})

> readJSON """{ "title": null }"""
Right (Option.fromRecord {})

> readJSON """{ "name": null, "title": null }"""
Right (Option.fromRecord {})

We can also produce some different JSON values:

> writeJSON (Option.fromRecord {})
"{}"

> writeJSON (Option.fromRecord {title: "wonderful"})
"{\"title\":\"wonderful\"}"

> writeJSON (Option.fromRecord {name: "Pat"})
"{\"name\":\"Pat\"}"

> writeJSON (Option.fromRecord {name: "Pat", title: "Dr."})
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

Notice that we don't end up with a "title" field in the JSON output unless we have a title field in our record.

It might be instructive to compare how we might write a similar functions using a Record _ instead of Option _: With purescript-simple-json, the instances for decoding and encoding on records handle Data.Maybe.Maybe _ values like they are optional. If we attempt to go directly to Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ):

readJSON' ::
  String ->
  Simple.JSON.E (Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ))
readJSON' = Simple.JSON.readJSON

writeJSON' ::
  Record ( name :: Data.Maybe.Maybe String, title :: Data.Maybe.Maybe String ) ->
  String
writeJSON' = Simple.JSON.writeJSON

We get the behavior we expect:

> readJSON' """{}"""
(Right { name: Nothing, title: Nothing })

> readJSON' """{"title": "wonderful"}"""
(Right { name: Nothing, title: (Just "wonderful") })

> readJSON' """{"name": "Pat"}"""
(Right { name: (Just "Pat"), title: Nothing })

> readJSON' """{"name": "Pat", "title": "Dr."}"""
(Right { name: (Just "Pat"), title: (Just "Dr.") })
> decode' =<< Data.Argonaut.Parser.jsonParser """{}"""
(Left "JSON was missing expected field: title")

> writeJSON' {name: Data.Maybe.Nothing, title: Data.Maybe.Nothing}
"{}"

> writeJSON' {name: Data.Maybe.Nothing, title: Data.Maybe.Just "wonderful"}
"{\"title\":\"wonderful\"}"

> writeJSON' {name: Data.Maybe.Just "Pat", title: Data.Maybe.Nothing}
"{\"name\":\"Pat\"}"

> writeJSON' {name: Data.Maybe.Just "Pat", title: Data.Maybe.Just "Dr."}
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

How To: Decode and Encode JSON with required and optional values in purescript-argonaut

Another common pattern with JSON objects is that some keys always have to be present while others do not. Some APIs make the distinction between a JSON object like { "name": "Pat" } and one like { "name": "Pat", "title": null }. In the first case, it might recognize that the "title" key does not exist, and behave in a different way from the "title" key having a value of null. In the second case, it might notice that the "title" key exists and work with the value assuming it's good to go; the null might eventually cause a failure later.

In many cases, what we want is to not generate any fields that do not exist. Using purescript-argonaut, Option.Record _ _ can help with that idea:

import Prelude
import Data.Argonaut.Core as Data.Argonaut.Core
import Data.Argonaut.Decode.Class as Data.Argonaut.Decode.Class
import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error
import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class
import Data.Argonaut.Parser as Data.Argonaut.Parser
import Data.Either as Data.Either
import Option as Option

decode ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either String (Option.Record ( name :: String ) ( title :: String ))
decode = Data.Argonaut.Decode.Class.decodeJson

encode ::
  Option.Record ( name :: String ) ( title :: String ) ->
  Data.Argonaut.Core.Json
encode = Data.Argonaut.Encode.Class.encodeJson

parse ::
  String ->
  Data.Either.Either String (Option.Record ( name :: String ) ( title :: String ))
parse string = case Data.Argonaut.Parser.jsonParser string of
  Data.Either.Left error -> Data.Either.Left error
  Data.Either.Right json -> case decode json of
    Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error)
    Data.Either.Right record -> Data.Either.Right record

We can give that a spin with some different JSON values:

> parse """{}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'name':\n  No value was found.")

> parse """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'name':\n  No value was found.")

> parse """{"name": "Pat"}"""
(Right (Option.recordFromRecord { name: "Pat" }))

> parse """{"name": "Pat", "title": "Dr."}"""
(Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))

> parse """{ "name": "Pat", "title": null }"""
Right (Option.recordFromRecord { name: "Pat" })

We can also produce some different JSON values:

> Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat" }))
"{\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat", title: "Dr." }))
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

Notice that we don't end up with a "title" field in the JSON output unless we have a title field in our record.

It might be instructive to compare how we might write a similar functions using a Record _ instead of Option.Record _ _: With purescript-argonaut, the instances for decoding and encoding on records expect the field to always exist no matter its value. If we attempt to go directly to Record ( name :: String, title :: Data.Maybe.Maybe String ):

import Data.Argonaut.Core as Data.Argonaut.Core
import Data.Argonaut.Decode.Class as Data.Argonaut.Decode.Class
import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error
import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class
import Data.Argonaut.Parser as Data.Argonaut.Parser
import Data.Either as Data.Either
import Data.Maybe as Data.Maybe

decode' ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String ))
decode' = Data.Argonaut.Decode.Class.decodeJson

encode' ::
  Record ( name :: String, title :: Data.Maybe.Maybe String ) ->
  Data.Argonaut.Core.Json
encode' = Data.Argonaut.Encode.Class.encodeJson

parse' ::
  String ->
  Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String ))
parse' string = case Data.Argonaut.Parser.jsonParser string of
  Data.Either.Left error -> Data.Either.Left error
  Data.Either.Right json -> case decode json of
    Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error)
    Data.Either.Right record -> Data.Either.Right record

We won't get the behavior we expect:

> parse' """{}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'title':\n  No value was found.")

> parse' """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'name':\n  No value was found.")

> parse' """{"name": "Pat"}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'title':\n  No value was found.")

> parse' """{"name": "Pat", "title": "Dr."}"""
(Right { name: "Pat", title: (Just "Dr.") })

> Data.Argonaut.Core.stringify (encode' { name: "Pat", title: Data.Maybe.Nothing })
"{\"title\":null,\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (encode' { name: "Pat", title: Data.Maybe.Just "Dr." })
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

Unless both fields exist, we cannot decode the JSON object. Similarly, no matter what the values are, we always encode them into a JSON object.

In order to emulate the behavior of an optional field, we have to name the record, and write our own instances:

import Prelude
import Data.Argonaut.Core as Data.Argonaut.Core
import Data.Argonaut.Decode.Class as Data.Argonaut.Decode.Class
import Data.Argonaut.Decode.Combinators as Data.Argonaut.Decode.Combinators
import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error
import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class
import Data.Argonaut.Encode.Combinators as Data.Argonaut.Encode.Combinators
import Data.Argonaut.Parser as Data.Argonaut.Parser
import Data.Either as Data.Either
import Data.Generic.Rep as Data.Generic.Rep
import Data.Generic.Rep.Show as Data.Generic.Rep.Show
import Data.Maybe as Data.Maybe

newtype Greeting
  = Greeting
  ( Record
      ( name :: String
      , title :: Data.Maybe.Maybe String
      )
  )

derive instance genericGreeting :: Data.Generic.Rep.Generic Greeting _

instance showGreeting :: Show Greeting where
  show = Data.Generic.Rep.Show.genericShow

instance decodeJsonGreeting :: Data.Argonaut.Decode.Class.DecodeJson Greeting where
  decodeJson json = do
    object <- Data.Argonaut.Decode.Class.decodeJson json
    name <- Data.Argonaut.Decode.Combinators.getField object "name"
    title <- Data.Argonaut.Decode.Combinators.getFieldOptional object "title"
    pure (Greeting { name, title })

instance encodeJsonGreeting :: Data.Argonaut.Encode.Class.EncodeJson Greeting where
  encodeJson (Greeting { name, title }) =
    Data.Argonaut.Encode.Combinators.extend
      (Data.Argonaut.Encode.Combinators.assoc "name" name)
      ( Data.Argonaut.Encode.Combinators.extendOptional
          (Data.Argonaut.Encode.Combinators.assocOptional "title" title)
          (Data.Argonaut.Core.jsonEmptyObject)
      )

parse'' ::
  String ->
  Data.Either.Either String Greeting
parse'' string = case Data.Argonaut.Parser.jsonParser string of
  Data.Either.Left error -> Data.Either.Left error
  Data.Either.Right json -> case decode json of
    Data.Either.Left error -> Data.Either.Left (Data.Argonaut.Decode.Error.printJsonDecodeError error)
    Data.Either.Right greeting -> Data.Either.Right greeting

If we try decoding and encoding now, we get something closer to what we wanted:

> parse'' """{}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'name':\n  No value was found.")

> parse'' """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  At object key 'name':\n  No value was found.")

> parse'' """{"name": "Pat"}"""
(Right (Greeting { name: "Pat", title: Nothing }))

> parse'' """{"name": "Pat", "title": "Dr."}"""
(Right (Greeting { name: "Pat", title: (Just "Dr.") }))

> Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: "Pat", title: Data.Maybe.Nothing }))
"{\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (Data.Argonaut.Encode.Class.encodeJson (Greeting { name: "Pat", title: Data.Maybe.Just "Dr." }))
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

How To: Decode and Encode JSON with required and optional values in purescript-codec-argonaut

Another common pattern with JSON objects is that some keys always have to be present while others do not. Some APIs make the distinction between a JSON object like { "name": "Pat" } and one like { "name": "Pat", "title": null }. In the first case, it might recognize that the "title" key does not exist, and behave in a different way from the "title" key having a value of null. In the second case, it might notice that the "title" key exists and work with the value assuming it's good to go; the null might eventually cause a failure later.

In many cases, what we want is to not generate any fields that do not exist. Using purescript-codec-argonaut, Option.Record _ _ can help with that idea:

import Data.Codec.Argonaut as Data.Codec.Argonaut
import Option as Option

jsonCodec :: Data.Codec.Argonaut.JsonCodec (Option.Record ( name :: String ) ( title :: String ))
jsonCodec =
  Option.jsonCodecRecord
    "Greeting"
    { name: Data.Codec.Argonaut.string
    , title: Data.Codec.Argonaut.string
    }

We can add a couple of helpers to make decoding/encoding easier in the REPL:

import Prelude
import Data.Argonaut.Core as Data.Argonaut.Core
import Data.Argonaut.Parser as Data.Argonaut.Parser
import Data.Codec.Argonaut as Data.Codec.Argonaut
import Data.Either as Data.Either
import Option as Option

decode ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Option.Record ( name :: String ) ( title :: String ))
decode = Data.Codec.Argonaut.decode jsonCodec

encode ::
  Option.Record ( name :: String ) ( title :: String ) ->
  Data.Argonaut.Core.Json
encode = Data.Codec.Argonaut.encode jsonCodec

parse ::
  String ->
  Data.Either.Either String (Option.Record ( name :: String ) ( title :: String ))
parse string = do
  json <- Data.Argonaut.Parser.jsonParser string
  case decode json of
    Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err)
    Data.Either.Right record -> Data.Either.Right record

We can give that a spin with some different JSON values:

> parse """{}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key name:\n  No value was found.")

> parse """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key name:\n  No value was found.")

> parse """{"name": "Pat"}"""
(Right (Option.recordFromRecord { name: "Pat" }))

> parse """{"name": "Pat", "title": "Dr."}"""
(Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))

> parse """{ "name": "Pat", "title": null }"""
Right (Option.recordFromRecord { name: "Pat" })

Notice that we have to supply a "name" field in the JSON input otherwise it will not parse.

We can also produce some different JSON values:

> Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat" }))
"{\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (encode (Option.recordFromRecord { name: "Pat", title: "Dr." }))
"{\"name\":\"Pat\",\"title\":\"Dr.\"}"

Notice that we don't end up with a "title" field in the JSON output unless we have a title field in our record.

It might be instructive to compare how we might write a similar functions using a Record _ instead of Option.Record _: With purescript-codec-argonaut, there are a couple of codecs that ship with the package for records: Data.Codec.Argonaut.recordProp and Data.Codec.Argonaut.Record.record. Each of those codecs expect the field to always exist no matter its value. The difference between those codecs is not very relevant except to say that the latter requires less characters to use. There are also a couple of codecs that ship with the package for Data.Maybe.Maybe _: Data.Codec.Argonaut.Common.maybe and Data.Codec.Argonaut.Compat.maybe. The former decodes/encodes with tagged values, the latter with nulls. If we attempt to go directly to Record ( name :: String, title :: Data.Maybe.Maybe String ) and Data.Codec.Argonaut.Common.maybe:

import Prelude
import Data.Argonaut.Core as Data.Argonaut.Core
import Data.Argonaut.Parser as Data.Argonaut.Parser
import Data.Codec.Argonaut as Data.Codec.Argonaut
import Data.Codec.Argonaut.Common as Data.Codec.Argonaut.Common
import Data.Codec.Argonaut.Compat as Data.Codec.Argonaut.Compat
import Data.Codec.Argonaut.Record as Data.Codec.Argonaut.Record
import Data.Either as Data.Either
import Data.Maybe as Data.Maybe

decode' ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: String, title :: Data.Maybe.Maybe String ))
decode' = Data.Codec.Argonaut.decode jsonCodec'

encode' ::
  Record ( name :: String, title :: Data.Maybe.Maybe String ) ->
  Data.Argonaut.Core.Json
encode' = Data.Codec.Argonaut.encode jsonCodec'

jsonCodec' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: String, title :: Data.Maybe.Maybe String ))
jsonCodec' =
  Data.Codec.Argonaut.Record.object
    "Greeting"
    { name: Data.Codec.Argonaut.string
    , title: Data.Codec.Argonaut.Common.maybe Data.Codec.Argonaut.string
    }

parse' ::
  String ->
  Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String ))
parse' string = do
  json <- Data.Argonaut.Parser.jsonParser string
  case decode' json of
    Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err)
    Data.Either.Right option -> Data.Either.Right option

We won't get the behavior we expect:

> parse' """{}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  No value was found.")

> parse' """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  Under 'Maybe':\n  Expected value of type 'Object'.")

> parse' """{"name": "Pat"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  No value was found.")

> parse' """{"name": "Pat", "title": "Dr."}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  Under 'Maybe':\n  Expected value of type 'Object'.")

> parse' """{"name": "Pat", "title": {"tag": "Just", "value": "Dr."}}"""
(Right { name: "Pat", title: (Just "Dr.") })

> Data.Argonaut.Core.stringify (encode' { name: "Pat", title: Data.Maybe.Nothing })
"{\"name\":\"Pat\",\"title\":{\"tag\":\"Nothing\"}}"

> Data.Argonaut.Core.stringify (encode' { name: "Pat", title: Data.Maybe.Just "Dr." })
"{\"name\":\"Pat\",\"title\":{\"tag\":\"Just\",\"value\":\"Dr.\"}}"

Unless both fields exist, we cannot decode the JSON object. Not only is every field required, they're serialized as tagged values. Similarly, no matter what the optional values are, we always encode them into a JSON object.

If we try with Data.Codec.Argonaut.Compat.maybe:

decode'' ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: String, title :: Data.Maybe.Maybe String ))
decode'' = Data.Codec.Argonaut.decode jsonCodec''

encode'' ::
  Record ( name :: String, title :: Data.Maybe.Maybe String ) ->
  Data.Argonaut.Core.Json
encode'' = Data.Codec.Argonaut.encode jsonCodec''

jsonCodec'' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: String, title :: Data.Maybe.Maybe String ))
jsonCodec'' =
  Data.Codec.Argonaut.Record.object
    "Greeting"
    { name: Data.Codec.Argonaut.string
    , title: Data.Codec.Argonaut.Compat.maybe Data.Codec.Argonaut.string
    }

parse'' ::
  String ->
  Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String ))
parse'' string = do
  json <- Data.Argonaut.Parser.jsonParser string
  case decode'' json of
    Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err)
    Data.Either.Right option -> Data.Either.Right option

We also don't get the behavior we expect:

> parse'' """{}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  No value was found.")

> parse'' """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key name:\n  No value was found.")

> parse'' """{"name": "Pat"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key title:\n  No value was found.")

> parse'' """{"name": "Pat", "title": "Dr."}"""
(Right { name: "Pat", title: (Just "Dr.") })

> Data.Argonaut.Core.stringify (encode'' { name: "Pat", title: Data.Maybe.Nothing })
"{\"name\":\"Pat\",\"title\":null}"

> Data.Argonaut.Core.stringify (encode'' { name: "Pat", title: Data.Maybe.Just "Dr." })
"{\"name\":\"Pat\",\"title\":\"Dr.\"}"

Unless both fields exist, we cannot decode the JSON object. Similarly, no matter what the optional values are, we always encode them into a JSON object.

In order to emulate the behavior of an optional field, we have to use a different codec:

import Prelude
import Control.Monad.Reader.Trans as Control.Monad.Reader.Trans
import Control.Monad.Writer as Control.Monad.Writer
import Control.Monad.Writer.Class as Control.Monad.Writer.Class
import Data.Codec as Data.Codec
import Data.List as Data.List
import Data.Profunctor.Star as Data.Profunctor.Star
import Data.Symbol as Data.Symbol
import Data.Tuple as Data.Tuple
import Foreign.Object as Foreign.Object
import Prim.Row as Prim.Row
import Record as Record

optionalField ::
  forall label record record' value.
  Data.Symbol.IsSymbol label =>
  Prim.Row.Cons label (Data.Maybe.Maybe value) record' record =>
  Prim.Row.Lacks label record' =>
  Data.Symbol.SProxy label ->
  Data.Codec.Argonaut.JsonCodec value ->
  Data.Codec.Argonaut.JPropCodec (Record record') ->
  Data.Codec.Argonaut.JPropCodec (Record record)
optionalField label codecValue codecRecord =
  Data.Codec.GCodec
    (Control.Monad.Reader.Trans.ReaderT decodeField)
    (Data.Profunctor.Star.Star encodeField)
  where
  decodeField ::
    Foreign.Object.Object Data.Argonaut.Core.Json ->
    Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record record)
  decodeField object' = do
    record <- Data.Codec.Argonaut.decode codecRecord object'
    case Foreign.Object.lookup key object' of
      Data.Maybe.Just json -> case Data.Codec.Argonaut.decode codecValue json of
        Data.Either.Left error -> Data.Either.Left (Data.Codec.Argonaut.AtKey key error)
        Data.Either.Right value -> Data.Either.Right (Record.insert label (Data.Maybe.Just value) record)
      Data.Maybe.Nothing -> Data.Either.Right (Record.insert label Data.Maybe.Nothing record)

  encodeField ::
    Record record ->
    Control.Monad.Writer.Writer (Data.List.List (Data.Tuple.Tuple String Data.Argonaut.Core.Json)) (Record record)
  encodeField record = do
    case Record.get label record of
      Data.Maybe.Just value ->
        Control.Monad.Writer.Class.tell
          ( Data.List.Cons
              (Data.Tuple.Tuple key (Data.Codec.Argonaut.encode codecValue value))
              Data.List.Nil
          )
      Data.Maybe.Nothing -> pure unit
    Control.Monad.Writer.Class.tell
      (Data.Codec.Argonaut.encode codecRecord (Record.delete label record))
    pure record

  key :: String
  key = Data.Symbol.reflectSymbol label

With this codec defined, we can implement a codec for the record with required and optional fields:

decode''' ::
  Data.Argonaut.Core.Json ->
  Data.Either.Either Data.Codec.Argonaut.JsonDecodeError (Record ( name :: String, title :: Data.Maybe.Maybe String ))
decode''' = Data.Codec.Argonaut.decode jsonCodec'''

encode''' ::
  Record ( name :: String, title :: Data.Maybe.Maybe String ) ->
  Data.Argonaut.Core.Json
encode''' = Data.Codec.Argonaut.encode jsonCodec'''

jsonCodec''' :: Data.Codec.Argonaut.JsonCodec (Record ( name :: String, title :: Data.Maybe.Maybe String ))
jsonCodec''' =
  Data.Codec.Argonaut.object
    "Greeting"
    ( Data.Codec.Argonaut.recordProp (Data.Symbol.SProxy :: _ "name") Data.Codec.Argonaut.string
        $ optionalField (Data.Symbol.SProxy :: _ "title") Data.Codec.Argonaut.string
        $ Data.Codec.Argonaut.record
    )

parse''' ::
  String ->
  Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String ))
parse''' string = do
  json <- Data.Argonaut.Parser.jsonParser string
  case decode''' json of
    Data.Either.Left err -> Data.Either.Left (Data.Codec.Argonaut.printJsonDecodeError err)
    Data.Either.Right option -> Data.Either.Right option

If we try decoding and encoding now, we get something closer to what we wanted:

> parse''' """{}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key name:\n  No value was found.")

> parse''' """{"title": "wonderful"}"""
(Left "An error occurred while decoding a JSON value:\n  Under 'Greeting':\n  At object key name:\n  No value was found.")

> parse''' """{"name": "Pat"}"""
(Right { name: "Pat", title: Nothing })

> parse''' """{"name": "Pat", "title": "wonderful"}"""
(Right { name: "Pat", title: (Just "wonderful") })

> Data.Argonaut.Core.stringify (encode''' { name: "Pat", title: Data.Maybe.Nothing })
"{\"name\":\"Pat\"}"

> Data.Argonaut.Core.stringify (encode''' { name: "Pat", title: Data.Maybe.Just "Dr." })
"{\"name\":\"Pat\",\"title\":\"Dr.\"}"

How To: Decode and Encode JSON with required and optional values in purescript-simple-json

Another common pattern with JSON objects is that some keys always have to be present while others do not. Some APIs make the distinction between a JSON object like { "name": "Pat" } and one like { "name": "Pat", "title": null }. In the first case, it might recognize that the "title" key does not exist, and behave in a different way from the "title" key having a value of null. In the second case, it might notice that the "title" key exists and work with the value assuming it's good to go; the null might eventually cause a failure later.

In many cases, what we want is to not generate any fields that do not exist. Using purescript-simple-json, Option.Record _ _ can help with that idea:

import Prelude
import Data.Either as Data.Either
import Data.Semigroup.Foldable as Data.Semigroup.Foldable
import Foreign as Foreign
import Option as Option
import Simple.JSON as Simple.JSON

parse ::
  String ->
  Data.Either.Either String (Option.Record ( name :: String ) ( title :: String ))
parse string = case readJSON string of
  Data.Either.Left errors -> Data.Either.Left (Data.Semigroup.Foldable.intercalateMap " " Foreign.renderForeignError errors)
  Data.Either.Right record -> Data.Either.Right record

readJSON ::
  String ->
  Simple.JSON.E (Option.Record ( name :: String ) ( title :: String ))
readJSON = Simple.JSON.readJSON

writeJSON ::
  Option.Record ( name :: String ) ( title :: String ) ->
  String
writeJSON = Simple.JSON.writeJSON

We can give that a spin with some different JSON values:

> parse """{}"""
(Left "Error at property \"name\": Type mismatch: expected String, found Undefined")

> parse """{"title": "wonderful"}"""
(Left "Error at property \"name\": Type mismatch: expected String, found Undefined")

> parse """{"name": "Pat"}"""
(Right (Option.recordFromRecord { name: "Pat" }))

> parse """{"name": "Pat", "title": "Dr."}"""
(Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))

> parse """{ "name": "Pat", "title": null }"""
Right (Option.recordFromRecord { name: "Pat" })

We can also produce some different JSON values:

> writeJSON (Option.recordFromRecord {name: "Pat"})
"{\"name\":\"Pat\"}"

> writeJSON (Option.recordFromRecord {name: "Pat", title: "Dr."})
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

Notice that we don't end up with a "title" field in the JSON output unless we have a title field in our record.

It might be instructive to compare how we might write a similar functions using a Record _ instead of Option.Record _ _: With purescript-simple-json, the instances for decoding and encoding on records handle Data.Maybe.Maybe _ values like they are optional. If we attempt to go directly to Record ( name :: String, title :: Data.Maybe.Maybe String ):

import Prelude
import Data.Either as Data.Either
import Data.Maybe as Data.Maybe
import Data.Semigroup.Foldable as Data.Semigroup.Foldable
import Foreign as Foreign
import Simple.JSON as Simple.JSON

parse' ::
  String ->
  Data.Either.Either String (Record ( name :: String, title :: Data.Maybe.Maybe String ))
parse' string = case readJSON string of
  Data.Either.Left errors -> Data.Either.Left (Data.Semigroup.Foldable.intercalateMap " " Foreign.renderForeignError errors)
  Data.Either.Right record -> Data.Either.Right record

readJSON' ::
  String ->
  Simple.JSON.E (Record ( name :: String, title :: Data.Maybe.Maybe String ))
readJSON' = Simple.JSON.readJSON

writeJSON' ::
  Record ( name :: String, title :: Data.Maybe.Maybe String ) ->
  String
writeJSON' = Simple.JSON.writeJSON

We get the behavior we expect:

> parse' """{}"""
(Left "Error at property \"name\": Type mismatch: expected String, found Undefined")

> parse' """{"title": "wonderful"}"""
(Left "Error at property \"name\": Type mismatch: expected String, found Undefined")

> parse' """{"name": "Pat"}"""
(Right { name: "Pat", title: Nothing })

> parse' """{"name": "Pat", "title": "Dr."}"""
(Right { name: "Pat", title: (Just "Dr.") })

> writeJSON' {name: "Pat", title: Data.Maybe.Nothing}
"{\"name\":\"Pat\"}"

> writeJSON' {name: "Pat", title: Data.Maybe.Just "Dr."}
"{\"title\":\"Dr.\",\"name\":\"Pat\"}"

How To: Provide an easier API for DateTime

The API for Data.DateTime is pretty nice because it means we cannot construct invalid dates. What's not so nice about it is that it pushes all of the correctness onto us. It might be a little easier to use if the API would allow optional values to be passed in and default to something sensible. For instance, constructing a Data.DateTime.DateTime can be done by passing in both a Data.Date.Date and a Data.Time.Time: The issue is, how do we construct a Data.Date.Date or Data.Time.Time.

One way to get construct these values is to use the Data.Enum.Enum instance for both of them:

> Data.DateTime.DateTime bottom bottom
(DateTime (Date (Year -271820) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0)))

This gives us a value, but some of its parts might not be what we really want; e.g. the year is -271820.

If we wanted to alter the year, we have to use Data.Enum.toEnum to construct a different year, then Data.DateTime.modifyDate, and Data.Date.canonicalDate to thread the year through:

> Data.DateTime.modifyDate (\date -> Data.Date.canonicalDate (Data.Maybe.fromMaybe bottom (Data.Enum.toEnum 2019)) (Data.Date.month date) (Data.Date.day date)) (Data.DateTime.DateTime bottom bottom)
(DateTime (Date (Year 2019) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0)))

Or create it with the correct year from the get-go:

> Data.DateTime.DateTime (Data.Date.canonicalDate (Data.Maybe.fromMaybe bottom (Data.Enum.toEnum 2019)) bottom bottom) bottom
(DateTime (Date (Year 2019) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0)))

That's a non-trivial amount of work in order to alter the year. We can clean it up with a named function that takes in an Int for the year and does all the boilerplate (using Data.Enum.toEnumWithDefaults to handle bounds a bit better):

dateTimeFromYear :: Int -> Data.DateTime.DateTime
dateTimeFromYear year =
  Data.DateTime.DateTime
    ( Data.Date.canonicalDate
        (Data.Enum.toEnumWithDefaults bottom top year)
        bottom
        bottom
    )
    bottom

This works decently for the year alone.

> dateTimeFromYear 2019
(DateTime (Date (Year 2019) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0)))

Once we decide we are okay with the year, but want to alter the day instead, or that we want to alter both at the same time it becomes just as hard as if we hadn't done anything. We either need to implement something similar for each part or change the arguments to Data.Maybe.Maybe Ints.

An alternative is to use an option for each part of the Data.DateTime.DateTime:

type Option
  = ( day :: Int
    , hour :: Int
    , millisecond :: Int
    , minute :: Int
    , month :: Data.Date.Component.Month
    , second :: Int
    , year :: Int
    )

Then we can build a Data.DateTime.DateTime from whatever happens to be passed in:

dateTime ::
  forall record.
  Option.FromRecord record Option =>
  Record record ->
  Data.DateTime.DateTime
dateTime record = Data.DateTime.DateTime date time
  where
  date :: Data.Date.Date
  date = Data.Date.canonicalDate year month day
    where
    day :: Data.Date.Component.Day
    day = get (Data.Symbol.SProxy :: _ "day")

    month :: Data.Date.Component.Month
    month = Option.getWithDefault bottom (Data.Symbol.SProxy :: _ "month") options

    year :: Data.Date.Component.Year
    year = get (Data.Symbol.SProxy :: _ "year")

  get ::
    forall label proxy record' value.
    Data.Enum.BoundedEnum value =>
    Data.Symbol.IsSymbol label =>
    Prim.Row.Cons label Int record' Option =>
    proxy label ->
    value
  get proxy = case Option.get proxy options of
    Data.Maybe.Just x -> Data.Enum.toEnumWithDefaults bottom top x
    Data.Maybe.Nothing -> bottom

  options :: Option.Option Option
  options = Option.fromRecord record

  time :: Data.Time.Time
  time = Data.Time.Time hour minute second millisecond
    where
    hour :: Data.Time.Component.Hour
    hour = get (Data.Symbol.SProxy :: _ "hour")

    minute :: Data.Time.Component.Minute
    minute = get (Data.Symbol.SProxy :: _ "minute")

    millisecond :: Data.Time.Component.Millisecond
    millisecond = get (Data.Symbol.SProxy :: _ "millisecond")

    second :: Data.Time.Component.Second
    second = get (Data.Symbol.SProxy :: _ "second")

Now, we can construct a Data.DateTime.DateTime fairly easily:

> dateTime {}
(DateTime (Date (Year -271820) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0)))

We can alter the year:

> dateTime {year: 2019}
(DateTime (Date (Year 2019) January (Day 1)) (Time (Hour 0) (Minute 0) (Second 0) (Millisecond 0)))

And, we can alter any of the components:

> dateTime {minute: 30, month: Data.Date.Component.April, year: 2019}
(DateTime (Date (Year 2019) April (Day 1)) (Time (Hour 0) (Minute 30) (Second 0) (Millisecond 0)))

Reference: FromRecord _ _ _

A typeclass for converting a Record _ into an Option _.

An instance FromRecord record required optional states that we can make a Record required and an Option optional from a Record record where every required field is in the record and the rest of the present fields in the record is present in the option. E.g. FromRecord () () ( name :: String ) says that the Record () has no fields and the Option ( name :: String ) will have no value; FromRecord ( name :: String ) () ( name :: String ) says that the Record () has no fields and the Option ( name :: String ) will have the given name value; FromRecord ( name :: String ) ( name :: String ) () says that the Record ( name :: String ) has the given name value and the Option () will have no value; FromRecord () ( name :: String) () is a type error since the name field is required but the given record lacks the field.

Since there is syntax for creating records, but no syntax for creating options, this typeclass can be useful for providing an easier to use interface to options.

E.g. Someone can say:

Option.fromRecord' { foo: true, bar: 31 }

Instead of having to say:

Option.insert
  (Data.Symbol.SProxy :: _ "foo")
  true
  ( Option.insert
      (Data.Symbol.SProxy :: _ "bar")
      31
      Option.empty
  )

Not only does it save a bunch of typing, it also mitigates the need for a direct dependency on SProxy _.

purescript-option's People

Contributors

bbarker avatar joneshf avatar seanyu4296 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

purescript-option's Issues

Documentation for alter function

Currently the alter function says:

Manipulates the values of an option.
If the field exists in the option, the given function is applied to the value.
If the field does not exist in the option, there is no change to the option.

I don't believe this is accurate. From my testing:

someOption :: Option ( foo :: Boolean, bar :: Int )
someOption = insert (Proxy :: _ "bar") 31 empty

anotherOption :: Option.Option ( foo :: Boolean, bar :: Int )
anotherOption = Option.alter { foo: (\(_ :: Maybe Boolean) -> Just false), bar: (\(_ :: Maybe Int) -> Just 41) } someOption

> anotherOption 
(Option.fromRecord { bar: 41, foo: false })

So it seems alter is able to adjust fields that don't exist in the option. I think the existing documentation reflects the modify' function behavior instead.

Defining setMay

I'm curious if it is possible to have a function like setMay:

setMay ::
  forall label option option' option'' proxy value.
  IsSymbol label =>
  Prim.Row.Cons label value option'' option =>
  Prim.Row.Cons label value' option'' option' =>
  proxy label ->
  Maybe value ->
  Opt.Option option' ->
  Opt.Option option
setMay proxy vMay def = case vMay of
  Just v -> Opt.set proxy v def
  Nothing -> def

The immediate issue is that it is defined in terms of set, which assumes that the output option type differs from the input option type, but this usage would require them to be the same. However, this would violate the Cons constraints, also required by set.

I also tried to do this directly in terms of modify, but of course, value is still different from value':

setMay ::
  forall label option option' option'' proxy value value'.
  IsSymbol label =>
  Prim.Row.Cons label value option'' option =>
  Prim.Row.Cons label value' option'' option' =>
  proxy label ->
  Maybe value ->
  Opt.Option option' ->
  Opt.Option option
setMay proxy vMay def = Opt.modify proxy go def
  where
  go optVal = case vMay of
    Just v -> v
    Nothing -> optVal

Question on using ToRecord

In the following example, I get an instance not found error for ToRecordOption:

type MyRecRows  = (foo:: Int, bar:: String)
type MyRec = Record MyRecRows

recTest :: Opt.Option MyRecRows -> String
recTest myRecOpt = (show fooInt)
  <> "\n"
  <> (show $ Opt.get(SProxy :: _ "bar") myRecOpt)
  where
    fooInt :: Int
    fooInt = ((Opt.toRecord' myRecOpt) :: MyRec).foo

One point of confusion is that I think this should be returning a "maybe-fied" record, so I should really have fooInt :: Maybe Int:

recTest :: Opt.Option MyRecRows -> String
recTest myRecOpt = (show fooInt)
  <> "\n"
  <> (show $ Opt.get(SProxy :: _ "bar") myRecOpt)
  where
    fooInt :: Maybe Int
    fooInt = (Opt.toRecord' myRecOpt).foo

But the error becomes worse:

    Option.ToRecordOption t3
                          ( bar :: String
                          , foo :: Int
                          )
                          ( foo :: Maybe Int
                          | t0
                          )

Is there a suggested way around this problem?

Also, I wonder if it might be possible to have another function, toRecordMay or some such, that goes from Option opt -> Maybe rec, such that we get a populated record only if every field of the record (or, perhaps easier to reason about: every field in the option) is not Nothing*, otherwise just return Nothing. I think this could be highly useful for accumulating state incrementally.

*One exception is that if some fields of the output record are themselves Maybe values, but really we would still be requiring a value is Just x where x might be Nothing. Maybe this is worthy of its own issue to discuss separately.

PureScript 0.15 support

Would you accept a PR for changes to support PureScript v0.15? I think the biggest change is swapping out RLProxy for just Proxy (and I would think making Proxy used concretely everywhere instead of a type parameter now that it is the canonical proxy type)

Provide a way to get any values that exist in an `Option.Option _` or none at all

The idea

I was talking to @gabejohnson the other day about using purescript-option to solve a problem. It came up that it'd be ideal if we could take an Option.Option _, and convert it to Data.Maybe.Just _ if any of the values exist or Data.Maybe.Nothing if none of them do. @gabejohnson mentioned that it'd be akin to Option.getAll, but only return Data.Maybe.Nothing if no values are there (as opposed to at least one value not being there). @gabejohnson also suggested Option.getSome as the name of the value.

The problem

While it seemed feasible at first, I think it's a value that would end up being hard to use. An example might help. Let's say we have:

type Greeting
  = Option.Option
      ( name :: String
      , title :: String
      )

What we want is a family of functions:

getSome ::
  Greeting ->
  Data.Maybe.Maybe
    {
    }
getSome ::
  Greeting ->
  Data.Maybe.Maybe
    { name :: String
    }
getSome ::
  Greeting ->
  Data.Maybe.Maybe
    { title :: String
    }
getSome ::
  Greeting ->
  Data.Maybe.Maybe
    { name :: String
    , title :: String
    }

We can probably write a typeclass and instance(s) for this family of functions. The hard part is using it. If we were to say:

greet ::
  Greeting ->
  Data.Maybe.Maybe ?option
greet option = Option.getSome option

What would we expect ?option to be? There's four valid choices for it, and if we choose the wrong one, we'll might get a Data.Maybe.Nothing when we didn't expect it . That's not really what we wanted. We wanted to take any Option.Option _, and only get a Data.Maybe.Nothing if none of the values were there.

The real implementation?

It almost seems like what we want is something like:

getSome ::
  Greeting ->
  Data.Maybe.Maybe
    ( Data.Variant.Variant
        ( name :: String
        , name_title ::
            { name :: String
            , title :: String
            }
        , title :: String
        )
    )

This way, we'll at least have a single type that is always the same. You'd be able to discriminate the cases dynamically instead of having to take a guess statically.

An alternative implementation

Instead of creating that family of functions, we can throw more Data.Maybe.Maybe _s in the mix and have a single function:

getSome ::
  Greeting ->
  Maybe
    { name :: Maybe String
    , title :: Maybe String
    }

I think this is actually more inline with the specific example @gabejohnson was dealing with. That seems like something we could throw together immediately as:

getSome ::
  forall option record.
  ToRecord option record =>
  Option option ->
  Data.Maybe.Maybe (Record record)
getSome option@(Option object)
  | Foreign.Object.isEmpty object = Data.Maybe.Nothing
  | otherwise = Data.Maybe.Just (toRecord option)

We can open up the Option.Option _ and check if the underlying Foreign.Object.Object _ is empty. If it is, there's no values, so we can return Data.Maybe.Nothing. Otherwise, we grab what we can.

My main qualm with this implementation is that anyone consuming it still has to handle the Data.Maybe.Just { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing } case. That doesn't seem like it should be a possible value, but the types still allow it.

The current workaround

Assuming the Option.Option _ can have a Data.Eq.Eq _ instance, there's a way to get the alternative implementation without adding something to the Option module. Anyone can write the following value external to the Option module:

getSome ::
  forall option record.
  Data.Eq.Eq (Option.Option option) =>
  Option.ToRecord option record =>
  Option.Option option ->
  Data.Maybe.Maybe (Record record)
getSome option
  | option == Option.empty = Data.Maybe.Nothing
  | otherwise = Data.Maybe.Just (Option.toRecord option)

If the Option.Option _ has values without a Data.Eq.Eq _ instance, they can't use that value. It's not an end-all, but it should unstick while we try to figure things out here.

What to do?

I'd like to sit on this for a while. Each of the implementations above come with their own set of issues: hard to choose a type, illegal states represented, additional constraints.

A PR to be PureScript 0.14.x compatible?

Hello!

Would you be interested in a PR bumping purescript-option to be compatible with the 0.14.x compiler?

The required changes as I see it would be:

  • Row kinds: The replacement of # with Row
  • RowList: The addition of the Type parameter to Prim.RowList.RowList
  • The Foreign.Index/Indexable change: The compiler needs a bit of help in the ReadForeignOption Prim.RowList.Nil instance because of the change from Index i to Index i m.

Optional at this time would be updating the uses of Data.Symbol.SProxy/etc. to the new Type.Proxy.Proxy.

I've done the above already for a copy of the library internal to a project of mine and would be happy to contribute it back. No problem if not, though. :)

Cheers either way for the cool library!

Proposal: getAll alternative that returns an Either with errors (missing required fields)

In certain cases, it may be useful to quickly ascertain why a Record wasn't obtained, rather than just having Nothing. So instead of just:

getAll :: forall option record. GetAll option record => Option option -> Maybe (Record record)

It may be useful to have something like:

getAllWithError :: forall option record. GetAll option record => Option option -> Either (Tree String) (Record record)

In this case, the Left Tree would ideally be a sort of "stack trace" (except it is an Option trace here) that includes the fields that were absent. Here's an example mockup (if I've got this right):

type Inner = Option.Record ( reqInner :: String ) ( optInner :: Number) 
type Outer = Option.Record ( reqOuter :: Inner ) ( optOuter :: String ) 

If getAllWithError was called on Option.Empty, it would ideally return something like Left (Node { children : [Node {children: [], value: "reqInner"}], value : "reqOuter" }.

Is it possible to have a mix of required and optional rows in Record?

Thanks for the awesome library. I have some code like this. May I ask if it is possible to have a mix of required and optional rows in Record?

import Prelude
import Data.Maybe (Maybe(..))
import Data.Symbol (SProxy(..))
import Option as Option
import Type.Row (type (+))

type Req r
  = ( surname :: String | r )

greeting ::
  forall record.
  Option.FromRecord record ( name :: String, title :: String ) =>
  Record (Req + record) ->
  String
greeting record = "Hello, " <> name' <> record.surname
  where
  option :: Option.Option ( name :: String, title :: String )
  option = Option.fromRecord record

  name' :: String
  name' = case Option.get (SProxy :: _ "name") option of
    Just name -> name
    Nothing -> "World"

I get this error here

[1/1 NoInstanceFound] src/purs/AdminUi/Exp.purs:43:12

  43    option = Option.fromRecord record
                 ^^^^^^^^^^^^^^^^^^^^^^^^
  
  No type class instance was found for
  
    Option.FromRecordOption t2
                            ( surname :: String
                            | record3
                            )
                            ( name :: String
                            , title :: String
                            )
  

Drop support for `purescript-simple-json`

There's two reasons for dropping support for purescript-simple-json: the maintainer is actively abrasive, and it's not a "library."

The first point is a non-technical reason, but the more important reason. I personally don't feel like integrating with a package when that maintainer is so abrasive. I get that people have different ways of maintaining code. I mean, I don't accept contributions to purty because I can't deal with them. But, this sort of interaction is a bit much:

Supporting these sorts of interactions–even passively by keeping the integration in this package–just reinforces that this behavior is acceptable. And nobody should find this sort of behavior acceptable; I sure don't. This is motivation enough to drop support.

The second point is a technical reason not to support purescript-simple-json. The intent of supporting different packages is to make it easier to use them with this package. But, that's not really the way purescript-simple-json is supposed to be used. It's got a different workflow it pushes, and that's fine. That workflow doesn't really line up with how things work in this package though.

This is a breaking change, since we're dropping support for an entire package. So, we'll have to do a major release.

Discuss possible PR for projection

Now that I'm back to using Option I realized I needed another function, project. I also combined this with the recently added (in PR) getAll to get a convenience function, getSubset. It works well, but there are a few possible discussion points I can think of already:

  1. Is there a way to implement project without resorting to the FFI (the unexported utility function pickFn)? This is based almost exactly on pickFn from Record-Extra, but an undefined guard is needed here since Foreign.Object can have missing values, whereas a Record cannot. In any case I tried a few things to this end, namely using alter, but ran out of steam trying to massage the types. Denis Stoyanov had this note on Slack, which I'm not sure I follow exactly:

    I use it like newtype MyRecord (r :: # Type) = MyRecord (FO.Object (Exists Identity)) but just for example what is record will be

  2. I'm relying on Records.Extra just to get the Keys class and related functions, but we might want to reconsider this.
  3. Of course add docs when the dust settles.

Here is the branch and commit as of this posting.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.