5

After a previous question on stackoverflow regarding async / await it seemed to me that await was much more powerful and general than the marketing suggested. It seems to be a general method of building computation expressions just like in F#. So after a bit of a struggle I came up with some code that successfully executes as below.

    using FluentAssertions;
    using System.Collections.Generic;

    namespace EnumerableViaAwait.Specs
    {
        [global::Microsoft.VisualStudio.TestTools.UnitTesting.TestClass]
        public class MyTestClass
        {
            public IEnumerable<int> Numbers()
            {
                return EnumeratorMonad.Build<int>(async Yield =>
                {
                    await Yield(11);
                    await Yield(22);
                    await Yield(33);
                });
            }

            [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod]
            public void TestEnum()
            {
                var v = Numbers();
                var e = v.GetEnumerator();

                int[] expected = { 11, 22, 33 };

                Numbers().Should().ContainInOrder(expected);

            }

        }
    }

Now carefully note what is going on here. I am NOT building a reactive observable. I am building an IEnumerable. It is strictly a pull system. I can quite happily write.

    foreach item in Numbers(){
            Console.WriteLine(item);
    }

and it will print out

    11
    22
    33

This is very interesting because the system is not strictly asynchronous but I'm abusing the await framework and the ability to "await anything" as outlined here. http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx.The question(s) are.

  1. How far can this abuse of await / async go?
  2. Is this pattern as powerfull as F# computation expressions
  3. Is traditional IEnumerator / Yield pattern just syntactic sugar that evaluates exactly to this pattern

The code implementing the pattern is below

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Text;
    using System.Threading.Tasks;

    namespace EnumerableViaAwait
    {

        public class EnumeratorMonad<T> : IEnumerable<T>, IEnumerator<T>
        {
            public class Yield
            {
                private EnumeratorMonad<T> _Monad;

                public Yield(EnumeratorMonad<T> monad)
                {
                    _Monad = monad;
                }

                public YieldAwaiter GetAwaiter()
                {
                    return new YieldAwaiter(_Monad);
                }
            }

            public class YieldAwaiter : INotifyCompletion
            {
                EnumeratorMonad<T> _Monad;

                public YieldAwaiter(EnumeratorMonad<T> monad)
                {
                    _Monad = monad;
                }

                public bool IsCompleted
                {
                    get { return _Monad.IsCompleted(); }
                }

                public void GetResult()
                { }

                public void OnCompleted(Action continuation)
                {
                    _Monad.Next = continuation; 
                }

            }

            private bool Completed { get; set; }

            public EnumeratorMonad()
            {
                Completed = false;
            }

            public bool IsCompleted()
            {
                return Completed;
            }

            public void Build(Func<Func<T, Yield>, Task> action)
            {
                Func<T, Yield> yielder = (T value) => { 
                    _Current = value;
                    return new Yield(this);
                };
                Next = async () => { 
                    await action(yielder);
                    Completed = true;
                };
            }

            private T _Current;
            public T Current
            {
                get { return _Current; }
            }

            public void Dispose()
            {
            }

            object System.Collections.IEnumerator.Current
            {
                get { return _Current; }
            }


            Action Next;
            public bool MoveNext()
            {
                if (!Completed )
                {
                    Next();
                }
                return !Completed;
            }

            public void Reset()
            {
                throw new NotImplementedException();
            }



            IEnumerator<T> IEnumerable<T>.GetEnumerator()
            {
                return this;
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return this;
            }
        }

        public class EnumeratorMonad{
            public static EnumeratorMonad<T> Build<T>(Func<Func<T, EnumeratorMonad<T>.Yield>, Task> action)
            {
                var monad = new EnumeratorMonad<T>();
                monad.Build(action);
                return monad;
            }
        }

}

Steve Czetty
  • 6,147
  • 9
  • 39
  • 48
bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217

1 Answers1

5

yield return and await/async are just different specialized forms of coroutines. You've shown that you can (essentially) implement yield return using await/async, and I would not be surprised to find that it were possible the other way around. I'm sure that they are implemented in a very similar manner.

In practice, of course, I would not use await/async for iteration, since yield return is much simpler and more clear.

So,

  1. You can probably take this "abuse" as far as you want.
  2. Not sufficiently familiar with F# to answer.
  3. No, but IIRC the features are implemented in more or less the same way.
Thom Smith
  • 13,916
  • 6
  • 45
  • 91
  • What I'm trying to figure out is whether async/await are "specialized" or are they totally generic and can be used to implement more interesting workflows than just IEnumerator or Async. I don't think you can implement async / await in terms of yield return as yield return does not return an lvalue. – bradgonesurfing Oct 22 '12 at 14:00
  • You're probably right in that you couldn't get the syntax as pretty the other way. As far as what you can do with them -- probably anything, but the best way to tell is to try it. – Thom Smith Oct 22 '12 at 14:30
  • 1
    I'm going to try the Maybe monad next. I think I can write something like Do notation. The sequence should terminate early with Maybe::Nothing if any Maybe::Nothings are returned. It's not super pretty but it might be good for parsing XML – bradgonesurfing Oct 22 '12 at 14:35
  • One *huge* benefit to this pattern - one that might make up for the reduction is readability - is that it allows you to create anonymous enumerable generators (as in the given example code), which the `yield return` keyword does not permit. – Erik Forbes Jul 09 '13 at 22:39