Created
April 8, 2021 11:24
-
-
Save leidegre/56cf58309123431fbafbdf089bb70dd8 to your computer and use it in GitHub Desktop.
Lazy sequence that does composition via transducers (can be extended to do chunked stuff for async support)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Sentinel value. Signal break. */ | |
const BREAK = Symbol("break") | |
type Break = typeof BREAK | |
function isBreak<Result>(result: Result | Break): result is Break { | |
return result === BREAK | |
} | |
/** Sentinel value. Signal continue. */ | |
const CONTINUE = Symbol("continue") | |
type Continue = typeof CONTINUE | |
function isContinue<Result>(result: Result | Continue): result is Continue { | |
return result === CONTINUE | |
} | |
// --- | |
/** | |
* A reducing function suitable for use with our transducer protocol. | |
* | |
* The `Result` type is `unknown` until the reducing function is called. | |
* | |
* `Break` is a unique type used to break out of the reducing loop. | |
*/ | |
interface reducer<T, Result = unknown> { | |
(result: Result, input: T): Result | Break | |
} | |
/** | |
* A function that transforms one reducing function to another. | |
*/ | |
interface transform<A, B, Result = unknown> { | |
(reducer: reducer<B, Result>): reducer<A, Result> | |
} | |
// --- | |
/** Mapping transducer function. */ | |
function map<A, B>(mapper: (a: A) => B): transform<A, B> { | |
return (reducer) => { | |
return (result, input) => { | |
return reducer(result, mapper(input)) | |
} | |
} | |
} | |
/** Filtering transducer function. */ | |
function filter<T>(predicate: (value: T) => boolean): transform<T, T> { | |
return (reducer) => { | |
return (result, input) => { | |
if (predicate(input)) { | |
return reducer(result, input) | |
} | |
return result | |
} | |
} | |
} | |
// --- | |
/** Identity transducer function. */ | |
function identity<A, B>(a: A): B { | |
return (a as unknown) as B | |
} | |
function combine<A, B, C>(a: transform<A, B>, b: transform<B, C>): transform<A, C> { | |
// Silly optimization (one less transducer to allocate) | |
if (a === identity) { | |
return (b as unknown) as transform<A, C> | |
} | |
return reducer => a(b(reducer)) | |
} | |
function bind<A, B, Result>(xf: transform<A, B>, reducer: reducer<B, Result>): reducer<A, Result> { | |
return (xf as transform<A, B, Result>)(reducer) | |
} | |
// --- | |
class _Seq<From, T = From> { | |
it: Iterable<From> // the type of the underlying iterable (source) | |
xf: transform<From, T> // a transformation from that type to some other type (the actual enumeration type, can be same) | |
constructor(it: Iterable<From>, xf: transform<From, T> = identity) { | |
this.it = it | |
this.xf = xf | |
} | |
map<M>(mapper: (value: T) => M) { | |
return new _Seq(this.it, combine(this.xf, map(mapper))) | |
} | |
filter(predicate: (value: T) => boolean) { | |
return new _Seq(this.it, combine(this.xf, filter(predicate))) | |
} | |
reduce<Result>(reducer: reducer<T, Result>, initialValue: Result) { | |
const f = bind(this.xf, reducer) | |
let result = initialValue | |
for (const input of this.it) { | |
const next = f(result, input) | |
if (isBreak(next)) { | |
break | |
} | |
result = next | |
} | |
return result | |
} | |
/** Eager evaluation. */ | |
toArray() { | |
return this.reduce<Array<T>>((array, value) => (array.push(value), array), []) | |
} | |
/** Lazy evaluation. */ | |
*[Symbol.iterator](): IterableIterator<T> { | |
const f = bind(this.xf, ({ }: T | Continue, input) => input) | |
for (const input of this.it) { | |
const next = f(CONTINUE, input) | |
if (isContinue(next)) { | |
continue | |
} | |
if (isBreak(next)) { | |
break | |
} | |
yield next | |
} | |
} | |
} | |
console.log(new _Seq([1, 2, 3]).map(x => x * x).toArray()) | |
console.log([...new _Seq([1, 2, 3]).map(x => x * x)]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment