Skip to content

Commit 3efd2e7

Browse files
andy.pattersonandnp
authored andcommitted
feat: add join method to combine maybes
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, ); ```
1 parent ff150e0 commit 3efd2e7

File tree

5 files changed

+60
-3
lines changed

5 files changed

+60
-3
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,20 @@ const nullable = maybe(value).asNullable();
157157
assert(nullable === value);
158158
```
159159

160+
### join
161+
`join` takes a "joiner" function and another `Maybe` instance and combines them.
162+
If either of the `Maybe`s are empty, then the joiner function is not called.
163+
164+
```typescript
165+
const first = maybe(getFirstName());
166+
const last = maybe(getLastName());
167+
168+
const name_007 = first.join(
169+
(a, b) => `${b}. ${a} ${b}.`,
170+
last,
171+
);
172+
```
173+
160174
## MaybeT
161175
```typescript
162176
export function apiUserSearch(user: string): MaybeT<Promise<UserData>> {

src/maybe.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export default abstract class Maybe<T> {
3030
abstract eq(other: Maybe<T>): boolean;
3131
abstract asNullable(): T | null;
3232

33+
abstract join<U, R>(f: (x: T, y: U) => R | Nil, other: Maybe<U>): Maybe<R>;
34+
3335
// Fantasy-land aliases
3436
static [fl.of]: <T>(x: T) => Maybe<T>;
3537
[fl.map] = binder(this, this.map);

src/none.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Maybe, { MatchType } from "./maybe";
1+
import Maybe, { MatchType, Nil } from "./maybe";
22
import { maybe } from "./index";
33

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

47+
join<U, R>(f: (x: T, y: U) => R | Nil, other: Maybe<U>): Maybe<R> {
48+
return this as any;
49+
}
50+
4751
asNullable(): T | null { return null; }
4852
}
4953

src/some.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export default class Some<T> extends Maybe<T> {
3434
.orElse(false);
3535
}
3636

37+
join<U, R>(f: (x: T, y: U) => Nullable<R>, other: Maybe<U>): Maybe<R> {
38+
return this.flatMap(x => other.map(y => f(x, y)));
39+
}
40+
3741
asNullable(): T | null {
3842
return this.value!;
3943
}

tests/maybe.test.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const noop = () => { /* stub */ };
88
const raiseError = () => {
99
throw new Error('oops');
1010
};
11+
const pass = () => expect(true).toBe(true);
12+
const fail = () => expect(true).toBe(false);
1113

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

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

187189
none().caseOf({
188190
some: raiseError,
189-
none: () => expect(true).toBe(true),
191+
none: () => pass(),
190192
});
191193
});
192194

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

208210
none().caseOf({
209-
none: () => expect(true).toBe(true),
211+
none: () => pass(),
210212
});
211213
});
212214

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

240+
// ----
241+
// join
242+
// ----
243+
244+
test('join - calls f if both sides are some', () => {
245+
expect.assertions(1);
246+
247+
const x = some('hi ');
248+
const y = some('there');
249+
250+
const z = x.join((a, b) => a + b, y);
251+
252+
z.map(c => expect(c).toBe('hi there'));
253+
});
254+
255+
test('join - does not call f if either side is none', () => {
256+
expect.assertions(3);
257+
258+
const left = some('hi');
259+
const right = none<string>();
260+
const middle = none<string>();
261+
262+
const z1 = left.join((a, b) => a + b, right);
263+
const z2 = right.join((a, b) => a + b, left);
264+
const z3 = right.join((a, b) => a + b, middle);
265+
266+
z1.map(fail).orElse(pass);
267+
z2.map(fail).orElse(pass);
268+
z3.map(fail).orElse(pass);
269+
});
270+
238271
// -------
239272
// Fantasy
240273
// -------

0 commit comments

Comments
 (0)