Skip to content

Commit

Permalink
feat(alt): add an interface for Alt with instance for Maybe
Browse files Browse the repository at this point in the history
re #3
  • Loading branch information
williamareynolds committed Jan 5, 2020
1 parent 52294e7 commit d8a5f21
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 25 deletions.
9 changes: 9 additions & 0 deletions src/Alt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Type, URIs } from './HKT'

export interface Alt1<F extends URIs, A> {
alt: (fa: Type<F, A>) => Type<F, A>
}

export interface AltS1<F extends URIs> {
alt: <A>(f2: Type<F, A>, f1: Type<F, A>) => Type<F, A>
}
32 changes: 29 additions & 3 deletions src/instance/Maybe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Monad1, MonadS1 } from '../Monad'
import { Alt1, AltS1 } from '../Alt'

declare module '../HKT' {
interface URItoHKT<A> {
Expand All @@ -12,7 +13,7 @@ type URI = typeof URI
export const MAYBE_URI = URI
export type MaybeURI = URI

export abstract class Maybe<A> implements Monad1<URI, A> {
export abstract class Maybe<A> implements Monad1<URI, A>, Alt1<URI, A> {
/** @ignore */
readonly tag!: 'Just' | 'Nothing'

Expand Down Expand Up @@ -71,6 +72,13 @@ export abstract class Maybe<A> implements Monad1<URI, A> {
* @param f A unary function which returns a Maybe
*/
abstract chain<B>(f: (a: A) => Maybe<B>): Maybe<B>

/**
* If this Maybe is Nothing, provide another Maybe.
*
* @param fa
*/
abstract alt(fa: Maybe<A>): Maybe<A>
}

class Nothing<A> extends Maybe<A> {
Expand All @@ -92,9 +100,13 @@ class Nothing<A> extends Maybe<A> {
chain<B>(_: (a: A) => Maybe<B>): Maybe<B> {
return new Nothing() as Maybe<NonNullable<B>>
}

alt(fa: Maybe<A>): Maybe<A> {
return fa
}
}

class Just<A> extends Maybe<A> implements Monad1<URI, A> {
class Just<A> extends Maybe<A> {
tag: 'Just' = 'Just'

toString(): string {
Expand All @@ -121,12 +133,16 @@ class Just<A> extends Maybe<A> implements Monad1<URI, A> {
chain<B>(f: (a: A) => Maybe<B>): Maybe<B> {
return f(this.value)
}

alt(fa: Maybe<A>): Maybe<A> {
return Maybe.of(this.value)
}
}

/**
* The set of static functions which can be applied with Maybe
*/
export const maybe: MonadS1<URI> = {
export const maybe: MonadS1<URI> & AltS1<URI> = {
/**
* Apply a function to the value contained in Maybe
*
Expand Down Expand Up @@ -177,5 +193,15 @@ export const maybe: MonadS1<URI> = {
*/
chain: <A, B>(f: (a: A) => Maybe<B>, ma: Maybe<A>): Maybe<B> => {
return ma.chain(f)
},

/**
* Replace f1 with f2 if f1 is Nothing.
*
* @param f2
* @param f1
*/
alt: <A>(f2: Maybe<A>, f1: Maybe<A>): Maybe<A> => {
return f1.alt(f2)
}
}
91 changes: 69 additions & 22 deletions test/maybe.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import * as fc from 'fast-check'
import { Maybe, maybe } from '../src/ts-cat'

const maybeIdentity = (a: number) => a
const f = (a?: number) => {
if (a === undefined) return a
return a * 2
}
const g = (a: number) => {
if (a === undefined) return a
return a - 8
}

const intOrUndefined = fc.frequency(
{
weight: 1,
Expand All @@ -12,6 +22,17 @@ const intOrUndefined = fc.frequency(
}
)

const fOrUndefined = fc.frequency(
{
weight: 1,
arbitrary: fc.constant(undefined)
},
{
weight: 1,
arbitrary: fc.constant(f)
}
)

describe("Maybe's", () => {
describe('string helper function', () => {
it('translates an Maybe to a pretty string', () => {
Expand All @@ -37,29 +58,19 @@ describe("Maybe's", () => {
})
})

const maybeFn = (a: number) => a
const f = (a?: number) => {
if (a === undefined) return a
return a * 2
}
const g = (a: number) => {
if (a === undefined) return a
return a - 8
}

describe('instance of functor', () => {
it('should obey the maybe law', () => {
fc.assert(
fc.property(intOrUndefined, int => {
const m = Maybe.of(int)
expect(m.map(maybeFn)).toStrictEqual(m)
expect(m.map(maybeIdentity)).toStrictEqual(m)
})
)

fc.assert(
fc.property(intOrUndefined, int => {
const m = maybe.of(int)
expect(maybe.map(maybeFn, m)).toStrictEqual(m)
expect(maybe.map(maybeIdentity, m)).toStrictEqual(m)
})
)
})
Expand All @@ -85,16 +96,6 @@ describe("Maybe's", () => {

describe('instance of Apply', () => {
it('should obey the composition law', () => {
const fOrUndefined = fc.frequency(
{
weight: 1,
arbitrary: fc.constant(undefined)
},
{
weight: 1,
arbitrary: fc.constant(f)
}
)
const idG = Maybe.of(g)

fc.assert(
Expand Down Expand Up @@ -255,4 +256,50 @@ describe("Maybe's", () => {
)
})
})

describe('instance of Alt', () => {
it('should obey the law of associativity', () => {
fc.assert(
fc.property(intOrUndefined, intOrUndefined, intOrUndefined, (a, b, c) => {
const x = Maybe.of(a)
const y = Maybe.of(b)
const z = Maybe.of(c)

expect(x.alt(y).alt(z)).toStrictEqual(x.alt(y.alt(z)))
})
)

fc.assert(
fc.property(intOrUndefined, intOrUndefined, intOrUndefined, (a, b, c) => {
const x = maybe.of(a)
const y = maybe.of(b)
const z = maybe.of(c)

expect(maybe.alt(maybe.alt(x, y), z)).toStrictEqual(maybe.alt(x, maybe.alt(y, z)))
})
)
})

it('should obey the law of distributivity', () => {
fc.assert(
fc.property(intOrUndefined, intOrUndefined, (a, b) => {
const x = Maybe.of(a)
const y = Maybe.of(b)

expect(x.alt(y).map(f)).toStrictEqual(x.map(f).alt(y.map(f)))
})
)

fc.assert(
fc.property(intOrUndefined, intOrUndefined, (a, b) => {
const x = maybe.of(a) // 0
const y = maybe.of(b) // 1

expect(maybe.map(maybeIdentity, maybe.alt(y, x))).toStrictEqual(
maybe.alt(maybe.map(maybeIdentity, y), maybe.map(maybeIdentity, x))
)
})
)
})
})
})

0 comments on commit d8a5f21

Please sign in to comment.