Skip to content

Instantly share code, notes, and snippets.

@unclechu
Created December 17, 2019 00:30
Show Gist options
  • Save unclechu/4297e07db922ec6783b1f171b3b2b193 to your computer and use it in GitHub Desktop.
Save unclechu/4297e07db922ec6783b1f171b3b2b193 to your computer and use it in GitHub Desktop.
TypeScript experiment with immitating Haskell's Maybe Monad behavior
/*!
* Date: December 2019
* Author: Viacheslav Lotsmanov
* License: Public Domain
*
* Special thanks to Gerrit Birkeland @gitter_gerrit0:matrix.org
* from https://riot.im/app/#/room/#typescript:matrix.org
* for good working examples and explanation!
*
* This module provides "deNull" which tries to
* immitate "Maybe" "Monad" in Haskell.
*
* It resolves multiple nullables ("Maybe"s in Haskell) into non-nullable values
* (unwrapped from "Maybe" values in Haskell).
*
* If any of the values is either "null" or "undefined" the whole computation
* resolves to "null" (function from the second argument won't be called).
*
* This solution tested with TypeScript 3.x
*
* Run to see successfull case (but not for the second usage example):
* tsc --strictNullChecks denull.ts && node denull.js 123 foo
*
* Run to get "null" results (due to one or both arguments being "null"):
* tsc --strictNullChecks denull.ts && node denull.js 123
* tsc --strictNullChecks denull.ts && node denull.js foo bar
*
* Run to get both results resolved successfully:
* tsc --strictNullChecks denull.ts && node denull.js 123 foo bar
* Run to get "null" for both examples (due to first argument being "null"
* because it's failed to be parsed as a number, and second example depends on
* first one, first one also has to be not "null", or whole second computation
* would fail to "null"):
* tsc --strictNullChecks denull.ts && node denull.js foo bar baz
*/
// By getting it from command-line arguments
// we're making it be unknown at compile-time
// in order to avoid ignoring "| null" in the type by the compiler
// (since value is determined and guaranteed to be not "null"
// when it is known at compile-time).
const foo: number | null = (() => {
const x: string | null = process.argv[2] ?? null;
if (x === null || x === '') return null;
const n: number = Number(x);
return isNaN(n) ? null : n;
})();
const bar: string | null = process.argv[3] ?? null;
const nonNullableNumber = (x: number): number => x + 1;
const nonNullableString = (x: string): string => `>>${x}<<`;
// Intersect with "unknown[]" to remind TS that the result is an array.
type NullableArguments<T extends any[]> =
{ [N in keyof T]: T[N] | undefined | null } & unknown[];
const deNull = <T, F extends (...args: any[]) => T>(
nullables: NullableArguments<Parameters<F>>,
func: F
): T | null => {
if (nullables.some(x => (x ?? null) === null)) return null;
return func(...nullables);
};
// Usage example.
// Types in the function arguments ("x: number, y: string") has to be defined!
// Otherwise type-safety will be eliminated (values transform to "any")!
const first: string | null = deNull(
[foo, bar],
(x: number, y: string) =>
`number: ${nonNullableNumber(x)}; string: ${nonNullableString(y)}`
);
console.log('first:', first);
// Usage example of composed "deNull"s.
const baz: string | null = process.argv[4] ?? null;
const second: string[] | null = deNull(
[first, baz],
(x: string, y: string) => [x, y]
);
console.log('second:', second);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment