Skip to content

Commit d681f42

Browse files
fix(maybe): complete instance of applicative (#116)
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.
1 parent 5243fe5 commit d681f42

File tree

3 files changed

+28
-2
lines changed

3 files changed

+28
-2
lines changed

src/interfaces/maybe.interface.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,9 @@ export interface IMaybe<T> extends IMonad<T> {
9898
* otherwise return an empty Maybe
9999
*/
100100
filter(fn: (t: T) => boolean): IMaybe<T>
101-
}
101+
102+
/**
103+
* Apply a function wrapped in Maybe
104+
*/
105+
apply<R>(fab: IMaybe<(t: T) => R>): IMaybe<R>
106+
}

src/monads/maybe.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const tapSome = <T>(value?: T) => (fn: (val: NonNullable<T>) => void) => isNotEm
1414
const match = <T>(value?: T) => <R>(pattern: IMaybePattern<T, R>) => isEmpty(value) ? pattern.none() : pattern.some(value as NonNullable<T>)
1515
const map = <T>(value?: T) => <R>(fn: (t: NonNullable<T>) => R) => isEmpty(value) ? maybe<R>() : maybe<R>(fn(value as NonNullable<T>))
1616
const flatMap = <T>(value?: T) => <R>(fn: (d: NonNullable<T>) => IMaybe<R>) => isEmpty(value) ? maybe<R>() : fn(value as NonNullable<T>)
17+
const apply = <T>(value?: T) => <R>(maybeFn: IMaybe<(t: T) => R>) => maybeFn.flatMap(f => map(value)(f))
1718
const flatMapAuto = <T>(value?: T) => <R>(fn: (d: NonNullable<T>) => R) => isEmpty(value) ? maybe<R>() : maybe<R>(fn(value as NonNullable<T>))
1819
const valueOrThrow = <T>(value?: T) => (msg?: string) => isEmpty(value) ? (() => { throw Error(msg) })() : value as NonNullable<T>
1920

@@ -42,7 +43,8 @@ export const maybe = <T>(value?: T): IMaybe<NonNullable<T>> => {
4243
map: map(value),
4344
flatMap: flatMap(value),
4445
flatMapAuto: flatMapAuto(value),
45-
filter: filter(value)
46+
filter: filter(value),
47+
apply: apply(value)
4648
}
4749
}
4850

test/monads/maybe.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,4 +495,23 @@ describe('Maybe', () => {
495495
.catch(error => expect(error).toEqual('err'))
496496
})
497497
})
498+
499+
describe('apply', () => {
500+
it('should return none in nullish cases', () => {
501+
const thisNone = maybe<number>()
502+
const fnNone = maybe<(n: number) => number>()
503+
const thisSome = maybe(5)
504+
const fnSome = maybe((a: number) => a * 2)
505+
506+
expect(thisNone.apply(fnNone).isNone()).toBe(true)
507+
expect(thisNone.apply(fnSome).isNone()).toBe(true)
508+
expect(thisSome.apply(fnNone).isNone()).toBe(true)
509+
})
510+
511+
it('should apply the function in a maybe in someish cases', () => {
512+
const a = maybe(5)
513+
const f = maybe((a: number) => a * 2)
514+
expect(a.apply(f).valueOrThrow()).toBe(10)
515+
})
516+
})
498517
})

0 commit comments

Comments
 (0)