From dc4313eee66e5a80fa1dd43656f15f3a14421ddc Mon Sep 17 00:00:00 2001 From: William Reynolds Date: Sat, 1 Feb 2020 14:14:29 -0600 Subject: [PATCH] fix(maybe): complete instance of applicative In order to complete the instance of Applicative, a method "apply" must be added to the type. Technically, a Monad does not require an instance of Applicative, but most modern functional languages and libraries enforce this constraint due to the benefits derived when implementing instances of other Categories. Particularly in the case of Maybe, this instance is desirable due to the behavior of Maybes which wrap a function. Because applying an undefined function to a value results in an error, rather than undefined or null, mapping a function wrapped by Maybe demands that the function be unwrapped and coerced to some default before it can be used, or checking to see if it is defined. The "apply" method allows a value wrapped by Maybe to be applied to a function wrapped by Maybe. --- src/interfaces/maybe.interface.ts | 7 ++++++- src/monads/maybe.ts | 4 +++- test/monads/maybe.spec.ts | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/interfaces/maybe.interface.ts b/src/interfaces/maybe.interface.ts index d6ee377..d400d2a 100644 --- a/src/interfaces/maybe.interface.ts +++ b/src/interfaces/maybe.interface.ts @@ -98,4 +98,9 @@ export interface IMaybe extends IMonad { * otherwise return an empty Maybe */ filter(fn: (t: T) => boolean): IMaybe -} \ No newline at end of file + + /** + * Apply a function wrapped in Maybe + */ + apply(fab: IMaybe<(t: T) => R>): IMaybe +} diff --git a/src/monads/maybe.ts b/src/monads/maybe.ts index 9042dbf..94889cb 100644 --- a/src/monads/maybe.ts +++ b/src/monads/maybe.ts @@ -14,6 +14,7 @@ const tapSome = (value?: T) => (fn: (val: NonNullable) => void) => isNotEm const match = (value?: T) => (pattern: IMaybePattern) => isEmpty(value) ? pattern.none() : pattern.some(value as NonNullable) const map = (value?: T) => (fn: (t: NonNullable) => R) => isEmpty(value) ? maybe() : maybe(fn(value as NonNullable)) const flatMap = (value?: T) => (fn: (d: NonNullable) => IMaybe) => isEmpty(value) ? maybe() : fn(value as NonNullable) +const apply = (value?: T) => (maybeFn: IMaybe<(t: T) => R>) => maybeFn.flatMap(f => map(value)(f)) const flatMapAuto = (value?: T) => (fn: (d: NonNullable) => R) => isEmpty(value) ? maybe() : maybe(fn(value as NonNullable)) const valueOrThrow = (value?: T) => (msg?: string) => isEmpty(value) ? (() => { throw Error(msg) })() : value as NonNullable @@ -42,7 +43,8 @@ export const maybe = (value?: T): IMaybe> => { map: map(value), flatMap: flatMap(value), flatMapAuto: flatMapAuto(value), - filter: filter(value) + filter: filter(value), + apply: apply(value) } } diff --git a/test/monads/maybe.spec.ts b/test/monads/maybe.spec.ts index b51d548..1e94bf6 100644 --- a/test/monads/maybe.spec.ts +++ b/test/monads/maybe.spec.ts @@ -495,4 +495,23 @@ describe('Maybe', () => { .catch(error => expect(error).toEqual('err')) }) }) + + describe('apply', () => { + it('should return none in nullish cases', () => { + const thisNone = maybe() + const fnNone = maybe<(n: number) => number>() + const thisSome = maybe(5) + const fnSome = maybe((a: number) => a * 2) + + expect(thisNone.apply(fnNone).isNone()).toBe(true) + expect(thisNone.apply(fnSome).isNone()).toBe(true) + expect(thisSome.apply(fnNone).isNone()).toBe(true) + }) + + it('should apply the function in a maybe in someish cases', () => { + const a = maybe(5) + const f = maybe((a: number) => a * 2) + expect(a.apply(f).valueOrThrow()).toBe(10) + }) + }) })