Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/unreleased/1558.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- `types/nullable`: `match` accepts independent result types for its two
branches and no longer widens the result to `Nullable` when they agree.
`map` is now derived from it; its own signature is unchanged
25 changes: 21 additions & 4 deletions fjs/types/nullable/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,31 @@ import { assert } from '../../asserts/module.f.mjs'
import { fn } from '../function/module.f.mjs'

/**
* @type {<T, R>(f: (value: T) => R) => (value: Nullable<T>) => Nullable<R>}
* Folds a `Nullable<T>` into a single value: `f` for a present value, `none`
* for `null`.
*
* The two branches carry independent result types, and `R2` lives on its own
* curry step on purpose. With `T`, `R1` and `R2` all on the outer generic they
* are instantiated together at `match(f)` — before `none` exists — so `R2` has
* nothing to infer from and collapses to `unknown`. Inferring it at the second
* call instead is what lets {@link map} be derived below.
*
* @type {<T, R1>(f: (_: T) => R1) => <R2>(none: () => R2) => (_: Nullable<T>) => R1 | R2}
*/
export const map = f => value => value === null ? null : f(value)
export const match = f => none => value => value === null ? none() : f(value)

/** The absent branch `map` fixes `match`'s `none` to. */
const noneIsNull = () => null

/**
* @type {<T, R>(f: (_: T) => R) => (none: () => R) => (_: Nullable<T>) => Nullable<R>}
* Projects the present value of a `Nullable<T>`, passing `null` through.
*
* `map` is `match` with the absent branch fixed to `null`, so the
* `value === null` guard is written once, in `match`.
*
* @type {<T, R>(f: (value: T) => R) => (value: Nullable<T>) => Nullable<R>}
*/
export const match = f => none => value => value === null ? none() : f(value)
export const map = f => match(f)(noneIsNull)

/**
* @type {<T>(value: Nullable<T>) => Option<T>}
Expand Down
15 changes: 15 additions & 0 deletions fjs/types/nullable/proof.f.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* @import { Assert } from '../../asserts/types.ts'
* @import { Equal } from '../ts/types.ts'
*/

import { fromUndefined, map, match, toOption } from './module.f.mjs'
import { assert, assertEq } from '../../asserts/module.f.mjs'

Expand All @@ -24,6 +29,16 @@ export const proof = [
assertEq(double(3), 6)
assertEq(double(null), -1)
},
() => {
// The two branches carry independent result types: `describe` is
// `(_: Nullable<number>) => number | string`, not one unified `R`.
/** @type {(v: number) => number} */
const twice = v => v * 2
const describe = match(twice)(() => 'none')
/** @typedef {Assert<Equal<ReturnType<typeof describe>, number | string>>} _Branches */
assertEq(describe(3), 6)
assertEq(describe(null), 'none')
},
() => {
assertEq(fromUndefined(undefined), null, 0)
assertEq(fromUndefined(5), 5, 1)
Expand Down
78 changes: 0 additions & 78 deletions fjs/types/nullable/todo/map-from-match.md

This file was deleted.

Loading