0

Consider this snippet (much simplified than the original code):

    async IAsyncEnumerable<(DateTime, double)> GetSamplesAsync()
    {
       // ...

            var cbpool = new CallbackPool(
                HandleBool: (dt, b) => { },
                HandleDouble: (dt, d) =>
                {
                    yield return (dt, d);     //not allowed
                });

            while (await cursor.MoveNextAsync(token))
            {
                this.Parse(cursor.Current, cbpool);
            }
    }

    private record CallbackPool(
        Action<DateTime, bool> HandleBool,
        Action<DateTime, double> HandleDouble
        );

Then, the below Parse is just a behavior-equivalent of the original.

    Random _rnd = new Random();
    void Parse(object cursor, CallbackPool cbpool)
    {
        double d = this._rnd.NextDouble();  //d = [0..1)
        if (d >= 0.5)
        {
            cbpool.HandleDouble(new DateTime(), d);
        }
        else if (d >= 0.25)
        {
            cbpool.HandleBool(new DateTime(), d >= 0.4);
        }
    }

However, I do like the GetSamplesAsync code, but the compiler does not: the yield cannot be used within a lambda.

So, I changed the function as follows, although it became much less readable (and also error-prone):

    async IAsyncEnumerable<(DateTime, double)> GetSamplesAsync()
    {
       // ...

            (DateTime, double) pair = default;
            bool flag = false;
            var cbpool = new CallbackPool(
                HandleBool: (dt, b) => { },
                HandleDouble: (dt, d) =>
                {
                    pair = (dt, d);
                    flag = true;
                });

            while (await cursor.MoveNextAsync(token))
            {
                this.Parse(cursor.Current, cbpool);
                if (flag)
                {
                    yield return pair;
                }
                flag = false;
            }
    }

I wonder if there is a better way to solve this kind of pattern.

Mario Vernari
  • 6,649
  • 1
  • 32
  • 44

1 Answers1

1

The external flag/pair is pretty dangerous and unnecessary (and it forces a closure); it seems like this bool could be returned from the Parse method, for example:

await foreach (var item in cursor)
{
    if (Parse(item, cbpool, out var result))
        yield return result;
}

(everything could also be returned via a value-tuple if you don't like the out)

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • I totally agree with you, Marc, but I should change the Parse signature. I'd like to know if there's a decent way by keeping the Parse as-is. – Mario Vernari Dec 18 '20 at 09:11
  • @MarioVernari you could add the result state onto the `CallbackPool`? not quite as clean as functional, but IMO cleaner than the captures – Marc Gravell Dec 18 '20 at 09:15
  • Yes, I can do it. Definitely better than the current solution. Do you have any point that explains the problems on the yield with lambdas? – Mario Vernari Dec 18 '20 at 09:18
  • That is the same in non-async too; basically, `yield` is not simple - that is a *huge* rewrite of the method (by the compiler) into a state machine, with `yield` being the state change; it has a tough enough job rewriting *one* method into an iterator, but rewriting two co-dependent methods withe the `yield` in the wrong one: not even remotely viable; see: https://sharplab.io/#v2:EYLgtghgzgLgpgJwD4AEAMACFBGALAbgFgAoElAZiwCYMBhDAbxIxa0pQA4MBJAQSgCeAOwDGAUSEBXMIgjAANnAA8ASyEwAfBgDicGADUI8yXCj9hIgBQBKZqybFWTjADMA9ggyW1MDCowAvBiY+H4YShgArBihKgDUcbaOzik42FgA7H5EySwAviR5QA== for what happens here. – Marc Gravell Dec 18 '20 at 09:24