How to handle optional fields in PureScript

Zelenya - Jul 14 '23 - - Dev Community

šŸ“¹Ā 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'/>
Enter fullscreen mode Exit fullscreen mode

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/>
Enter fullscreen mode Exit fullscreen mode

šŸ’” 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 }
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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

// Problems.js
export { default as reactPlayerImpl } from 'react-player';
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Zero cost coercion

The library provides Opt exactly for our use cases:

type ReactPlayerProps = { url :: String, light :: Opt Boolean }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode
  • 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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

[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 and Open. 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
Enter fullscreen mode Exit fullscreen mode

And adopt the props type:

type ReactPlayerProps = { url :: String, light :: Opt Mode }
Enter fullscreen mode Exit fullscreen 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 }
Enter fullscreen mode Exit fullscreen mode

We can glue the two by adopting the constructor function.

import Data.Undefined.NoProblem (pseudoMap)
Enter fullscreen mode Exit fullscreen mode
reactPlayer :: forall p. Coerce p ReactPlayerProps => p -> JSX
reactPlayer props = 
  element reactPlayerImpl 
    $ modify (Proxy :: _ "light") (pseudoMap toBoolean) -- [2]
    $ coerce props                                      -- [1]
Enter fullscreen mode Exit fullscreen mode

[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'/>
Enter fullscreen mode Exit fullscreen mode

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:

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:

<ReactPlayer url='https://www.youtube.com/watch?v=ysz5S6PUM-U' light=true/>
Enter fullscreen mode Exit fullscreen mode

šŸ’” 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 }
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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

// Problems.js
export { default as reactPlayerImpl } from 'react-player';
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Zero cost coercion

The library provides Opt exactly for our use cases:

type ReactPlayerProps = { url :: String, light :: Opt Boolean }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode
  • 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 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

[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 and Open. 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
Enter fullscreen mode Exit fullscreen mode

And adopt the props type:

type ReactPlayerProps = { url :: String, light :: Opt Mode }
Enter fullscreen mode Exit fullscreen 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 }
Enter fullscreen mode Exit fullscreen mode

We can glue the two by adopting the constructor function.

import Data.Undefined.NoProblem (pseudoMap)
Enter fullscreen mode Exit fullscreen mode
reactPlayer :: forall p. Coerce p ReactPlayerProps => p -> JSX
reactPlayer props = 
  element reactPlayerImpl 
    $ modify (Proxy :: _ "light") (pseudoMap toBoolean) -- [2]
    $ coerce props                                      -- [1]
Enter fullscreen mode Exit fullscreen mode

[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.



. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player