Created
December 17, 2019 00:30
-
-
Save unclechu/4297e07db922ec6783b1f171b3b2b193 to your computer and use it in GitHub Desktop.
TypeScript experiment with immitating Haskell's Maybe Monad behavior
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* 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