Skip to content

Commit

Permalink
feat(elseDo): Side effects for failed computations
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `ap` has been removed
  • Loading branch information
kofno committed Nov 22, 2019
1 parent 266e452 commit d6f22a2
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 71 deletions.
7 changes: 6 additions & 1 deletion src/Err.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,19 @@ class Err<E, A> extends Result<E, A> {

public assign<K extends string, B>(
k: K,
other: Result<E, B> | ((a: A) => Result<E, B>),
other: Result<E, B> | ((a: A) => Result<E, B>)
): Result<E, A & { [k in K]: B }> {
return new Err<E, A & { [k in K]: B }>(this.error);
}

public do(fn: (a: A) => void): Result<E, A> {
return new Err<E, A>(this.error);
}

public elseDo(fn: (err: E) => void): Result<E, A> {
fn(this.error);
return this;
}
}

/**
Expand Down
14 changes: 5 additions & 9 deletions src/Ok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,9 @@ class Ok<E, A> extends Result<E, A> {
return matcher.Ok(this.value);
}

public ap<B, C>(result: Result<E, B>): Result<E, C> {
if (typeof this.value !== 'function') {
throw new TypeError(`'ap' can only be applied to functions: ${JSON.stringify(this.value)}`);
}

return result.map(this.value);
}

public assign<K extends string, B>(
k: K,
other: Result<E, B> | ((a: A) => Result<E, B>),
other: Result<E, B> | ((a: A) => Result<E, B>)
): Result<E, A & { [k in K]: B }> {
const result = other instanceof Result ? other : other(this.value);
return result.map<A & { [k in K]: B }>(b => ({
Expand All @@ -57,6 +49,10 @@ class Ok<E, A> extends Result<E, A> {
fn(this.value);
return new Ok<E, A>(this.value);
}

public elseDo(fn: (err: E) => void): Result<E, A> {
return this;
}
}

/**
Expand Down
25 changes: 17 additions & 8 deletions src/Result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,6 @@ abstract class Result<E, A> {
*/
public abstract cata<B>(matcher: Catamorphism<E, A, B>): B;

/**
* Apply the value of a successful result to a function result. If this
* result is an Err, nothing happens. If this result is NOT a function,
* a TypeError is raised.
*/
public abstract ap<B, C>(result: Result<E, B>): Result<E, C>;

/**
* Encapsulates a common pattern of needing to build up an Object from
* a series of Result values. This is often solved by nesting `andThen` calls
Expand All @@ -69,7 +62,7 @@ abstract class Result<E, A> {
*/
public abstract assign<K extends string, B>(
k: K,
other: Result<E, B> | ((a: A) => Result<E, B>),
other: Result<E, B> | ((a: A) => Result<E, B>)
): Result<E, A & { [k in K]: B }>;

/**
Expand All @@ -90,6 +83,22 @@ abstract class Result<E, A> {
*
*/
public abstract do(fn: (a: A) => void): Result<E, A>;

/**
* Inject a side-effectual operation into a chain of Result computations.
*
* The side effect only runs when there is an error (Err).
*
* The value will remain unchanged during the `elseDo` operation.
*
* ok({})
* .assign('foo', ok(42))
* .assign('bar', ok('hello'))
* .do(scope => console.log('Scope: ', JSON.stringify(scope)))
* .map(doSomethingElse)
*
*/
public abstract elseDo(fn: (err: E) => void): Result<E, A>;
}

export default Result;
56 changes: 33 additions & 23 deletions tests/Err.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { err, ok } from './../src/index';

test('Err.getOrElse', t => {
const result = err<string, string>('foo');
t.equal('bar', result.getOrElse(() => 'bar'));
t.equal(
'bar',
result.getOrElse(() => 'bar')
);
t.end();
});

Expand Down Expand Up @@ -35,37 +38,44 @@ test('Err.cata', t => {
});

test('Err.mapError', t => {
err('foo').mapError(m => m.toUpperCase()).cata({
Err: err => t.equal('FOO', err),
Ok: v => t.fail('should not have passed'),
});
err('foo')
.mapError(m => m.toUpperCase())
.cata({
Err: err => t.equal('FOO', err),
Ok: v => t.fail('should not have passed'),
});
t.end();
});

test('Err.ap', t => {
const fn = (a: string) => (b: number) => ({ a, b });

err('oops!').ap(ok('hi')).ap(ok(42)).cata({
Err: m => t.pass(`Failed as expected: ${m}`),
Ok: v => t.fail(`Should have failed: ${JSON.stringify(v)}`),
});

test('Err.assign', t => {
ok({})
.assign('x', ok(42))
.assign('y', () => err('ooops!'))
.cata({
Err: m => t.pass(`Failed as expected: ${m}`),
Ok: v => t.fail(`Should have failed: ${JSON.stringify(v)}`),
});
t.end();
});

test('Err.assign', t => {
ok({}).assign('x', ok(42)).assign('y', () => err('ooops!')).cata({
Err: m => t.pass(`Failed as expected: ${m}`),
Ok: v => t.fail(`Should have failed: ${JSON.stringify(v)}`),
});
test('Err.do', t => {
err('oops!')
.do(v => t.fail(`Should NOT run side effect: ${v}`))
.cata({
Err: m => t.pass(`Should be an error: ${m}`),
Ok: v => t.fail(`Should not succeeded: ${JSON.stringify(v)}`),
});

t.end();
});

test('Err.do', t => {
err('oops!').do(v => t.fail(`Should NOT run side effect: ${v}`)).cata({
Err: m => t.pass(`Should be an error: ${m}`),
Ok: v => t.fail(`Should not succeeded: ${JSON.stringify(v)}`),
});
test('Err.elseDo', t => {
err('oops!')
.elseDo(v => t.pass(`Error side effect ran: ${v}`))
.cata({
Err: m => t.pass(`Should be an error: ${m}`),
Ok: v => t.fail(`Should not succeed: ${JSON.stringify(v)}`),
});

t.end();
});
69 changes: 39 additions & 30 deletions tests/Ok.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import * as test from 'tape';
import { err, ok } from './../src/index';
import { ok } from './../src/index';

test('Ok.getOrElse', t => {
const result = ok<string, string>('foo');
t.equal('foo', result.getOrElse(() => 'bar'));
t.equal(
'foo',
result.getOrElse(() => 'bar')
);
t.end();
});

test('OK.map', t => {
const result = ok<string, string>('foo');
t.equal('FOO', result.map(s => s.toUpperCase()).getOrElse(() => ''));
t.equal(
'FOO',
result.map(s => s.toUpperCase()).getOrElse(() => '')
);
t.end();
});

Expand All @@ -35,43 +41,46 @@ test('Ok.cata', t => {
});

test('Ok.mapError', t => {
ok<string, string>('foo').mapError(m => m.toUpperCase()).cata({
Err: err => t.fail('should have passed'),
Ok: v => t.pass('Worked!'),
});
ok<string, string>('foo')
.mapError(m => m.toUpperCase())
.cata({
Err: err => t.fail('should have passed'),
Ok: v => t.pass('Worked!'),
});

t.end();
});

test('Ok.ap', t => {
const fn = (a: string) => (b: number) => ({ a, b });

ok(fn).ap(ok('hi')).ap(ok(42)).cata({
Err: m => t.fail(`Should have passed: ${m}`),
Ok: v => t.pass(`Worked!: ${JSON.stringify(v)}`),
});

ok(fn).ap(ok('hi')).ap(err('oops!')).cata({
Err: m => t.pass(`ap failed: ${m}`),
Ok: v => t.fail(`should have failed: ${v}`),
});

test('Ok.assign', t => {
ok({})
.assign('x', ok(42))
.assign('y', v => ok(String(v.x + 8)))
.cata({
Err: m => t.fail(`Should have succeeded: ${m}`),
Ok: v => t.deepEqual(v, { x: 42, y: '50' }),
});
t.end();
});

test('Ok.assign', t => {
ok({}).assign('x', ok(42)).assign('y', v => ok(String(v.x + 8))).cata({
Err: m => t.fail(`Should have succeeded: ${m}`),
Ok: v => t.deepEqual(v, { x: 42, y: '50' }),
});
test('Ok.do', t => {
ok({})
.assign('x', ok(42))
.do(scope => t.pass(`'do' should run: ${JSON.stringify(scope)}`))
.cata({
Err: m => t.fail(`Should have succeeded: ${m}`),
Ok: v => t.deepEqual(v, { x: 42 }),
});

t.end();
});

test('Ok.do', t => {
ok({}).assign('x', ok(42)).do(scope => t.pass(`'do' should run: ${JSON.stringify(scope)}`)).cata({
Err: m => t.fail(`Should have succeeded: ${m}`),
Ok: v => t.deepEqual(v, { x: 42 }),
});
test('Ok.elseDo', t => {
ok({ x: 42 })
.elseDo(err => t.fail(`Error side effect should not run: ${err}`))
.cata({
Err: m => t.fail(`Should have succeeded: ${m}`),
Ok: v => t.deepEqual(v, { x: 42 }),
});

t.end();
});

0 comments on commit d6f22a2

Please sign in to comment.