diff --git a/package.json b/package.json index d3e1211..461ea81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phantasy", - "version": "0.10.4", + "version": "0.10.5", "description": "", "author": "Tim Kuminecz ", "license": "MIT", @@ -54,10 +54,9 @@ "npm-run-all": "^3.1.2", "nyc": "^10.0.0", "tap-diff": "^0.1.1", - "tape": "^4.6.2" + "tape": "4.5.1" }, "dependencies": { - "bluebird": "^3.4.6", - "tape": "4.5.1" + "bluebird": "^3.4.6" } } diff --git a/src/eff.js b/src/eff.js index 195b6e6..7478d96 100644 --- a/src/eff.js +++ b/src/eff.js @@ -6,7 +6,9 @@ import { Task } from './task'; /** * The `Eff` monad * - * Represents synchronous computations which are both dependency-injected and explicitly effectful + * Represents synchronous computations which are both dependency-injected and + * explicitly effectful. `Eff` can be used for effectful code where the effect + * resolvers are injected. This allows the effect handling to be customizable. */ export class Eff { diff --git a/src/identity.js b/src/identity.js index 73ecf8a..87ae169 100644 --- a/src/identity.js +++ b/src/identity.js @@ -2,7 +2,11 @@ import { inspect } from 'util'; /** - * The Identity monad + * The `Identity` monad + * + * Simply acts a container for values without any additional behavior + * + * @example let a = Identity.of(42) */ export class Identity { @@ -10,57 +14,68 @@ export class Identity { /** * Constructs a new Maybe instance + * + * @private */ constructor(data: A): void { this.data = data; } /** - * map :: Identity a ~> (a -> b) -> Identity b + * `map :: Identity a ~> (a -> b) -> Identity b` + * + * Transforms the value in the `Identity` instance */ - map(f: (a: A) => B): Identity { - return Identity.of(f(this.data)); + map(transform: (a: A) => B): Identity { + return Identity.of(transform(this.data)); } /** - * andThen :: Identity a ~> (a -> Identity b) -> Identity b + * `andThen :: Identity a ~> (a -> Identity b) -> Identity b` + * + * Chains the value of the `Identity` instance + * with another `Identity`-producing function */ andThen(next: (a: A) => Identity): Identity { return next(this.data); } /** - * toString :: Identity a ~> () -> String + * `toString :: Identity a ~> () -> String` + * + * Returns a string representation of the `Identity` instance */ toString(): string { return `Identity ${ inspect(this.data) }`; } /** - * of :: a -> Identity a + * `of :: a -> Identity a` + * + * Returns an `Identity` instance wrapping the giving value */ static of(a: A): Identity { return new Identity(a); } /** - * lift :: (a -> b) -> Identity a -> Identity b + * `lift :: (a -> b) -> Identity a -> Identity b` */ - static lift(f: (a: A) => B): * { + static lift(f: (a: A) => B): (a: Identity) => Identity { return (ma) => ma.andThen(a => Identity.of(f(a))); } /** - * lift2 :: (a -> b -> c) -> Identity a -> Identity b -> Identity c + * `lift2 :: (a -> b -> c) -> (Identity a -> Identity b) -> Identity c` */ - static lift2(f: (a: A, b: B) => C): * { + static lift2(f: (a: A, b: B) => C): (a: Identity, b: Identity) => Identity { return (ma, mb) => ma.andThen(a => mb.andThen(b => Identity.of(f(a, b)))); } /** - * lift3 :: (a -> b -> c -> d) -> Identity a -> Identity b -> Identity c -> Identity d + * `lift3 :: (a -> b -> c -> d) -> (Identity a -> Identity b -> Identity c) -> Identity d` */ - static lift3(f: (a: A, b: B, c: C) => D): * { + static lift3(f: (a: A, b: B, c: C) => D): (a: Identity, b: Identity, c: Identity) => Identity { return (ma: Identity, mb: Identity, mc: Identity) => ma.andThen(a => mb.andThen(b => mc.andThen(c => Identity.of(f(a, b, c))))); } diff --git a/src/io.js b/src/io.js index a1fea29..2d4577c 100644 --- a/src/io.js +++ b/src/io.js @@ -3,43 +3,75 @@ /** * The `IO` monad * - * Represents a potentially-effectful synchronous computation + * Represents a potentially-effectful synchronous computation. */ export class IO { - runIO: () => A + _runIO: () => A /** - * Constructs a new IO instance + * Constructs a new IO instance. * * @private */ constructor(runIO: () => A): void { - this.runIO = runIO; + this._runIO = runIO; + } + + /** + * `runIO :: IO a ~> () -> a` + * + * Executes the function contained in the `IO` instance and returns the produced value. Note that any side-effects in the `IO` would be performed at this time. + * + * @example let a = IO.of(42); + * + * a.runIO() === 42; + */ + runIO(): A { + return this._runIO(); } /** * `map :: IO a ~> (a -> b) -> IO b` * - * Transforms the result of the `IO` instance + * Transforms the result of the `IO` instance. + * + * @example let a = IO.of(2); + * + * a.map(a => a * 2) === IO.of(4); */ - map(transform: (a: A) => B): IO { - return IO.of(transform(this.runIO())); + map(f: (a: A) => B): IO { + return IO.of(f(this.runIO())); } /** * `andThen :: IO a ~> (a -> IO b) -> IO b` * - * Chains the result of the `IO` instance with another `IO`-producing function + * Chains the result of the `IO` instance with another `IO`-producing function. + * + * @example let a = IO.of(42); + * + * a.andThen(a => IO.of(a / 2)) == IO.of(24); */ andThen(next: (a: A) => IO): IO { return next(this.runIO()); } + /** + * `from :: (() -> a) -> IO a` + * + * Returns a `IO` instance wrapping the given function. + */ + static from(f: () => A): IO { + return new IO(f); + } + /** * `of :: a -> IO a` * - * Returns an `IO` instance that always produces the given value + * Returns an `IO` instance that always produces the given value. + * + * @example let a = IO.of(42); */ static of(value: A): IO { return new IO(() => value); diff --git a/src/maybe.js b/src/maybe.js index fc63b16..94f2cd5 100644 --- a/src/maybe.js +++ b/src/maybe.js @@ -30,9 +30,19 @@ type MaybeData = Just | Nothing /** * The `Maybe` monad * - * Represents the possibility of a value or nothing. Commonly used - * to safely deal with nullable values because it is composable and - * forces the developer to explicitly handle the null case. + * Represents the possibility of the presence or absence of a value. + * Commonly used to safely deal with nullable values because it is + * composable and forces the developer to explicitly handle the null case. + * + * @example // construct a Maybe with a value + * let some = Maybe.Just(42); + * + * // construct a maybe absent of a value + * let none = Maybe.Nothing; + * + * // create Maybes from nullable values + * let ma = Maybe.of('foo'), + * mb = Maybe.of(null); */ export class Maybe { @@ -65,7 +75,7 @@ export class Maybe { /** * `isJust :: Maybe a ~> () -> Bool` * - * Returns `true` if the instance contains a value + * Returns `true` if the `Maybe` instance contains a value */ isJust(): bool { return this.cases({ @@ -77,7 +87,7 @@ export class Maybe { /** * `isNothing :: Maybe a ~> () -> Bool` * - * Returns `true` if the instance is absent of value + * Returns `true` if the `Maybe` instance is absent of value */ isNothing(): bool { return !this.isJust(); @@ -102,9 +112,9 @@ export class Maybe { * If `Just`, returns `Just` the value transformed by * the given function, otherwise returns `Nothing` */ - map(transform: (a: A) => B): Maybe { + map(f: (a: A) => B): Maybe { return (this.data instanceof Just) - ? Maybe.Just(transform(this.data.value)) + ? Maybe.Just(f(this.data.value)) : Maybe.Nothing; } @@ -135,7 +145,7 @@ export class Maybe { /** * `of :: a -> Maybe a` * - * Takes a nullable value and constructs a new `Maybe` instance containing it + * Returns `Maybe.Just` of the given value if it is not null and `Maybe.Nothing` otherwise */ static of(a: ?A): Maybe { return (a == null) @@ -146,7 +156,7 @@ export class Maybe { /** * `Just :: a -> Maybe a` * - * Data constructor for indicating the presence of a value + * Returns a a `Maybe` instance containing the given value */ static Just(a: A): Maybe { return new Maybe(new Just(a)); @@ -155,7 +165,7 @@ export class Maybe { /** * `Nothing :: Maybe a` * - * Data constructor for indicating the absence of a value + * A `Maybe` instance which is absent of a value */ static Nothing = (new Maybe(new Nothing()): any); @@ -180,25 +190,25 @@ export class Maybe { * * Takes an unary function and returns an equivalent unary function which operates on `Maybe` values */ - static lift(f: (a: A) => B): * { + static lift(f: (a: A) => B): (a: Maybe) => Maybe { return (ma) => ma.andThen(a => Maybe.of(f(a))); } /** - * `lift2 :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c` + * `lift2 :: (a -> b -> c) -> (Maybe a -> Maybe b) -> Maybe c` * * Takes an binary function and returns an equivalent binary function which operates on `Maybe` values */ - static lift2(f: (a: A, b: B) => C): * { + static lift2(f: (a: A, b: B) => C): (a: Maybe, b: Maybe) => Maybe { return (ma, mb) => ma.andThen(a => mb.andThen(b => Maybe.of(f(a, b)))); } /** - * `lift3 :: (a -> b -> c -> d) -> Maybe a -> Maybe b -> Maybe c -> Maybe d` + * `lift3 :: (a -> b -> c -> d) -> (Maybe a -> Maybe b -> Maybe c) -> Maybe d` * * Takes an ternary function and returns an equivalent ternary function which operates on `Maybe` values */ - static lift3(f: (a: A, b: B, c: C) => D): * { + static lift3(f: (a: A, b: B, c: C) => D): (a: Maybe, b: Maybe, c: Maybe) => Maybe { return (ma, mb, mc) => ma.andThen(a => mb.andThen(b => mc.andThen(c => Maybe.of(f(a, b, c))))); } diff --git a/src/pair.js b/src/pair.js index 29211cf..f44c11f 100644 --- a/src/pair.js +++ b/src/pair.js @@ -1,35 +1,81 @@ // @flow /** - * Pair functor + * The `Pair` functor + * + * Represents a pair of values. */ export class Pair { data: [A, B] + /** + * Constructs a new `Pair` instance. + * + * @private + */ constructor(data: [A, B]): void { this.data = data; } /** - * map :: Pair a b ~> ([a, b] -> [x, y]) -> Pair x y + * `toTuple :: Pair a b ~> [a, b]` + * + * Returns the tuple contained in the `Pair` + */ + toTuple(): [A, B] { + return this.data; + } + + /** + * `left :: Pair a b ~> a` + * + * Returns the left (first) value in the `Pair`. */ - map(f: (p: [A, B]) => [X, Y]): Pair { - return Pair.of(f(this.data)); + left(): A { + const [ a] = this.data; + return a; } /** - * from :: a -> b -> Pair a b + * right :: Pair a b ~> b + * + * Returns the right (second) value in the `Pair`. + */ + right(): B { + const [ , b ] = this.data; + return b; + } + + /** + * `map :: Pair a b ~> (b -> c) -> Pair a c` + * + * Transforms the right (second) value in the `Pair` instance. + */ + map(f: (b: B) => C): Pair { + const [ a, b ] = this.data; + return Pair.of([ a, f(b) ]); + } + + /** + * `from :: a -> b -> Pair a b` + * + * Returns a `Pair` containing the given two values. */ static from(a: A, b: B): Pair { return Pair.of([a, b]); } /** - * of :: [a, b] -> Pair a b + * `of :: [a, b] -> Pair a b` + * + * Returns a `Pair` containing the given tuple of values. */ - static of(data: [A, B]): Pair { - return new Pair(data); + static of(tuple: [A, B]): Pair { + const [ a, b ] = tuple, + pairData = [ a, b ]; + + return new Pair(pairData); } } diff --git a/src/result.js b/src/result.js index f2375fc..0dd8bb7 100644 --- a/src/result.js +++ b/src/result.js @@ -16,16 +16,35 @@ class Err { } /** - * The Result monad + * The `Result` monad + * + * Represents the possiblity of either a success value or an error value. + * + * @example // create a "success" value + * let a = Result.Val(42), + * b = Result.of(42); + * + * // create an "error" value + * let x = Result.Error('error!'); */ export class Result { data: Val | Err + /** + * Constructs a new `Result` instance. + * + * @private + */ constructor(data: Val | Err) { this.data = data; } + /** + * `cases :: Result a ~> { Val: a -> b, Err: x -> b } -> b` + * + * Performs a match against the possible `Result` cases and returns a value by executing the appropriate function. + */ cases(patterns: { Val: (a: A) => B, Err: (x: X) => B}): B { if (this.data instanceof Val) { return patterns.Val(this.data.val); @@ -36,7 +55,7 @@ export class Result { } /** - * map :: Result a x ~> (a -> b) -> Result b x + * `map :: Result a x ~> (a -> b) -> Result b x` */ map(f: (a: A) => B): Result { return this.cases({ @@ -46,7 +65,10 @@ export class Result { } /** - * andThen :: Result a x ~> (a -> Result b x) -> Result b x + * `andThen :: Result a x ~> (a -> Result b x) -> Result b x` + * + * Chains the success value of the `Result` instance + * with another `Result`-producing function. */ andThen(next: (a: A) => Result): Result { return this.cases({ @@ -56,7 +78,10 @@ export class Result { } /** - * handleError :: Result a x ~> (x -> Result a y) -> Result a y + * `handleError :: Result a x ~> (x -> Result a y) -> Result a y` + * + * Chains the error value of the `Result` instance + * with another `Result`-producing function. */ handleError(handle: (x: X) => Result): Result { return this.cases({ @@ -66,7 +91,9 @@ export class Result { } /** - * toMaybe :: Result a x ~> Maybe a + * `toMaybe :: Result a x ~> Maybe a` + * + * Returns the `Result` instance converted to a `Maybe`. */ toMaybe(): Maybe { return this.cases({ @@ -76,28 +103,37 @@ export class Result { } /** - * of :: a -> Result a x + * `of :: a -> Result a x` + * + * Alias of `Result.Val`. */ - static of(v: B): Result { - return Result.Val(v); + static of(value: B): Result { + return Result.Val(value); } /** - * Val :: a -> Result a x + * `Val :: a -> Result a x` + * + * Returns a `Result` which succeeds with the given value. */ - static Val(v: B): Result { - return new Result(new Val(v)); + static Val(value: B): Result { + return new Result(new Val(value)); } /** - * Err :: x -> Result a x + * `Err :: x -> Result a x` + * + * Returns a `Result` which fails with the given error value. */ - static Err(e: Y): Result { - return new Result(new Err(e)); + static Err(error: Y): Result { + return new Result(new Err(error)); } /** - * fromThrowable :: (() -> a) -> Result a Error + * `fromThrowable :: (() -> a) -> Result a Error` + * + * Takes a function which may throw an exception and returns a `Result` which + * either succeeds with the return value or fails with the thrown exception. */ static fromThrowable(throwFn: () => B): Result { try { diff --git a/src/task.js b/src/task.js index 34c22cf..2da7696 100644 --- a/src/task.js +++ b/src/task.js @@ -7,23 +7,37 @@ import { Result } from './result'; type TaskExecutor = (res: (a: A) => void, rej: (x: X) => void) => void; /** - * The Task monad + * The `Task` monad + * + * Represents an asynchronous computation which may succeed or fail. They are very similar to `Promise`s. */ export class Task { - runTask: TaskExecutor + _runTask: TaskExecutor /** - * Constructs a new Task instance + * Constructs a new Task instance. + * + * @private */ constructor(runTask: TaskExecutor) { - this.runTask = runTask; + this._runTask = runTask; + } + + /** + * `runTask:: Task a x ~> ((a -> void) -> (x -> void)) -> void` + * + * Takes success and error callbacks and executes the `Task`. The appropriate + * callback is called with produced value depending on the resolution of the `Task`. + */ + runTask(onSuccess: (a: A) => void, onFailure: (x: X) => void): void { + return this._runTask(onSuccess, onFailure); } /** - * map :: Task a x ~> (a -> b) -> Task b x + * `map :: Task a x ~> (a -> b) -> Task b x` * - * Transforms the result of this task if successful + * Transforms the result of the `Task` if successful. */ map(f: (a: A) => B): Task { return new Task((succ, fail) => { @@ -35,10 +49,10 @@ export class Task { } /** - * andThen :: Task a x ~> (a -> Task b x) -> Task b x + * `andThen :: Task a x ~> (a -> Task b x) -> Task b x` * - * Passes the result of this task into another task-producing - * function if successful + * Passes the result of the `Task` instance into + * another task-producing function if successful. */ andThen(next: (a: A) => Task): Task { return new Task((succ, fail) => { @@ -53,9 +67,9 @@ export class Task { } /** - * handleError :: Task a x ~> (x -> Task a x) -> Task a x + * `handleError :: Task a x ~> (x -> Task a x) -> Task a x` * - * Handles a failed task result + * Handles a failed task result. */ handleError(handle: (x: X) => Task): Task { return new Task((succ, fail) => { @@ -70,7 +84,10 @@ export class Task { } /** - * toMaybe :: Task a x ~> Task (Maybe a) x + * `toMaybe :: Task a x ~> Task (Maybe a) x` + * + * Converts the `Task` instance into a `Task` + * that always succeeds with a `Maybe` value. */ toMaybe(): Task, any> { return this.andThen(val => Task.Success(Maybe.Just(val))) @@ -78,36 +95,40 @@ export class Task { } /** - * of :: a -> Task a x + * `of :: a -> Task a x` + * + * Alias of `Task.Success`. */ static of(a: B): Task { return Task.Success(a); } /** - * Success :: a -> Task a x + * `Success :: a -> Task a x` * - * Returns a Task that always succeeds with the given value + * Returns a `Task` that always succeeds with the given value. */ static Success(a: B): Task { return new Task(succ => succ(a)); } /** - * Fail :: x -> Task a x + * `Fail :: x -> Task a x` * - * Returns a Task that always fails with the given value + * Returns a `Task` that always fails with the given value. */ static Fail(x: Y): Task { return new Task((_, fail) => fail(x)); } /** - * fromResult :: Result a x -> Task a x + * `fromResult :: Result a x -> Task a x` + * + * Returns a `Task` equivalent to the given `Result` instance. */ - static fromResult(res: Result): Task { + static fromResult(result: Result): Task { return new Task((succ, fail) => { - res.cases({ + result.cases({ Val: a => succ(a), Err: x => fail(x) }); @@ -115,34 +136,40 @@ export class Task { } /** - * fromIO :: IO a -> Task a x + * `fromIO :: IO a -> Task a x` + * + * Returns a `Task` which always succeeds with + * the value produced by the given `IO` instance. */ static fromIO(io: IO): Task { return new Task(succ => succ(io.runIO())); } /** - * fromPromise :: Promise a -> Task a Error + * `fromPromise :: Promise a -> Task a Error` * - * Converts a promise to a task + * Returns a `Task` which is resolved to the same resolution as the given `Promise`. */ static fromPromise(promise: Promise): Task { return new Task((succ, fail) => { promise.then(succ).catch(fail); }); } /** - * fromPromiseFunc :: (() -> Promise a) -> Task a Error + * `fromPromiseFunc :: (() -> Promise a) -> Task a Error` + * + * Returns a `Task` which is resolved to the same resolution + * as the `Promise` produced by the given function. */ static fromPromiseFunc(promiseFn: () => Promise): Task { return Task.fromPromise(promiseFn()); } /** - * fromCallback :: (x -> a -> ()) -> Task a x + * `fromCallback :: (x -> a -> ()) -> Task a x` */ - static fromCallback(fn: (cb: (e: Y, v: B) => void) => void): Task { + static fromCallback(callbackFn: (cb: (e: Y, v: B) => void) => void): Task { return new Task((succ, fail) => { - fn((err, val) => { + callbackFn((err, val) => { if (err) { fail(err); } @@ -154,12 +181,15 @@ export class Task { } /** - * fromThrowable :: (() -> a) -> Task a Error + * `fromThrowable :: (() -> a) -> Task a Error` + * + * Takes a function which may throw an exception and returns a `Task` that + * either succeeds with the produced value or fails with the thrown exception. */ - static fromThrowable(fn: () => A): Task { + static fromThrowable(throwFn: () => A): Task { return new Task((succ, fail) => { try { - succ(fn()); + succ(throwFn()); } catch (err) { fail(err); @@ -168,24 +198,30 @@ export class Task { } /** - * lift :: (a -> b) -> Task a x -> Task b x + * `lift :: (a -> b) -> Task a x -> Task b x` + * + * Takes an unary function an returns an equivalent function that operates on `Task` instances */ - static lift(f: (t: T) => U): (tt: Task) => Task { - return (tt) => tt.andThen(t => Task.of(f(t))); + static lift(f: (t: A) => B): (ta: Task) => Task { + return (taskA) => taskA.andThen(a => Task.of(f(a))); } /** - * lift2 :: (a -> b -> c) -> Task a x -> Task b x -> Task c x + * `lift2 :: (a -> b -> c) -> Task a x -> Task b x -> Task c x` + * + * Takes an binary function an returns an equivalent function that operates on `Task` instances */ - static lift2(f: (t: T, u: U) => V): * { - return (tt, tu) => tt.andThen(tt => tu.map(tu => f(tt, tu))); + static lift2(f: (t: A, u: B) => C): (ta: Task, tb: Task) => Task { + return (taskA, taskB) => taskA.andThen(a => taskB.map(b => f(a, b))); } /** - * lift3 :: (a -> b -> c -> d) -> Task a x -> Task b x -> Task c x -> Task d x + * `lift3 :: (a -> b -> c -> d) -> Task a x -> Task b x -> Task c x -> Task d x` + * + * Takes an ternary function an returns an equivalent function that operates on `Task` instances */ static lift3(f: (a: A, b: B, c: C) => D): * { - return (ta, tb, tc) => ta.andThen(a => tb.andThen(b => tc.andThen(c => Task.of(f(a, b, c))))); + return (taskA, taskB, taskC) => taskA.andThen(a => taskB.andThen(b => taskC.andThen(c => Task.of(f(a, b, c))))); } } diff --git a/test/io.js b/test/io.js index ccb194f..7c9700d 100644 --- a/test/io.js +++ b/test/io.js @@ -6,7 +6,7 @@ import * as Util from './test-util'; const ioMapper = (ia: IO): A => ia.runIO(); test('IO', t => { - t.plan(5); + t.plan(6); const tIO = { t, mapper: ioMapper }; @@ -15,4 +15,6 @@ test('IO', t => { Util.testLift(tIO, IO); Util.testLift2(tIO, IO); Util.testLift3(tIO, IO); + + t.equal(IO.from(() => 42).runIO(), 42, 'IO.from'); }); diff --git a/test/pair.js b/test/pair.js index 7135530..70c32a8 100644 --- a/test/pair.js +++ b/test/pair.js @@ -3,7 +3,7 @@ import { Pair } from '../src/pair'; import test from 'tape'; test('Pair', t => { - t.plan(4); + t.plan(7); let pair = Pair.of([ 'foo', 42 ]); @@ -18,9 +18,12 @@ test('Pair', t => { ); t.deepEqual( - pair.map(([ s, n ]) => [s.toUpperCase(), n * 2]), - Pair.of([ 'FOO', 84 ]), + pair.map(n => n * 2), + Pair.of([ 'foo', 84 ]), 'Pair.map' ); + t.deepEqual(pair.toTuple(), [ 'foo', 42 ], 'pair.toTuple'); + t.equal(pair.left(), 'foo', 'pair.left'); + t.equal(pair.right(), 42, 'pair.right'); });