recursive generator
To get a lazy iterable of the subsets, you can use a recursive generator. A utility tee function is used to "branch" the iterable -
function *subsets(arr) {
if (arr.length === 0) return yield []
const last = arr.slice(-1)
const [exclude, include] = tee(subsets(arr.slice(0, -1))) // recursive
yield *exclude
for (const s of include) yield s.concat(last)
}
function *tee(g, n = 2) {
const memo = []
function* iter(i) {
while (true) {
if (i >= memo.length) {
const w = g.next()
if (w.done) return
memo.push(w.value)
}
else yield memo[i++]
}
}
while (n-- >= 0) yield iter(0)
}
console.log(JSON.stringify(Array.from(subsets([]))))
console.log(JSON.stringify(Array.from(subsets([1]))))
console.log(JSON.stringify(Array.from(subsets([1,2]))))
console.log(JSON.stringify(Array.from(subsets([1,2,3]))))
.as-console-wrapper { min-height: 100%; top: 0; }
[[]]
[[],[1]]
[[],[1],[2],[1,2]]
[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
map for free
Array.from has optional parameters, mapFn and thisArg. We might naively write Array.from(...).map(mapFn), but Array.from(..., mapFn) does the same thing in half the iterations.
Imagine we write withStat to provide some statistic about the subsets as they are generated -
function withStat(subset) {
return {
length: subset.length,
sum: subset.reduce((a,b) => a + b, 0),
subset,
}
}
Array.from(subsets([1,2,3]), withStat)
[
{"length":0,"sum":0,"subset":[]},
{"length":1,"sum":1,"subset":[1]},
{"length":1,"sum":2,"subset":[2]},
{"length":2,"sum":3,"subset":[1,2]},
{"length":1,"sum":3,"subset":[3]},
{"length":2,"sum":4,"subset":[1,3]},
{"length":2,"sum":5,"subset":[2,3]},
{"length":3,"sum":6,"subset":[1,2,3]},
]
persistent iterators
Above we use tee to read an iterable's values multiple times. Below we show iter that wraps a generator in a persistent iterator interface, allowing us to use the iterator multiple times.
function *subsets(arr) {
if (arr.length === 0) return yield []
const last = arr.slice(-1)
const r = iter(subsets(arr.slice(0, -1))) // assign r
yield *r // read from r
for (const s of r) yield s.concat(last) // read from r again
}
For that we write an iter abstraction -
function iter(g) {
const computed = thunk(_ => g.next())
return {
get done() {
return computed().done
},
get value() {
return computed().value
},
next: thunk(_ =>
computed() && iter(g)
),
*[Symbol.iterator]() {
let t = this
while(!t.done) yield t.value, t = t.next()
}
}
}
Which depends on a thunk abstraction -
function thunk(f) {
const nil = Symbol()
let computed = nil
return _ => {
if (computed === nil) computed = f()
return computed
}
}
Hopefully you can begin to see how data abstraction allows us to write higher-level programs and keep complexity under control.
function *subsets(arr) {
if (arr.length === 0) return yield []
const last = arr.slice(-1)
const r = iter(subsets(arr.slice(0, -1)))
yield *r
for (const s of r) yield s.concat(last)
}
function iter(g) {
let computed = thunk(_ => g.next())
return {
get done() {
return computed().done
},
get value() {
return computed().value
},
next: thunk(_ =>
computed() && iter(g)
),
*[Symbol.iterator]() {
let t = this
while(!t.done) yield t.value, t = t.next()
}
}
}
function thunk(f) {
const nil = Symbol()
let computed = nil
return _ => {
if (computed === nil) computed = f()
return computed
}
}
console.log(JSON.stringify(Array.from(subsets([]))))
console.log(JSON.stringify(Array.from(subsets([1]))))
console.log(JSON.stringify(Array.from(subsets([1,2]))))
console.log(JSON.stringify(Array.from(subsets([1,2,3]))))
.as-console-wrapper { min-height: 100%; top: 0; }
[[]]
[[],[1]]
[[],[1],[2],[1,2]]
[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]