How to handle optional fields in PureScript
Zelenya
Posted on July 14, 2023
📹 Hate reading articles? Check out the complementary video, which covers the same content.
This tutorial shows how to handle optional fields using the undefined-is-not-a-problem library.
Why?
An ergonomic way of working with optional record fields is especially useful when interacting with JavaScript (aka FFI).
Imagine we want to integrate react-player into our app. The docs show that we can create a basic react player component by passing a url
:
<ReactPlayer url='https://www.youtube.com/watch?v=ysz5S6PUM-U'/>
But then, if we scroll down to the Props (properties) section, we see many other properties, which also have default values.
In the wild-west JavaScript, we don’t have to worry about any of these – we pass values we care about and ignore the rest. For example, we can set light
to true
:
<ReactPlayer url='https://www.youtube.com/watch?v=ysz5S6PUM-U' light=true/>
đź’ˇ The
light
prop will render a video thumbnail with a simple play icon and only load the full player once a user has interacted with the image.
But what about the PureScript world – where we must be strict about types?
Optional values in PureScript
First, we can also ignore all the fields:
type ReactPlayerProps = { url :: String }
This is a fine type, which we can use to interact with the react-player library and create player components with any url, while everything else stays the default.
props :: ReactPlayerProps
props = { url: "https://youtu.be/tIUXB0TrlpU" }
What if we want to have an option to set the light
property?
💡 Note that there is no
null
,undefined
, or anything like that in the PureScript standard library.
If you have an FP background, you might reach for option type or maybe type.
type ReactPlayerProps = { url :: String, light :: Maybe Boolean }
But this doesn’t come for free: it has a runtime cost (it’s not a js primitive) and a developer cost (you can’t omit these properties – you have to pass Nothing
explicitly), which is annoying, especially for large records.
prop1 :: ReactPlayerProps
prop1 = { url: "...", light: Nothing }
prop2 :: ReactPlayerProps
prop2 = { url: "...", light: Just true }
Luckily there is undefined-is-not-a-problem.
Undefined is not a problem
The undefined-is-not-a-problem library unlocks a neat way to handle optional record fields using undefined | a
values and typesafe zero-cost coercion.
đź’ˇÂ
undefined | a
is an untagged union – the value is eitherundefined
or has typea
.
(Ignore unless you’re following at home)
JS dependencies: react
, react-dom
, react-player
.
PureScript dependencies: react-basic-dom
, react-basic-hooks
, undefined-is-not-a-problem
, and whatever they bring.
JS file:
// Problems.js
export { default as reactPlayerImpl } from 'react-player';
PureScript file, imports:
-- Problems.purs
module Problems where
import Data.Undefined.NoProblem (Opt, opt)
import Data.Undefined.NoProblem.Closed (class Coerce, coerce)
import React.Basic.Hooks (JSX, ReactComponent, element)
Zero cost coercion
The library provides Opt
exactly for our use cases:
type ReactPlayerProps = { url :: String, light :: Opt Boolean }
Optional field value (Opt a
) is just a value, and there are two ways to create it: undefined
or opt
.
example1 :: ReactPlayerProps
example1 = { url: "...", light: undefined }
example2 :: ReactPlayerProps
example2 = { url: "...", light: opt true }
undefined
is JavaScript’s undefined
, and opt
calls PureScript’s coerce function. This means we don’t pay for these. There is no wrapping or unwrapping. Primitive, plain JavaScript.
However, we still explicitly pass undefined
and call opt
by hand, which could be better. Let’s deal with it next.
Boilerplate coercion
This is a typical FFI boilerplate that I use:
type ReactPlayerProps = { url :: String, light :: Opt Boolean }
foreign import reactPlayerImpl :: ReactComponent ReactPlayerProps
-- [3] [4]
reactPlayer :: forall p. Coerce p ReactPlayerProps => p -> JSX
reactPlayer props = element reactPlayerImpl (coerce props) -- [5]
- Create a type for the component props with required field(s) and optional field(s) that matter.
- Import foreign component using the props type and call it something suffixed with
Impl
. - Create a function to construct the component – it takes props and returns a
JSX
(rendered React VDOM).
Let’s see how it can be used and then how it works.
player1 :: JSX
player1 = reactPlayer { url: "https://youtu.be/tIUXB0TrlpU" }
player2 :: JSX
player2 = reactPlayer { url: "https://youtu.be/tIUXB0TrlpU", light: false }
No boilerplate! We can ignore optional fields and use straightforward types to set them (no opt
, no Just
, etc.). Note that required fields are still required!
player3 = reactPlayer {}
-- Compilation error: Missing required field: url
How it works
Here is the component constructor once again:
-- [2]
reactPlayer :: forall p. Coerce p ReactPlayerProps => p -> JSX
reactPlayer props = element reactPlayerImpl (coerce props) -- [1]
[1] coerce
fills the missing fields in a given record and transforms values to Opt
if needed. This happens on the type level. [2] The Coerce
 constraint is to check/prove if it’s safe to coerce
.
💡 Note the imports we’re using:
import Data.Undefined.NoProblem.Closed (class Coerce, coerce)
The library also provides two coercing strategies:
Closed
andOpen
. They share the interface but differ in instance chains, which results in slightly different behaviors. Check out the docs when/if you’re curious.
And that’s the core of it.
[Bonus] Additional type-safety
There are cases when we want even more type-safety.
For example, I’m not a fan of booleans, so I might create a proper type to use in PureScript:
data Mode = Light | Full
toBoolean :: Mode -> Boolean
toBoolean = case _ of
Light -> true
Full -> false
And adopt the props type:
type ReactPlayerProps = { url :: String, light :: Opt Mode }
Good for me, but the JavaScript library still expects a boolean. We must ffi the old props with a boolean:
foreign import reactPlayerImpl :: ReactComponent { url :: String, light :: Opt Boolean }
We can glue the two by adopting the constructor function.
import Data.Undefined.NoProblem (pseudoMap)
reactPlayer :: forall p. Coerce p ReactPlayerProps => p -> JSX
reactPlayer props =
element reactPlayerImpl
$ modify (Proxy :: _ "light") (pseudoMap toBoolean) -- [2]
$ coerce props -- [1]
[1] We coerce
all the props as we did before and then [2] map the light
value to its boolean representation using pseudoMap
.
💡 We use the
Record
module to modify a property for a label specified using a value-level proxy for a type-level string.
Footer
With a little prep work, we get a nice way of working with optional values. Outside the component module, we use straightforward types and don’t worry about unnecessary optional fields.
📹 Hate reading articles? Check out the complementary video, which covers the same content.
This tutorial shows how to handle optional fields using the undefined-is-not-a-problem library.
Why?
An ergonomic way of working with optional record fields is especially useful when interacting with JavaScript (aka FFI).
Imagine we want to integrate react-player into our app. The docs show that we can create a basic react player component by passing a url
:
<ReactPlayer url='https://www.youtube.com/watch?v=ysz5S6PUM-U'/>
But then, if we scroll down to the Props (properties) section, we see many other properties, which also have default values. Here is a preview:
In the wild-west JavaScript, we don’t have to worry about any of these – we pass values we care about and ignore the rest. For example, we can set light
to true
:
<ReactPlayer url='https://www.youtube.com/watch?v=ysz5S6PUM-U' light=true/>
đź’ˇ The
light
prop will render a video thumbnail with a simple play icon and only load the full player once a user has interacted with the image.
But what about the PureScript world – where we must be strict about types?
Optional values in PureScript
First, we can also ignore all the fields:
type ReactPlayerProps = { url :: String }
This is a fine type, which we can use to interact with the react-player library and create player components with any url, while everything else stays the default.
props :: ReactPlayerProps
props = { url: "https://youtu.be/tIUXB0TrlpU" }
What if we want to have an option to set the light
property?
💡 Note that there is no
null
,undefined
, or anything like that in the PureScript standard library.
If you have an FP background, you might reach for option type or maybe type.
type ReactPlayerProps = { url :: String, light :: Maybe Boolean }
But this doesn’t come for free: it has a runtime cost (it’s not a js primitive) and a developer cost (you can’t omit these properties – you have to pass Nothing
explicitly), which is annoying, especially for large records.
prop1 :: ReactPlayerProps
prop1 = { url: "...", light: Nothing }
prop2 :: ReactPlayerProps
prop2 = { url: "...", light: Just true }
Luckily there is undefined-is-not-a-problem.
Undefined is not a problem
The undefined-is-not-a-problem library unlocks a neat way to handle optional record fields using undefined | a
values and typesafe zero-cost coercion.
đź’ˇÂ
undefined | a
is an untagged union – the value is eitherundefined
or has typea
.
(Ignore unless you’re following at home)
JS dependencies: react
, react-dom
, react-player
.
PureScript dependencies: react-basic-dom
, react-basic-hooks
, undefined-is-not-a-problem
, and whatever they bring.
JS file:
// Problems.js
export { default as reactPlayerImpl } from 'react-player';
PureScript file, imports:
-- Problems.purs
module Problems where
import Data.Undefined.NoProblem (Opt, opt)
import Data.Undefined.NoProblem.Closed (class Coerce, coerce)
import React.Basic.Hooks (JSX, ReactComponent, element)
Zero cost coercion
The library provides Opt
exactly for our use cases:
type ReactPlayerProps = { url :: String, light :: Opt Boolean }
Optional field value (Opt a
) is just a value, and there are two ways to create it: undefined
or opt
.
example1 :: ReactPlayerProps
example1 = { url: "...", light: undefined }
example2 :: ReactPlayerProps
example2 = { url: "...", light: opt true }
undefined
is JavaScript’s undefined
, and opt
calls PureScript’s coerce function. This means we don’t pay for these. There is no wrapping or unwrapping. Primitive, plain JavaScript.
However, we still explicitly pass undefined
and call opt
by hand, which could be better. Let’s deal with it next.
Boilerplate coercion
This is a typical FFI boilerplate that I use:
type ReactPlayerProps = { url :: String, light :: Opt Boolean }
foreign import reactPlayerImpl :: ReactComponent ReactPlayerProps
-- [3] [4]
reactPlayer :: forall p. Coerce p ReactPlayerProps => p -> JSX
reactPlayer props = element reactPlayerImpl (coerce props) -- [5]
- Create a type for the component props with required field(s) and optional field(s) that matter.
- Import foreign component using the props type and call it something suffixed with
Impl
. - Create a function to construct the component – it takes props and returns a
JSX
(rendered React VDOM).
Let’s see how it can be used and then how it works.
player1 :: JSX
player1 = reactPlayer { url: "https://youtu.be/tIUXB0TrlpU" }
player2 :: JSX
player2 = reactPlayer { url: "https://youtu.be/tIUXB0TrlpU", light: false }
No boilerplate! We can ignore optional fields and use straightforward types to set them (no opt
, no Just
, etc.). Note that required fields are still required!
player3 = reactPlayer {}
-- Compilation error: Missing required field: url
How it works
Here is the component constructor once again:
-- [2]
reactPlayer :: forall p. Coerce p ReactPlayerProps => p -> JSX
reactPlayer props = element reactPlayerImpl (coerce props) -- [1]
[1] coerce
fills the missing fields in a given record and transforms values to Opt
if needed. This happens on the type level. [2] The Coerce
 constraint is to check/prove if it’s safe to coerce
.
💡 Note the imports we’re using:
import Data.Undefined.NoProblem.Closed (class Coerce, coerce)
The library also provides two coercing strategies:
Closed
andOpen
. They share the interface but differ in instance chains, which results in slightly different behaviors. Check out the docs when/if you’re curious.
And that’s the core of it.
[Bonus] Additional type-safety
There are cases when we want even more type-safety.
For example, I’m not a fan of booleans, so I might create a proper type to use in PureScript:
data Mode = Light | Full
toBoolean :: Mode -> Boolean
toBoolean = case _ of
Light -> true
Full -> false
And adopt the props type:
type ReactPlayerProps = { url :: String, light :: Opt Mode }
Good for me, but the JavaScript library still expects a boolean. We must ffi the old props with a boolean:
foreign import reactPlayerImpl :: ReactComponent { url :: String, light :: Opt Boolean }
We can glue the two by adopting the constructor function.
import Data.Undefined.NoProblem (pseudoMap)
reactPlayer :: forall p. Coerce p ReactPlayerProps => p -> JSX
reactPlayer props =
element reactPlayerImpl
$ modify (Proxy :: _ "light") (pseudoMap toBoolean) -- [2]
$ coerce props -- [1]
[1] We coerce
all the props as we did before and then [2] map the light
value to its boolean representation using pseudoMap
.
💡 We use the
Record
module to modify a property for a label specified using a value-level proxy for a type-level string.
Footer
With a little prep work, we get a nice way of working with optional values. Outside the component module, we use straightforward types and don’t worry about unnecessary optional fields.
Posted on July 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.