Skip to content

Commit

Permalink
Add Either
Browse files Browse the repository at this point in the history
  • Loading branch information
danwang committed May 17, 2017
1 parent aa2f095 commit 52a3380
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 0 deletions.
56 changes: 56 additions & 0 deletions src/__tests__/either.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// @flow
import {left, right} from 'either.js';
import {none, some} from 'option.js';

describe('either', () => {
test('map', () => {
const strlen = (s: string): number => s.length;
expect(left('left').left.map(strlen)).toEqual(left(4));
expect(left('left').right.map(strlen)).toEqual(left('left'));
expect(right('right').left.map(strlen)).toEqual(right('right'));
expect(right('right').right.map(strlen)).toEqual(right(5));
});

test('flatMap', () => {
const mapLeft = (n: number) => left('left mapped');
const mapRight = (n: number) => right('right mapped');

expect(left(1).left.flatMap(mapLeft)).toEqual(left('left mapped'));
expect(left(1).left.flatMap(mapRight)).toEqual(right('right mapped'));
expect(left(1).right.flatMap(mapLeft)).toEqual(left(1));
expect(left(1).right.flatMap(mapRight)).toEqual(left(1));

expect(right(1).left.flatMap(mapLeft)).toEqual(right(1));
expect(right(1).left.flatMap(mapRight)).toEqual(right(1));
expect(right(1).right.flatMap(mapLeft)).toEqual(left('left mapped'));
expect(right(1).right.flatMap(mapRight)).toEqual(right('right mapped'));
});

test('getOrElse', () => {
const def = 'hello';
expect(left(1).left.getOrElse(def)).toEqual(1);
expect(left(1).right.getOrElse(def)).toEqual('hello');
expect(right(1).left.getOrElse(def)).toEqual('hello');
expect(right(1).right.getOrElse(def)).toEqual(1);
});

test('filter', () => {
const even = (n: number) => n % 2 === 0;
expect(left(1).left.filter(even)).toEqual(none);
expect(left(1).right.filter(even)).toEqual(none);
expect(left(2).left.filter(even)).toEqual(some(left(2)));
expect(left(2).right.filter(even)).toEqual(none);

expect(right(1).left.filter(even)).toEqual(none);
expect(right(1).right.filter(even)).toEqual(none);
expect(right(2).left.filter(even)).toEqual(none);
expect(right(2).right.filter(even)).toEqual(some(right(2)));
});

test('toOption', () => {
expect(left(1).left.toOption()).toEqual(some(1));
expect(left(1).right.toOption()).toEqual(none);
expect(right(1).left.toOption()).toEqual(none);
expect(right(1).right.toOption()).toEqual(some(1));
});
});
119 changes: 119 additions & 0 deletions src/either.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// @flow
/* eslint-disable no-use-before-define */
import type {Option} from 'option.js';
import {some, none} from 'option.js';

class AbstractEither<A, B> {
isLeft: $Subtype<boolean>;
isRight: $Subtype<boolean>;
left: LeftProjection<A, B> = (new LeftProjection((this: any)));
right: RightProjection<A, B> = (new RightProjection((this: any)));
}

class LeftProjection<A, B> {
e: Either<A, B>;
constructor(e: Either<A, B>) {
this.e = e;
}
get() {
const {e} = this;
if (e.isLeft === true) {
return e.a;
} else {
throw new Error('Either.left.value on Right');
}
}
map<X>(f: A => X): Either<X, B> {
const {e} = this;
return e.isLeft === true ? left(f(e.a)) : right(e.b);
}
flatMap<X>(f: A => Either<X, B>): Either<X, B> {
const {e} = this;
return e.isLeft === true ? f(e.a) : right(e.b);
}
getOrElse<X>(def: X): (A | X) {
const {e} = this;
return e.isLeft === true ? e.a : def;
}
filter(p: A => boolean): Option<Either<A, B>> {
const {e} = this;
if (e.isLeft === true && p(e.a)) {
return some(e);
} else {
return none;
}
}
toOption(): Option<A> {
const {e} = this;
return e.isLeft === true ? some(e.a) : none;
}
}

class RightProjection<A, B> {
e: Either<A, B>;
constructor(e: Either<A, B>) {
this.e = e;
}
get() {
const {e} = this;
if (e.isRight === true) {
return e.b;
} else {
throw new Error('Either.right.value on Left');
}
}
map<X>(f: B => X): Either<A, X> {
const {e} = this;
return e.isRight === true ? right(f(e.b)) : left(e.a);
}
flatMap<X>(f: B => Either<A, X>): Either<A, X> {
const {e} = this;
return e.isRight === true ? f(e.b) : left(e.a);
}
getOrElse<X>(def: X): (B | X) {
const {e} = this;
return e.isRight === true ? e.b : def;
}
filter(p: B => boolean): Option<Either<A, B>> {
const {e} = this;
if (e.isRight === true && p(e.b)) {
return some(e);
} else {
return none;
}
}
toOption(): Option<B> {
const {e} = this;
return e.isRight === true ? some(e.b) : none;
}
}

class Left<A, B> extends AbstractEither<A, B> {
a: A;
constructor(a: A) {
super();
this.a = a;
}
isLeft: true = true;
isRight: false = false;
}

class Right<A, B> extends AbstractEither<A, B> {
b: B;
constructor(b: B) {
super();
this.b = b;
}
isLeft: false = false;
isRight: true = true;
}

export const left = <A, B>(a: A): Left<A, B> => new Left(a);
export const right = <A, B>(b: B): Right<A, B> => new Right(b);

export type Either<A, B> = Left<A, B> | Right<A, B>;

export default {
left,
right,
};

0 comments on commit 52a3380

Please sign in to comment.