How to handle optional fields in PureScript
Tags: functional, webdev, purescript, react
Published: 2023-07-14 02:52:43

📹 Hate reading articles? Check out [the complementary video](https://youtu.be/oRBySsofDTA), which covers the same content.

---

This tutorial shows how to handle optional fields using the [undefined-is-not-a-problem](https://github.com/paluh/purescript-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](https://book.purescript.org/chapter10.html)). 

Imagine we want to integrate [react-player](https://www.npmjs.com/package/react-player) into our app. The docs show that we can create a basic **react player** component by passing a `url`:

```jsx
<ReactPlayer url='https://www.youtube.com/watch?v=ysz5S6PUM-U'/>
```

But then, if we scroll down to the [Props](https://www.npmjs.com/package/react-player#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`:

```jsx
<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:

```haskell
type ReactPlayerProps = { url :: String }
```

This is a fine type, which we can use to interact with [the react-player](https://www.npmjs.com/package/react-player) library and create player components with any url, while everything else stays the default.

```haskell
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**.

```haskell
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.

```haskell
prop1 :: ReactPlayerProps
prop1 = { url: "...", light: Nothing }

prop2 :: ReactPlayerProps
prop2 = { url: "...", light: Just true }
```

Luckily there is [undefined-is-not-a-problem](https://github.com/paluh/purescript-undefined-is-not-a-problem).

## Undefined is not a problem

The [undefined-is-not-a-problem](https://github.com/paluh/purescript-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 either `undefined` or has type `a`.
> 

### (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:**

```jsx
// Problems.js
export { default as reactPlayerImpl } from 'react-player';
```

PureScript file, **imports**:

```haskell
-- 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:

```haskell
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`.

```haskell
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:

```haskell
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.

```haskell
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!

```haskell
player3 = reactPlayer {}
-- Compilation error: Missing required field: url
```

### How it works

Here is the component constructor once again:

```haskell
--                       [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:
> 
> 
> ```haskell
> import Data.Undefined.NoProblem.Closed (class Coerce, coerce)
> ```
> 
> The library also provides two coercing strategies: `Closed` and `Open`. They share the interface but differ in instance chains, which results in slightly different behaviors. Check out the [docs](https://github.com/paluh/purescript-undefined-is-not-a-problem#usage) 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:

```haskell
data Mode = Light | Full

toBoolean :: Mode -> Boolean
toBoolean = case _ of 
 Light -> true
 Full -> false
```

And adopt the props type:

```haskell
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:

```haskell
foreign import reactPlayerImpl :: ReactComponent { url :: String, light :: Opt Boolean }
```

We can glue the two by adopting the constructor function.

```haskell
import Data.Undefined.NoProblem (pseudoMap)
```

```haskell
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.

---

* [Library](https://github.com/paluh/purescript-undefined-is-not-a-problem)
* [PureScript React Kickstart](https://github.com/Zelenya/purescript-kickstarter)
* [react-player](https://www.npmjs.com/package/react-player)

--- 

📹 Hate reading articles? Check out [the complementary video](https://youtu.be/oRBySsofDTA), which covers the same content.

---

This tutorial shows how to handle optional fields using the [undefined-is-not-a-problem](https://github.com/paluh/purescript-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](https://book.purescript.org/chapter10.html)). 

Imagine we want to integrate [react-player](https://www.npmjs.com/package/react-player) into our app. The docs show that we can create a basic **react player** component by passing a `url`:

```jsx
<ReactPlayer url='https://www.youtube.com/watch?v=ysz5S6PUM-U'/>
```

But then, if we scroll down to the [Props](https://www.npmjs.com/package/react-player#Props) (properties) section, we see many other properties, which also have default values. Here is a preview:

![Screenshot 2023-07-11 at 17.28.24.png](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f864ea54-7b7a-4c71-aa81-449a1df997d6/Screenshot_2023-07-11_at_17.28.24.png)

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`:

```jsx
<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:

```haskell
type ReactPlayerProps = { url :: String }
```

This is a fine type, which we can use to interact with [the react-player](https://www.npmjs.com/package/react-player) library and create player components with any url, while everything else stays the default.

```haskell
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**.

```haskell
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.

```haskell
prop1 :: ReactPlayerProps
prop1 = { url: "...", light: Nothing }

prop2 :: ReactPlayerProps
prop2 = { url: "...", light: Just true }
```

Luckily there is [undefined-is-not-a-problem](https://github.com/paluh/purescript-undefined-is-not-a-problem).

## Undefined is not a problem

The [undefined-is-not-a-problem](https://github.com/paluh/purescript-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 either `undefined` or has type `a`.
> 

### (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:**

```jsx
// Problems.js
export { default as reactPlayerImpl } from 'react-player';
```

PureScript file, **imports**:

```haskell
-- 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:

```haskell
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`.

```haskell
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:

```haskell
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.

```haskell
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!

```haskell
player3 = reactPlayer {}
-- Compilation error: Missing required field: url
```

### How it works

Here is the component constructor once again:

```haskell
--                       [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:
> 
> 
> ```haskell
> import Data.Undefined.NoProblem.Closed (class Coerce, coerce)
> ```
> 
> The library also provides two coercing strategies: `Closed` and `Open`. They share the interface but differ in instance chains, which results in slightly different behaviors. Check out the [docs](https://github.com/paluh/purescript-undefined-is-not-a-problem#usage) 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:

```haskell
data Mode = Light | Full

toBoolean :: Mode -> Boolean
toBoolean = case _ of 
 Light -> true
 Full -> false
```

And adopt the props type:

```haskell
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:

```haskell
foreign import reactPlayerImpl :: ReactComponent { url :: String, light :: Opt Boolean }
```

We can glue the two by adopting the constructor function.

```haskell
import Data.Undefined.NoProblem (pseudoMap)
```

```haskell
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.

---

* [Library](https://github.com/paluh/purescript-undefined-is-not-a-problem)
* [PureScript React Kickstart](https://github.com/Zelenya/purescript-kickstarter)
* [react-player](https://www.npmjs.com/package/react-player)

--- 

{% embed https://youtu.be/oRBySsofDTA %}