-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add maybeT monadic transformer
Generally, a monadic transformer allows one to merge two monads into a single tool with the traits of both parent monads. In the case of the `maybe` monad, the resultant tool will behave with maybe semantics: if the contained value is null then the `map` function will not be called, else it will. When combined with an array (which is monadic in a sense), then the `maybeT` transform will iterate the `map` function over each element of the array and call the `map` function only if the element is non-null. This change adds special handling for `Promises` - or more generically, thenables - and treats the `then` function as a `map` function. This isn't technically true, since `then` is actually a `flatMap` function but these pedantics are relatively minor. When `maybeT` is combined with `Promise`, the `map` function will be called once the promise resolves if the resolve value is not nullable.
- Loading branch information
Showing
4 changed files
with
172 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,5 +68,8 @@ | |
}, | ||
"release": { | ||
"branch": "master" | ||
}, | ||
"dependencies": { | ||
"simplytyped": "^1.0.5" | ||
} | ||
} |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { ConstructorFor, Unknown, Nullable } from 'simplytyped'; | ||
// @ts-ignore | ||
import Maybe, { MatchType, Nil } from './maybe'; | ||
import { maybe } from './index'; | ||
|
||
export interface Monad<T> { | ||
map: <U>(f: (v: T) => Nullable<U>) => any; | ||
} | ||
|
||
export type MonadLike<T> = Monad<T> | PromiseLike<T>; | ||
export type MonadValue<T extends MonadLike<any>> = | ||
T extends PromiseLike<infer U> ? U : | ||
T extends Monad<infer U> ? U : never; | ||
|
||
export type MaybeValue<T extends MonadLike<any>> = NonNullable<MonadValue<T>>; | ||
|
||
const isPromise = (x: any): x is PromiseLike<any> => typeof x.then === 'function'; | ||
|
||
const getMap = (x: MonadLike<any>): Monad<any>['map'] => { | ||
if (isPromise(x)) return x.then.bind(x) as any; | ||
|
||
return x.map.bind(x) as any; | ||
}; | ||
|
||
export class MaybeT<T extends MonadLike<Unknown>> { | ||
private constructor(private value: T) {} | ||
|
||
static maybeT<V extends MonadLike<Unknown>>(monad: V) { | ||
return new MaybeT(monad); | ||
} | ||
|
||
map<U>(f: (v: MaybeValue<T>) => U): MaybeT<Monad<U>> { | ||
const map = getMap(this.value); | ||
return new MaybeT(map(inner => | ||
maybe(inner) | ||
.map(f) | ||
.asNullable() as any, | ||
)); | ||
} | ||
|
||
caseOf<R>(matcher: MatchType<MaybeValue<T>, R>): MaybeT<Monad<R>> { | ||
const map = getMap(this.value); | ||
return new MaybeT(map(inner => | ||
maybe(inner) | ||
.caseOf(matcher) | ||
.asNullable() as any, | ||
)); | ||
} | ||
|
||
orElse<U extends MonadValue<T>>(def: U | (() => U)): U { | ||
const map = getMap(this.value); | ||
return map(inner => | ||
maybe(inner) | ||
.orElse(def), | ||
); | ||
} | ||
|
||
asNullable() { return this.value; } | ||
asType<M extends MonadLike<Nullable<MonadValue<T>>>>(c: ConstructorFor<M>): M { | ||
if (!(this.value instanceof c)) throw new Error(`Expected value to be instance of monad ${c.name}`); | ||
|
||
return this.value; | ||
} | ||
} | ||
|
||
export const maybeT = MaybeT.maybeT; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { maybeT } from '../src'; | ||
|
||
test('array - can generate a maybeT', () => { | ||
const raw = [1, 2, null, undefined, 5]; | ||
const x = maybeT(raw); | ||
const got = x.asNullable(); | ||
|
||
expect(got).toEqual(raw); | ||
}); | ||
|
||
test('array - can map function over non-nil elements in array', () => { | ||
const x = maybeT([1, null, 1, undefined, 1]); | ||
|
||
const y = x.map(v => { | ||
expect(v).toBe(1); | ||
return 2; | ||
}).asNullable(); | ||
|
||
expect(y).toEqual([2, null, 2, null, 2]); | ||
}); | ||
|
||
test('array - can get back array type from maybeT', () => { | ||
const x = maybeT(['1', '2', null, '4']); | ||
|
||
const y = x | ||
.map(v => parseInt(v)) | ||
.map(v => v + 1) | ||
.asType<Array<number | null>>(Array); | ||
|
||
expect(y).toEqual([2, 3, null, 5]); | ||
}); | ||
|
||
test('array - cannot get back wrong monadic type', () => { | ||
const x = maybeT(['hi']); | ||
|
||
expect(() => { | ||
x.asType<Promise<string>>(Promise); | ||
}).toThrowError(); | ||
}); | ||
|
||
test('array - can pattern match over values', () => { | ||
const x = maybeT(['1', '2', null, '4']); | ||
|
||
const y = x | ||
.caseOf({ | ||
none: () => 3, | ||
some: v => parseInt(v), | ||
}) | ||
.asNullable(); | ||
|
||
expect(y).toEqual([1, 2, 3, 4]); | ||
}); | ||
|
||
test('array - can default null values', () => { | ||
const x = maybeT([1, 2, null, 4]); | ||
|
||
const y = x.orElse(3); | ||
|
||
expect(y).toEqual([1, 2, 3, 4]); | ||
}); | ||
|
||
test('promise - can generate a maybeT', async () => { | ||
const value = 'hi'; | ||
const x = maybeT(Promise.resolve(value)); | ||
|
||
const got = await x.asNullable(); | ||
|
||
expect(got).toBe(value); | ||
}); | ||
|
||
test('promise - cannot create maybe with rejected promise', async () => { | ||
expect.assertions(1); | ||
const value = 'hey'; | ||
const x = maybeT(Promise.reject(value)); | ||
|
||
try { | ||
await x.asNullable(); | ||
} catch (e) { | ||
expect(e).toBe(value); | ||
} | ||
}); | ||
|
||
test('promise - can map function over non-nil value', async () => { | ||
const x = maybeT(Promise.resolve('hey')); | ||
|
||
const got = await x | ||
.map(v => v + ' there') | ||
.asType<Promise<string | null>>(Promise); | ||
|
||
expect(got).toBe('hey there'); | ||
}); | ||
|
||
test('promise - will not map function over nil values', async () => { | ||
const x = maybeT(Promise.resolve(null)); | ||
|
||
const got = await x | ||
.map(v => { | ||
throw new Error("I shouldn't be here!"); | ||
}).asNullable(); | ||
|
||
expect(got).toBe(null); | ||
}); |