(日文/TS)为什么我的生成器函数代码在幂等性上有不同的行为?

(JS/TS) Why my generator function code have different behavior on the idempotence?

提问人:ypa y yhm 提问时间:8/17/2023 最后编辑:ypa y yhm 更新时间:8/28/2023 访问量:82

问:

定义

下面是一个由 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]

实现方法如下:droppingtook

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
  • 如果你通过:它的方法对实例本身有副作用,那么它看起来是可变的。droppingtakestream

问题是,为什么返回的流或看起来不纯净,而所有由其他方法返回的流也由相同的形式 new Stream(( function* (this: Stream<T>): Generator<T> { /* ...一些产量 ... */ } ).bind(this)) 不会对此实例本身产生副作用。taketooktookUntiltake


我在 TS Playground (v5.1.6) 上尝试了所有代码,这是我的测试:https://tsplay.dev/Wklk0w

我刚才已经阅读了迭代器助手,所以我知道我的实现可能很丑陋......

(语言相同的问题 zh-cn)

JavaScript TypeScript 迭代器 生成器

评论

2赞 jcalz 8/17/2023
(1) 欢迎来到 Stack Overflow!请将代码编辑最小的可重现示例;如果可以通过一些方法证明问题,则不需要包含许多方法。(2) 您错误地重用了对迭代器的引用,这些迭代器是有状态的。您需要重构以仅使用迭代器一次,可能如下所示。是的,每次都会重做所有迭代。如果你想记住一些东西以使其更快,那很好,但超出了所问问题的范围。这是否完全解决了这个问题?如果是这样,我会写一个答案来解释;如果没有,我错过了什么?
0赞 ypa y yhm 8/18/2023
@jcalz 谢谢,我正在编辑以使其最小和清晰。我希望迭代器是不可变的,关键是,我认为它应该既是可变的,又应该是不可变的,但实际上它可能是其中之一,我不明白为什么。为什么我通过采取来实现,那么它将是可变的,但是当我直接实现它时,它不会,我没有看到任何差异,返回与直接实现的返回相似......我只想知道它的规则......dropUntilStreamtookStreamdropUntil
0赞 jcalz 8/18/2023
迭代器是有状态的,因此是可变的。不可变迭代器没有多大意义(它必须为所有调用返回相同的值!如果要重迭代一个可迭代对象,则需要获取一个新的迭代器。调用生成器函数会给出一个新的迭代器,这很好。当你重用一个已经被消耗的迭代器时,它会在你开始的时候立即使用,这很糟糕。我展示了一种修复它的方法,即始终重新运行生成器并且从不重用现有的迭代器。这能解释吗?我应该写下我的答案还是我遗漏了什么?next()done
0赞 ypa y yhm 8/18/2023
@jcalz我只是测试了你给我的代码,发现了另一个问题(如果我像 一样通过新的实现),你可以看到这个,同样的问题也出现在我自己身上......dropdropUntiltakeStream
1赞 ypa y yhm 8/18/2023
@jcalz你说“当你重用一个已经消耗过的迭代器时,它会在你开始的时候就完成,这很糟糕。所以。。。重用已经消耗的迭代器与重新运行新的生成器不同吗?谢谢,这是我可能想要的答案......tooktakedroptook

答:

1赞 jcalz 8/18/2023 #1

代码的主要问题是您正在尝试重用迭代器。但是迭代器是有状态的,一旦你调用了,迭代器就会前进,你不一定能从同一个迭代器中获取早期信息。因此,您永远不想使用现有迭代器创建新的迭代器,也不允许任何方法使用现有迭代器。相反,您需要为每个操作生成一个新的迭代器。iterator.next()StreamStream

由于您已经有一个生成器函数(),因此您可以直接调用它来获取新的迭代器。这意味着当你创建一个新的生成器函数时,你应该创建一个新的生成器函数来配合它。Streamfunction*(⋯){⋯}Stream

下面是原始代码的精简版本,它为返回新 s 的方法执行此操作。例如,保留对原始生成器函数的引用,然后在新的生成器函数中调用它:StreamdropUntil()

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] 

请注意,此实现会多次执行所有计算,并且不会尝试缓存任何结果以供以后重用。您可以编写自己的可迭代包装器,该包装器采用迭代器并将其元素缓存在数组中以供重放,但我认为这超出了所提问题的范围。

Playground 代码链接

评论

0赞 ypa y yhm 8/18/2023
谢谢。但是,正如您在评论中所说,“重用已经消耗的迭代器”才是真正的重点。那么,为什么已经消耗过的迭代器在我用它来制作新的迭代器时会有不同的行为呢?“it will be done as once you start”是什么意思?(多亏了你,我试图清理我的问题中的一些多余部分:))
0赞 jcalz 8/18/2023
关键是我所说的,s 是有状态的,并且会改变迭代器,你不一定能“倒带”它。已经完全消耗掉的是一种特例(例如,直到完成)。生成器函数本身不是迭代器,而是迭代器。因此,您可以根据需要重用生成器函数,但每个结果只能使用一次。iteratoriterator.next()iterator.next()
0赞 ypa y yhm 8/18/2023
例如。。。首先,我有一个迭代器,它是 ,然后,我取两个元素。这将使下一个成为,它的下一个是......所以,我想我可以将这个迭代器实例用作生成器函数的参数(或其他一些东西),并让它创建一个新的迭代器实例。我知道迭代器必须是可变的,所以我只是想创建一个新的实例,使我的实例看起来像是不可变的,就像 or new 方法所做的那样(如果它们是......[1,2,3,4,...]iterator.next()45streammapdropUntil
0赞 jcalz 8/18/2023
对不起,我不太理解你(也许是语言障碍?),我已经尽我所能向你解释了发生了什么,多次以多种方式。我不认为我知道如何做得更好。你不需要接受我的回答,但我认为我现在需要脱离接触,因为进一步的讨论似乎对我们双方都没有用。祝你好运。
0赞 ypa y yhm 8/20/2023
或。。。我很抱歉。事情很简单:(1),我需要一个下降方法;(2)、可以通过以下逻辑实现(我称之为:实现 a 返回 2 个东西,一个用于取一个用于丢弃);(3)、本来是到这里过来的,但是我发现了一个问题:返回的对象和另一个对象不一样(比如return),我想知道为什么。(4)、你的返回(我在这里不是说迭代器对象)看起来是不可变的,但是如何通过使滴落的方式做同样的事情呢?我真的很想看看如何在方法上做同样的事情......taketooktookstreamtook(...)[1]mapdropUntiltook
0赞 ypa y yhm 8/24/2023 #2

我找到了差异的重点,需要这种混乱的答案!

我只是添加一个方法。这里有 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 的差异取决于在哪里被调用:StreamgeneratorFunction

  • 如果它是新定义的,那么这个函数在调用时看起来是不可变的。function*Streamtake
  • 如果它在新定义之外,那么这个函数的 made 将看起来是可变的,并且在调用时 mudt 会静音。function*Streamtake

我不知道为什么这个功能的更多原因,但它完成了我的任务。我最想要的就是控制这种不同的行为,就是这样。

(而且因为“我真的不知道为什么”,这个答案不会最有帮助,这个问题仍然会打开。

这是我的链接: https://tsplay.dev/WPBKZN


和。。。缓存功能怎么样?我宁愿对 take 方法使用 memoize,仅当形式 as 与 JS/TS 中的格式完全相同时,而定义为 .a.b(c,d)b(a,c,d)bb(this, x, xx)

但这是另一个问题。:)