提问人:ypa y yhm 提问时间:8/17/2023 最后编辑:ypa y yhm 更新时间:8/28/2023 访问量:82
(日文/TS)为什么我的生成器函数代码在幂等性上有不同的行为?
(JS/TS) Why my generator function code have different behavior on the idempotence?
问:
定义
下面是一个由 generator func 定义的惰性序列:
type Fn <T, R> = (p: T) => R ;
class Stream
<T>
{
constructor
(private readonly generatorFunction: () => Generator<T>)
{} ;
static readonly iterate =
<T,> (initHead: T, f: Fn<T, T>)
: Stream<T> =>
new Stream
( function* ()
: Generator<T>
{
let head = initHead;
while (true)
{
yield head ;
head = f(head);
} ;
} ) ;
readonly map =
<R,> (f: Fn<T, R>)
: Stream<R> =>
new Stream
(( function* (this: Stream<T>)
: Generator<R>
{
const iterator = this.generatorFunction() ;
while (true)
{
const { value: head, done } = iterator.next();
if (done) break;
yield f(head) ;
} ;
} ).bind(this)) ;
readonly tookUntil =
(when: Fn<T, boolean>)
: [T[], Stream<T>] =>
{
const result: T[] = [] ;
const iterator = this.generatorFunction() ;
while (true)
{
const { value: head, done } = iterator.next();
if (done) break;
result.push(head);
if (when(head)) break;
} ;
const drops = iterator ;
return [result, new Stream
(( function* (this: Stream<T>)
: Generator<T>
{
while (true)
{
const { value, done } = drops.next();
if (done) break;
yield value ;
}
} ).bind(this)), ] ;
} ;
readonly took =
(limit: number)
: [T[], Stream<T>] =>
{
if (limit < 1)
{ return [[], this.follow({} as T).took(1)[1]] ; } else
{
let count = 1;
return this.tookUntil(() => !(count++ < limit));
} ;
} ;
readonly takeUntil =
(when: Fn<T, boolean>)
: T[] =>
this.tookUntil(when)[0] ;
readonly take =
(n: number)
: T[] =>
this.took(n)[0] ;
readonly droppingUntil =
(when: Fn<T, boolean>)
: Stream<T> =>
this.tookUntil(when)[1] ;
readonly dropping =
(limit: number)
: Stream<T> =>
this.took(limit)[1] ;
readonly dropUntil =
(when: Fn<T, boolean>)
: Stream<T> =>
new Stream
(( function* (this: Stream<T>)
: Generator<T>
{
const iterator = this.generatorFunction() ;
while (true)
{
const { value: head, done } = iterator.next();
if (done) break;
if (when(head)) { yield head ; break; } ;
} ;
while (true)
{
const { value, done } = iterator.next();
if (done) break;
yield value ;
} ;
} ).bind(this)) ;
readonly drop =
(limit: number)
: Stream<T> =>
{
const the = this ;
return new Stream(function* ()
{
if (limit < 1)
{ yield* the.generatorFunction() ; } else
{
let count = 1;
yield* the.dropUntil(() => (count++ > limit));
} ;
}) ;
} ;
// some code not very important here ...
[Symbol.iterator] = () => this.generatorFunction() ;
} ;
测试
直接实现的方法:drop
const droppedUntil = Stream.iterate(2, x => x + 1).dropUntil(x => x > 3) ;
console.log(droppedUntil.take(10)); // [4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
console.log(droppedUntil.take(10)); // [4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
const dropped = Stream.iterate(2, x => x + 1).drop(7) ;
console.log(dropped.take(10)); // [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
console.log(dropped.take(10)); // [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
实现方法如下:dropping
took
const dropped_bytook = Stream.iterate(2, x => x + 1).droppingUntil(x => !(x < 2)) ;
console.log(dropped_bytook.take(10)); // [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
console.log(dropped_bytook.take(10)); // [13, 14, 15, 16, 17, 18, 19, 20, 21, 22] ????
还有其他一些有趣的事情......
const fibonacci = Stream.iterate([0, 1], ([a, b]) => [b, a + b]).map(([x]) => x) ;
console.log(fibonacci.take(6 + 4 + 2 + 4 + 2 + 4 + 2));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657]
const xs = fibonacci.dropping(6).drop(4).map(x => [x]) ;
const xxs = fibonacci.dropping(6).map(x => [x]).drop(4) ;
console.log(xs.take(2)); // [[55], [89]] : 10 dropped with map once, and ...
console.log(xs.take(2)); // [[987], [1597]] : 4 more dropped with no more map before take ...
console.log(xs.take(2)); // [[17711], [28657]] ; 4 more dropped with no more map before take ...
console.log(xxs.take(2)); // [[55], [89]] : 10 dropped with map once, and ...
console.log(xxs.take(2)); // [[987], [1597]] : 4 more dropped with no more map before take ...
console.log(xxs.take(2)); // [[17711], [28657]] ; 4 more dropped with no more map before take ...
const ys = fibonacci.dropping(6).dropping(4).map(x => [x]) ;
const yys = fibonacci.dropping(6).map(x => [x]).dropping(4) ;
console.log(ys.take(2)); // [[55], [89] : 10 dropped with map once, and ...
console.log(ys.take(2)); // [[144], [233]] : just take 2 then, no more dropped ...
console.log(ys.take(2)); // [[377], [610]]] : just take 2 then, no more dropped ...
console.log(yys.take(2)); // [[55], [89] : 10 dropped with map once, and ...
console.log(yys.take(2)); // [[144], [233]] : just take 2 then, no more dropped ...
console.log(yys.take(2)); // [[377], [610]]] : just take 2 then, no more dropped ...
const zs = fibonacci.dropping(5).map(x => [x]).drop(4).dropping(1) ;
const zzs = fibonacci.dropping(6).map(x => [x]).drop(4).dropping(0) ;
console.log(zs.take(2)); // [[55], [89] : 10 dropped with map once, and ...
console.log(zs.take(2)); // [[144], [233]] : just take 2 then, no more dropped ...
console.log(zs.take(2)); // [[377], [610]]] : just take 2 then, no more dropped ...
console.log(zzs.take(2)); // [[55], [89] : 10 dropped with map once, and ...
console.log(zzs.take(2)); // [[144], [233]] : just take 2 then, no more dropped ...
console.log(zzs.take(2)); // [[377], [610]]] : just take 2 then, no more dropped ...
总结
所以,有两种类型:Stream
- 一个是看起来不可变的:它的方法没有副作用。
take
- 如果你通过:它的方法对实例本身有副作用,那么它看起来是可变的。
dropping
take
stream
问题是,为什么返回的流或看起来不纯净,而所有由其他方法返回的流也由相同的形式 new Stream(( function* (this: Stream<T>): Generator<T> { /* ...一些产量 ... */ } ).bind(this))
不会对此实例本身产生副作用。take
took
tookUntil
take
我在 TS Playground (v5.1.6) 上尝试了所有代码,这是我的测试:https://tsplay.dev/Wklk0w。
我刚才已经阅读了迭代器助手,所以我知道我的实现可能很丑陋......
答:
代码的主要问题是您正在尝试重用迭代器。但是迭代器是有状态的,一旦你调用了,迭代器就会前进,你不一定能从同一个迭代器中获取早期信息。因此,您永远不想使用现有迭代器创建新的迭代器,也不允许任何方法使用现有迭代器。相反,您需要为每个操作生成一个新的迭代器。iterator.next()
Stream
Stream
由于您已经有一个生成器函数(),因此您可以直接调用它来获取新的迭代器。这意味着当你创建一个新的生成器函数时,你应该创建一个新的生成器函数来配合它。Stream
function*(⋯){⋯}
Stream
下面是原始代码的精简版本,它为返回新 s 的方法执行此操作。例如,保留对原始生成器函数的引用,然后在新的生成器函数中调用它:Stream
dropUntil()
dropUntil(when: Fn<T, boolean>): Stream<T> {
const gen = this.generatorFunction;
return new Stream
(function* (): Generator<T> {
const iterator = gen();
while (true) {
const { value: head, done } = iterator.next();
if (done) return;
if (when(head)) {
yield head;
break;
}
}
while (true) {
const { value, done } = iterator.next();
if (done) break;
yield value;
}
});
};
并且需要做一些类似的事情,以免持久化变量的状态:drop()
count
drop(limit: number): Stream<T> {
const thiz = this;
return new Stream(function* () {
let count = 0;
yield* thiz.dropUntil(() => !(count++ < limit));
});
}
(使用 yield*
委托给生成器。dropUtil
现在我们可以看到它按预期工作:
const ps = Stream.iterate(2, x => x + 1).dropUntil(x => x > 5);
console.log(ps.take(10)); // [6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
console.log(ps.take(10)); // [6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
const qs = Stream.iterate(2, x => x + 1).drop(7);
console.log(qs.take(10)); // [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
console.log(qs.take(10)); // [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
console.log(qs.take(10)); // [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
const fibonacci = Stream.iterate([0, 1], ([a, b]) => [b, a + b]).map(([x]) => x);
const fs = fibonacci.drop(10);
console.log(fs.take(6)); // [55, 89, 144, 233, 377, 610]
console.log(fs.take(6)); // [55, 89, 144, 233, 377, 610]
console.log(fs.take(6)); // [55, 89, 144, 233, 377, 610]
请注意,此实现会多次执行所有计算,并且不会尝试缓存任何结果以供以后重用。您可以编写自己的可迭代包装器,该包装器采用迭代器并将其元素缓存在数组中以供重放,但我认为这超出了所提问题的范围。
评论
是什么意思?(多亏了你,我试图清理我的问题中的一些多余部分:))
iterator
iterator.next()
iterator.next()
[1,2,3,4,...]
iterator.next()
4
5
stream
map
dropUntil
take
took
took
stream
took(...)[1]
map
dropUntil
took
我找到了差异的重点,需要这种混乱的答案!
我只是添加一个方法。这里有 2 个案例
案例一:
readonly dropX =
(when: Fn<T, boolean>)
: Stream<T> =>
{
// const iterator = this.generatorFunction() ;
return new Stream
(( function* (this: Stream<T>)
: Generator<T>
{
const iterator = this.generatorFunction() ;
while (true)
{
const { value: head, done } = iterator.next();
if (done) break;
if (when(head)) { yield head ; break; } ;
} ;
while (true)
{
const { value, done } = iterator.next();
if (done) break;
yield value ;
} ;
} ).bind(this)) ;
} ;
案例二:
readonly dropX =
(when: Fn<T, boolean>)
: Stream<T> =>
{
const iterator = this.generatorFunction() ;
return new Stream
(( function* (this: Stream<T>)
: Generator<T>
{
// const iterator = this.generatorFunction() ;
while (true)
{
const { value: head, done } = iterator.next();
if (done) break;
if (when(head)) { yield head ; break; } ;
} ;
while (true)
{
const { value, done } = iterator.next();
if (done) break;
yield value ;
} ;
} ).bind(this)) ;
} ;
它们具有相同的测试代码:
const x = Stream.iterate(2, x => x + 1).dropX(x => x > 3) ;
console.log(x.take(5)); // [4, 5, 6, 7, 8]
console.log(x.take(5)); // case one: [4, 5, 6, 7, 8] or case two: [9, 10, 11, 12, 13]
尝试一些总结
看起来 is 的差异取决于在哪里被调用:Stream
generatorFunction
- 如果它是新定义的,那么这个函数在调用时看起来是不可变的。
function*
Stream
take
- 如果它在新定义之外,那么这个函数的 made 将看起来是可变的,并且在调用时 mudt 会静音。
function*
Stream
take
我不知道为什么这个功能的更多原因,但它完成了我的任务。我最想要的就是控制这种不同的行为,就是这样。
(而且因为“我真的不知道为什么”,这个答案不会最有帮助,这个问题仍然会打开。
这是我的链接: https://tsplay.dev/WPBKZN
和。。。缓存功能怎么样?我宁愿对 take 方法使用 memoize,仅当形式 as 与 JS/TS 中的格式完全相同时,而定义为 .a.b(c,d)
b(a,c,d)
b
b(this, x, xx)
但这是另一个问题。:)
评论
dropUntil
Stream
took
Stream
dropUntil
next()
done
drop
dropUntil
take
Stream
完成
,这很糟糕。所以。。。重用已经消耗的迭代器与重新运行新的生成器不同吗?谢谢,这是我可能想要的答案......took
take
drop
took