Skip to content

Instantly share code, notes, and snippets.

@OliverJAsh
Last active September 4, 2023 15:31
Show Gist options
  • Save OliverJAsh/e6ba59d88f4fd0166e3a4211d7eae2be to your computer and use it in GitHub Desktop.
Save OliverJAsh/e6ba59d88f4fd0166e3a4211d7eae2be to your computer and use it in GitHub Desktop.
`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;
@baetheus
Copy link

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
Copy link
Author

@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
Copy link

@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
Copy link

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