Skip to content

Commit

Permalink
feat(transducers): fix #401, update multiplex(), step()
Browse files Browse the repository at this point in the history
- add optional support to override single-result unwrapping behavior
- update docstrings/examples
- add tests
  • Loading branch information
postspectacular committed Jul 26, 2023
1 parent aa22030 commit 834b076
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 57 deletions.
11 changes: 10 additions & 1 deletion packages/transducers/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@ import type { Reduced } from "./reduced.js";

export type Transducer<A, B> = (rfn: Reducer<any, B>) => Reducer<any, A>;

/**
* A transducer or a custom type with a {@link IXform} implementation.
*/
export type TxLike<A, B> = Transducer<A, B> | IXform<A, B>;

/**
* Custom version of {@link TxLike} for use with {@link multiplex} and
* {@link multiplexObj}.
*/
export type MultiplexTxLike<T, A> = TxLike<T, A> | [TxLike<T, A>, boolean];

export type ReductionFn<A, B> = (acc: A, x: B) => A | Reduced<A>;

export interface Reducer<A, B> extends Array<any> {
Expand All @@ -20,7 +29,7 @@ export interface Reducer<A, B> extends Array<any> {
* functions in this package where a `Transducer` arg is expected.
*
* @example
* ```
* ```ts
* class Mul implements IXform<number, number> {
* constructor(public factor = 10) {}
*
Expand Down
13 changes: 6 additions & 7 deletions packages/transducers/src/multiplex-obj.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IObjectOf } from "@thi.ng/api";
import type { Reducer, Transducer, TxLike } from "./api.js";
import type { MultiplexTxLike, Reducer, Transducer } from "./api.js";
import { comp } from "./comp.js";
import { __iter } from "./iterator.js";
import { multiplex } from "./multiplex.js";
Expand All @@ -17,28 +17,27 @@ import { rename } from "./rename.js";
* upper: map(x => x.toUpperCase()),
* length: map(x => x.length)
* },
* ["Alice", "Bob", "Charlie", "Andy"]
* ["Alice", "Bob", "Charlie"]
* )]
* // [ { length: 5, upper: 'ALICE', initial: 'A' },
* // { length: 3, upper: 'BOB', initial: 'B' },
* // { length: 7, upper: 'CHARLIE', initial: 'C' },
* // { length: 4, upper: 'ANDY', initial: 'A' } ]
* // { length: 7, upper: 'CHARLIE', initial: 'C' } ]
* ```
*
* @param xforms - object of transducers
* @param rfn -
* @param src -
*/
export function multiplexObj<A, B>(
xforms: IObjectOf<TxLike<A, any>>,
xforms: IObjectOf<MultiplexTxLike<A, any>>,
rfn?: Reducer<B, [PropertyKey, any]>
): Transducer<A, B>;
export function multiplexObj<A, B>(
xforms: IObjectOf<TxLike<A, any>>,
xforms: IObjectOf<MultiplexTxLike<A, any>>,
src: Iterable<A>
): IterableIterator<B>;
export function multiplexObj<A, B>(
xforms: IObjectOf<TxLike<A, any>>,
xforms: IObjectOf<MultiplexTxLike<A, any>>,
rfn: Reducer<B, [PropertyKey, any]>,
src: Iterable<A>
): IterableIterator<B>;
Expand Down
115 changes: 76 additions & 39 deletions packages/transducers/src/multiplex.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { juxt } from "@thi.ng/compose/juxt";
import type { Transducer, TxLike } from "./api.js";
import type { MultiplexTxLike, Transducer } from "./api.js";
import { map } from "./map.js";
import { step } from "./step.js";

Expand All @@ -12,6 +12,12 @@ import { step } from "./step.js";
* Use the {@link noop} transducer for processing lanes which should retain the
* original input values.
*
* **Important note for transducers which are producing multiple (possibly
* varying number of) outputs for each received input:** If only a single output
* is produced per step, the default behavior of {@link step} is to unwrap it,
* i.e. `[42]` => `42`. To override this behavior, individual transducers given
* to `multiplex()` can be given as tuple `[xform, false]` (see 2nd example below).
*
* @example
* ```ts
* [...iterator(
Expand All @@ -20,63 +26,94 @@ import { step } from "./step.js";
* map(x => x.toUpperCase()),
* map(x => x.length)
* ),
* ["Alice", "Bob", "Charlie", "Andy"]
* ["Alice", "Bob", "Charlie"]
* )]
*
* // [ [ "A", "ALICE", 5 ], [ "B", "BOB", 3 ], [ "C", "CHARLIE", 7 ] ]
* ```
*
* @example
* ```ts
* [...iterator(
* multiplex(
* // override default unwrap behavior for this transducer
* // (i.e. here to ensure results are always arrays)
* [mapcat((x) => x), false],
* // use default behavior for this
* map((x) => x),
* ),
* [[1, 2], [3]]
* )]
*
* // [ [ [ 1, 2 ], [ 1, 2 ] ], [ [ 3 ], [ 3 ] ] ]
*
* // to compare: using the default behavior would produce this instead
* // (note the difference in the last result):
*
* // [ [ [ 1, 2 ], [ 1, 2 ] ], [ 3, [ 3 ] ] ]
* ```
*
* @param a -
*/
export function multiplex<T, A>(a: TxLike<T, A>): Transducer<T, [A]>;
export function multiplex<T, A>(a: MultiplexTxLike<T, A>): Transducer<T, [A]>;
export function multiplex<T, A, B>(
a: TxLike<T, A>,
b: TxLike<T, B>
a: MultiplexTxLike<T, A>,
b: MultiplexTxLike<T, B>
): Transducer<T, [A, B]>;
export function multiplex<T, A, B, C>(
a: TxLike<T, A>,
b: TxLike<T, B>,
c: TxLike<T, C>
a: MultiplexTxLike<T, A>,
b: MultiplexTxLike<T, B>,
c: MultiplexTxLike<T, C>
): Transducer<T, [A, B, C]>;
export function multiplex<T, A, B, C, D>(
a: TxLike<T, A>,
b: TxLike<T, B>,
c: TxLike<T, C>,
d: TxLike<T, D>
a: MultiplexTxLike<T, A>,
b: MultiplexTxLike<T, B>,
c: MultiplexTxLike<T, C>,
d: MultiplexTxLike<T, D>
): Transducer<T, [A, B, C, D]>;
export function multiplex<T, A, B, C, D, E>(
a: TxLike<T, A>,
b: TxLike<T, B>,
c: TxLike<T, C>,
d: TxLike<T, D>,
e: TxLike<T, E>
a: MultiplexTxLike<T, A>,
b: MultiplexTxLike<T, B>,
c: MultiplexTxLike<T, C>,
d: MultiplexTxLike<T, D>,
e: MultiplexTxLike<T, E>
): Transducer<T, [A, B, C, D, E]>;
export function multiplex<T, A, B, C, D, E, F>(
a: TxLike<T, A>,
b: TxLike<T, B>,
c: TxLike<T, C>,
d: TxLike<T, D>,
e: TxLike<T, E>,
f: TxLike<T, F>
a: MultiplexTxLike<T, A>,
b: MultiplexTxLike<T, B>,
c: MultiplexTxLike<T, C>,
d: MultiplexTxLike<T, D>,
e: MultiplexTxLike<T, E>,
f: MultiplexTxLike<T, F>
): Transducer<T, [A, B, C, D, E, F]>;
export function multiplex<T, A, B, C, D, E, F, G>(
a: TxLike<T, A>,
b: TxLike<T, B>,
c: TxLike<T, C>,
d: TxLike<T, D>,
e: TxLike<T, E>,
f: TxLike<T, F>,
g: TxLike<T, G>
a: MultiplexTxLike<T, A>,
b: MultiplexTxLike<T, B>,
c: MultiplexTxLike<T, C>,
d: MultiplexTxLike<T, D>,
e: MultiplexTxLike<T, E>,
f: MultiplexTxLike<T, F>,
g: MultiplexTxLike<T, G>
): Transducer<T, [A, B, C, D, E, F, G]>;
export function multiplex<T, A, B, C, D, E, F, G, H>(
a: TxLike<T, A>,
b: TxLike<T, B>,
c: TxLike<T, C>,
d: TxLike<T, D>,
e: TxLike<T, E>,
f: TxLike<T, F>,
g: TxLike<T, G>,
h: TxLike<T, H>
a: MultiplexTxLike<T, A>,
b: MultiplexTxLike<T, B>,
c: MultiplexTxLike<T, C>,
d: MultiplexTxLike<T, D>,
e: MultiplexTxLike<T, E>,
f: MultiplexTxLike<T, F>,
g: MultiplexTxLike<T, G>,
h: MultiplexTxLike<T, H>
): Transducer<T, [A, B, C, D, E, F, G, H]>;
export function multiplex(...args: any[]) {
return map(juxt.apply(null, <any>args.map(step)));
return map(
juxt.apply(
null,
<any>(
args.map((xf) =>
Array.isArray(xf) ? step(xf[0], xf[1]) : step(xf)
)
)
)
);
}
38 changes: 28 additions & 10 deletions packages/transducers/src/step.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
import type { Fn } from "@thi.ng/api";
import type { TxLike } from "./api.js";
import { ensureTransducer } from "./ensure.js";
import { push } from "./push.js";
import { isReduced } from "./reduced.js";

/**
* Single-step transducer execution wrapper.
* Returns array if transducer produces multiple results
* and undefined if there was no output. Else returns single
* result value.
* Single-step transducer execution wrapper. Returns array if the given
* transducer produces multiple results and undefined if there was no output. If
* the transducer only produces a single result (per step) and if `unwrap`
* is true (default), the function returns that single result value itself.
*
* @remarks
* Likewise, once a transducer has produced a final / reduced
* value, all further invocations of the stepper function will
* return undefined.
* Likewise, once a transducer has produced a final / reduced value, all further
* invocations of the stepper function will return undefined.
*
* @example
* ```ts
* // single result
* // single result (unwrapped, default)
* step(map(x => x * 10))(1);
* // 10
*
* // single result (no unwrapping)
* step(map(x => x * 10), false)(1);
* // [10]
*
* // multiple results
* step(mapcat(x => [x, x + 1, x + 2]))(1)
* // [ 1, 2, 3 ]
*
* // multiple results (default behavior)
* step(mapcat(x => x))([1, 2])
* // [1, 2]
* step(mapcat(x => x))([3])
* // 3
* // ...once more without unwrapping
* step(mapcat(x => x), false)([3])
* // [3]
*
* // no result
* f = step(filter((x) => !(x & 1)))
* f(1); // undefined
Expand All @@ -38,8 +51,9 @@ import { isReduced } from "./reduced.js";
* ```
*
* @param tx -
* @param unwrap -
*/
export const step = <A, B>(tx: TxLike<A, B>): ((x: A) => B | B[]) => {
export const step = <A, B>(tx: TxLike<A, B>, unwrap = true): Fn<A, B | B[]> => {
const { 1: complete, 2: reduce } = ensureTransducer(tx)(push());
let done = false;
return (x: A) => {
Expand All @@ -49,7 +63,11 @@ export const step = <A, B>(tx: TxLike<A, B>): ((x: A) => B | B[]) => {
if (done) {
acc = complete(acc.deref());
}
return acc.length === 1 ? acc[0] : acc.length > 0 ? acc : undefined;
return acc.length === 1 && unwrap
? acc[0]
: acc.length > 0
? acc
: undefined;
}
};
};
82 changes: 82 additions & 0 deletions packages/transducers/test/multiplex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { group } from "@thi.ng/testament";
import * as assert from "assert";
import { iterator, map, mapcat, multiplex, step } from "../src/index.js";
import { identity } from "@thi.ng/api";

group("multiplex/step", {
example: () => {
assert.deepStrictEqual(
[
...iterator(
multiplex(
map((x) => x.charAt(0)),
map((x) => x.toUpperCase()),
map((x) => x.length)
),
["Alice", "Bob", "Charlie"]
),
],
[
["A", "ALICE", 5],
["B", "BOB", 3],
["C", "CHARLIE", 7],
]
);
},
unwrap: () => {
assert.deepStrictEqual(
[
...iterator(multiplex(mapcat(identity), map(identity)), [
[1, 2],
[3],
]),
],
[
[
[1, 2],
[1, 2],
],
[3, [3]],
]
);
assert.deepStrictEqual(
step<number[], number>(mapcat((x) => x))([1, 2]),
[1, 2]
);
assert.deepStrictEqual(
step<number[], number>(mapcat((x) => x))([3]),
3
);
},
"no unwrap": () => {
assert.deepStrictEqual(
[
...iterator(
multiplex([mapcat(identity), false], map(identity)),
[[1, 2], [3]]
),
],
[
[
[1, 2],
[1, 2],
],
[[3], [3]],
]
);
assert.deepStrictEqual(
step<number[], number>(
mapcat((x) => x),
false
)([1, 2]),
[1, 2]
);
assert.deepStrictEqual(
step<number[], number>(
mapcat((x) => x),
false
)([3]),
[3]
);
},
});

0 comments on commit 834b076

Please sign in to comment.