Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh

OliverJAsh/foo.md

Last active Nov 10, 2020
Embed
What would you like to do?
`Option` vs non-`Option`

Option vs non-Option

Option<T> non-Option (T | undefined)
accessing property userOption.map(user => user.age) userNullish?.age
calling a method userOption.map(user => user.fn()) userNullish?.fn()
providing fallback ageOption.getOrElse(0) ageNullish ?? 0
filter ageOption.filter(checkIsOddNumber) ageNullish !== undefined && checkIsOddNumber(ageNullish) ? ageNullish : undefined
map ageOption.map(add1) ageNullish !== undefined ? add1(ageNullish) : undefined
flat map / chain ageOption.flatMap(add1) ageNullish !== undefined ? add1(ageNullish) : undefined
check for existence with predicate ageOption.exists(checkIsOddNumber) ageNullish !== undefined ? checkIsOddNumber(ageNullish) : false
check for existence with method nameOption.exists(name => name.startsWith('bob')) nameNullish?.startsWith('bob') ?? false
nesting Option<Option<T>> impossible
sequencing sequence(fa, fb) fa !== undefined ? fb !== undefined ? [fa, fb] : undefined : undefined
mapping multiple sequence(fa, fb).map(add) fa !== undefined ? fb !== undefined ? add([fa, fb]) : undefined : undefined

Comparison of advanced example

const age1 = userOption
  .flatMap(user => user.age)
  .map(plus1)
  .filter(checkIsOddNumber)
  .getOrElse(0);

const age2 =
  (user?.age !== undefined
    ? (() => {
        const agePlus1 = plus1(user.age);
        return checkIsOddNumber(agePlus1) ? agePlus1 : undefined;
      })()
    : undefined) ?? 0;
@tho-graf

This comment has been minimized.

Copy link

@tho-graf tho-graf commented Feb 20, 2020

7:0 for option 👍

@legzo

This comment has been minimized.

Copy link

@legzo legzo commented Feb 21, 2020

In your advanced examples, the resulting const would be an age and not an ageOption, would'nt it ? Because if the getOrElse().

@OliverJAsh

This comment has been minimized.

Copy link
Owner Author

@OliverJAsh OliverJAsh commented Feb 21, 2020

@legzo Fixed, thanks!

@hasparus

This comment has been minimized.

Copy link

@hasparus hasparus commented Feb 24, 2020

I'm not sure if anybody would write the non-option version.

const age3 = user?.age + 1 || 0;

const age3 = plus1(user?.age) || 0;

This is probably more difficult to understand than the Option example, because it requires to remember than NaN is falsy, but I have a feeling that a bunch of people used to JS would prefer it. I just wanted to point that non-Option example looks artificial IMHO.

@OliverJAsh

This comment has been minimized.

Copy link
Owner Author

@OliverJAsh OliverJAsh commented Feb 24, 2020

Your example is not logically equivalent—age should be treated as nullable, and we only want to call plus1 when it's non-nullable.

I wrote this gist on the assumption that you would never want to use boolean coercion (because of all the bugs it can lead to).

@OliverJAsh

This comment has been minimized.

Copy link
Owner Author

@OliverJAsh OliverJAsh commented Feb 24, 2020

Updated my example to use checkIsOddNumber instead of checkIsPositive, to demonstrate that this logical operation can't be replaced by boolean coercion.

@hasparus

This comment has been minimized.

Copy link

@hasparus hasparus commented Feb 24, 2020

Yeah sorry, I actually skipped the filter 😓 Negative values are truthy. There would need to be additional ternary and second call to plus1 or a partial result variable. My snippet was closer to this one, assuming you can pass undefined to plus1.

const age1 = userOption
    .flatMap(user => user.age)
    .map(plus1)
    .getOrElse(0);

This one should make more sense. Default parameters and destructuring are probably idiomatic JS.

interface User { age: number }

const plus1 = (x: number) => x + 1;
const checkIsOddNumber = (x: number) => x % 2 === 1;

const getAge = ({ age }: Partial<User> = {}) => {
    if (age === undefined) {
        return 0;
    }
    const agePlus1 = plus1(age);
    return checkIsOddNumber(agePlus1) ? agePlus1 : 0;
}

console.log(
    getAge(undefined), // 0
    getAge({}), // 0
    getAge({ age: 30 }), // 31
    getAge({ age: 31 }), // 0
)

One could argue that this doesn't require familiarity with any library.

I do believe that Option is very useful, especially because of how easy it is to move between Option and Either.
I just wanted to say that I find the second example (with IIFE) a bit artificial.

@OliverJAsh

This comment has been minimized.

Copy link
Owner Author

@OliverJAsh OliverJAsh commented Feb 24, 2020

Can you update your example so that user.age is nullable, like in my example? That will help us compare all of the different approaches.

@hasparus

This comment has been minimized.

Copy link

@hasparus hasparus commented Feb 25, 2020

Do you mean nullable as T | null or T | undefined?
Is it nullable in your example? Table head contains T | undefined, and we are (in both examples) using triple equal to undefined.

image

Do you mean that I should just drop Partial and change user to { age?: number }?

@OliverJAsh

This comment has been minimized.

Copy link
Owner Author

@OliverJAsh OliverJAsh commented Feb 25, 2020

I misread your example, it is already nullable (or "optional"). My bad!

@OliverJAsh

This comment has been minimized.

Copy link
Owner Author

@OliverJAsh OliverJAsh commented Feb 26, 2020

Btw I wrote it using the IIFE so I didn't have to repeat the default value (0), as shown in your example. We could avoid that with a constant, but it would be annoying if we had to extract a constant for default values everywhere they were used.

@slikts

This comment has been minimized.

Copy link

@slikts slikts commented Apr 6, 2020

A nice resource for building an intuition about why option types are useful is the Railway Oriented Programming talk.

@baetheus

This comment has been minimized.

Copy link

@baetheus baetheus commented Nov 10, 2020

I'm wondering if you can comment no the static-land modules implemented for type Nilable<T> = T | undefined | null from nilable and option in hkts (an fp-ts port to deno).

Option Nilable
accessing property userOption.map(user => user.age) pipe(userNilable, N.map(user => user.age))
calling a method userOption.map(user => user.fn()) pipe(userNilable, N.map(user => user.fn()))
providing fallback ageOption.getOrElse(0) pipe(userNilable, N.getOrElse(0))
filter ageOption.filter(checkIsOddNumber) N.Filterable.filter(checkIsOddNumber, userNilable)
map ageOption.map(add1) pipe(ageNilable, N.map(add1))
flat map / chain ageOption.flatMap(add1) pipe(ageNilable, N.chain(add1))
check for existence with predicate ageOption.exists(checkIsOddNumber) pipe(ageNilable, N.exists(checkIsOddNumber)) *unimplemented
check for existence with method nameOption.exists(name => name.startsWith('bob')) pipe(nameNilable, N.exists(name => name.startsWith('bob')) *unimplemented
nesting Option<Option> still impossible
sequencing sequence(fa, fb) N.sequenceTuple(ageNilable, nameNilable)
mapping multiple sequence(fa, fb).map(add) N.sequenceTuple(ageNilable1, ageNilable2).map(add)

Specifically, aside from being able to represent the different between [] and [undefined] when calling const head = <T>(ts: T[]): Nilable<T> => ts[0]; what are the type theoretical problems? (Even in this case the return value from head([]) and head([undefined]) are correct).

@OliverJAsh

This comment has been minimized.

Copy link
Owner Author

@OliverJAsh OliverJAsh commented Nov 10, 2020

@baetheus I've also explored an approach like nilables before but ultimately the problem is that it does not satisfy FP laws.

@hasparus attempted something similar here https://github.com/hasparus/maybe-ts, and wrote about his learnings here: https://haspar.us/speaking/maybe-ts.

@baetheus

This comment has been minimized.

Copy link

@baetheus baetheus commented Nov 10, 2020

@OliverJAsh Which laws does it not satisfy? I've created some assert statements that test instances of Functor, Apply, Applicative, and Monad and have tested my monad with them. It seems to pass for the simple valued case I setup. I've even run through a proof outline myself:

// Identity: F.map(x => x, a) ≡ a
map(identity, 1)
= isNil(1) ? undefined : identity(1)
= identity(1)
= 1

map(identity, undefined)
= isNil(undefined) ? undefined : identity(undefined)
= undefined

Ok!

// Composition: F.map(x => f(g(x)), a) ≡ F.map(f, F.map(g, a))
map(x => String(parseInt(x)), "1")
= isNil("1") ? undefined : String(parseInt("1"))
= String(parseInt("1"))
= String(1)
= 1

map(String, map(parseInt, "1"))
= isNil(isNil("1") ? undefined : parseInt("1")) ? undefined : String(parseInt("1")) *not actual reduction
= isNil(parseInt("1")) ? undefined : String(parseInt("1"))
= String(parseInt("1"))
= String(1)
= 1

Ok!

map(x => String(parseInt(x)), undefined)
= isNil(undefined) ? undefined : String(parseInt(undefined))
= undefined

map(String, map(parseInt, "1"))
= isNil(isNil(undefined) ? undefined : parseInt(undefined)) ? undefined : String(parseInt(undefined)) *not actual reduction
= isNil(undefined) ? undefined : String(parseInt(undefined))
= undefined

Ok!

Albeit only for functor.

@baetheus

This comment has been minimized.

Copy link

@baetheus baetheus commented Nov 10, 2020

I see, for the case where you are mapping with a function f where f<T>(ta: Nilable<T>): Nilable<T> you lose information during composition. Effectively this is the case where undefined is meaningful as a value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment