Skip to content

Commit

Permalink
feat: add join method to combine maybes
Browse files Browse the repository at this point in the history
It is common to have two "maybe streams" of logic that need to be merged
together. We could add to the `or` method by providing an `and` method
to allow this, but there are cases where the user will want to control
how the merge happens.

In the case of `and`, the merge will be "take the right side". In the
case of `or`, the merge will be "take the left side". It is possible to
define other logical operators, though those will also result in "take
the {x} side" under various other conditions.

Having `join` allows `and`-like conditions with a genericized merge
method. It may be worth considering a higher abstraction over `join` in
the future that will allow control over both the `merge` and the
`conditions`. I imagine something like:
```typescript
const x = some('thing');
const y = some('other thing');

const z = x.joinIf(
    other => other.length > 5,
    (a, b) => a + b,
);
```
  • Loading branch information
andy.patterson authored and andnp committed May 14, 2018
1 parent ff150e0 commit 3efd2e7
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 3 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ const nullable = maybe(value).asNullable();
assert(nullable === value);
```

### join
`join` takes a "joiner" function and another `Maybe` instance and combines them.
If either of the `Maybe`s are empty, then the joiner function is not called.

```typescript
const first = maybe(getFirstName());
const last = maybe(getLastName());

const name_007 = first.join(
(a, b) => `${b}. ${a} ${b}.`,
last,
);
```

## MaybeT
```typescript
export function apiUserSearch(user: string): MaybeT<Promise<UserData>> {
Expand Down
2 changes: 2 additions & 0 deletions src/maybe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export default abstract class Maybe<T> {
abstract eq(other: Maybe<T>): boolean;
abstract asNullable(): T | null;

abstract join<U, R>(f: (x: T, y: U) => R | Nil, other: Maybe<U>): Maybe<R>;

// Fantasy-land aliases
static [fl.of]: <T>(x: T) => Maybe<T>;
[fl.map] = binder(this, this.map);
Expand Down
6 changes: 5 additions & 1 deletion src/none.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Maybe, { MatchType } from "./maybe";
import Maybe, { MatchType, Nil } from "./maybe";
import { maybe } from "./index";

const invokeFunc = <T>(funcOrT: T | (() => T)): T => {
Expand Down Expand Up @@ -44,6 +44,10 @@ export default class None<T> extends Maybe<T> {
return other instanceof None;
}

join<U, R>(f: (x: T, y: U) => R | Nil, other: Maybe<U>): Maybe<R> {
return this as any;
}

asNullable(): T | null { return null; }
}

Expand Down
4 changes: 4 additions & 0 deletions src/some.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export default class Some<T> extends Maybe<T> {
.orElse(false);
}

join<U, R>(f: (x: T, y: U) => Nullable<R>, other: Maybe<U>): Maybe<R> {
return this.flatMap(x => other.map(y => f(x, y)));
}

asNullable(): T | null {
return this.value!;
}
Expand Down
37 changes: 35 additions & 2 deletions tests/maybe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const noop = () => { /* stub */ };
const raiseError = () => {
throw new Error('oops');
};
const pass = () => expect(true).toBe(true);
const fail = () => expect(true).toBe(false);

const checkInstance = (thing: Maybe<any>) => () => expect(thing instanceof Maybe).toBe(true);

Expand Down Expand Up @@ -186,7 +188,7 @@ test('caseOf - calls "none" function when nil', () => {

none().caseOf({
some: raiseError,
none: () => expect(true).toBe(true),
none: () => pass(),
});
});

Expand All @@ -206,7 +208,7 @@ test('caseOf - can be provided subset of matcher functions', () => {
});

none().caseOf({
none: () => expect(true).toBe(true),
none: () => pass(),
});
});

Expand Down Expand Up @@ -235,6 +237,37 @@ test('eq - some is `eq` to some if the contents are ===', () => {
expect(some(x).eq(some({}))).toBe(false);
});

// ----
// join
// ----

test('join - calls f if both sides are some', () => {
expect.assertions(1);

const x = some('hi ');
const y = some('there');

const z = x.join((a, b) => a + b, y);

z.map(c => expect(c).toBe('hi there'));
});

test('join - does not call f if either side is none', () => {
expect.assertions(3);

const left = some('hi');
const right = none<string>();
const middle = none<string>();

const z1 = left.join((a, b) => a + b, right);
const z2 = right.join((a, b) => a + b, left);
const z3 = right.join((a, b) => a + b, middle);

z1.map(fail).orElse(pass);
z2.map(fail).orElse(pass);
z3.map(fail).orElse(pass);
});

// -------
// Fantasy
// -------
Expand Down

0 comments on commit 3efd2e7

Please sign in to comment.