Iterators and async

How loop walks collections, functions that yield their values one at a time, async functions, await and tasks.

Last updated 7 min read Edit on GitHub

On this page

What loop walks

loop (T x in c) works on:

c What x takes
An array, of any number of dimensions Each element, in row order.
A str Each code unit (a u8: str is UTF-8).
Any value with an Iterator() method What the iterator gives.

For the last kind, the compiler calls c.Iterator() once, then, while the iterator's Next() returns true, reads its Current property into x and runs the body. There is no interface to implement: any type with those three members works, which is how List<T>, Dictionary<K, V>, HashSet<T> and the other collections are walked.

AntrilRun
import System;

public class Countdown
{
	i32 from;

	public:
		new(i32 from) { this.from = from; }
		Iterator() : CountdownIterator => new CountdownIterator(from);
}

public class CountdownIterator
{
	i32 next;
	i32 current;

	public:
		new(i32 start) { next = start; }

		Next() : bool
		{
			if (next < 0)
				return false;
			current = next;
			next--;
			return true;
		}

		i32 Current { get => current; }
}

Main()
{
	loop (i32 n in new Countdown(3))
		Console.Write(n + " ");
	Console.WriteLine();
	loop (u8 unit in "hé")
		Console.Write(unit + " ");
	Console.WriteLine();
}
Terminal
larva run
3 2 1 0 
104 195 169 

é is two units in UTF-8. The loop variable's type converts from Current's type as an assignment would.

Functions with yield

A function, method or static method whose result is Iterator<T> can give its values one at a time:

Statement What it does
yield value; Gives the next value and stops there until the next one is asked.
yield break; Ends: no more values.
return; Ends too, like reaching the end of the body.
AntrilRun
import System;

public class Bag
{
	List<str> items = new List<str>();

	public:
		Add(str s) { items.Add(s); }

		Each() : Iterator<str>
		{
			loop (str s in items)
			{
				if (s == "stop")
					yield break;
				yield s;
			}
		}
}

Evens(i32 limit) : Iterator<i32>
{
	loop (i32 i = 0..limit)
		if (i % 2 == 0)
			yield i;
}

Main()
{
	Bag b = new Bag();
	b.Add("a");
	b.Add("b");
	b.Add("stop");
	b.Add("c");
	loop (str s in b.Each())
		Console.Write(s);
	Console.WriteLine();

	Iterator<i32> it = Evens(7);
	it.Next();
	Console.WriteLine(it.Current);
	loop (i32 e in it)
		Console.Write(e + " ");
	Console.WriteLine();
}
Terminal
larva run
ab
0
2 4 6 
  • Calling the function runs nothing yet: it gives a System.Iterator<T>. Each Next() runs the body on to its next yield and returns true, or returns false once the body has ended (and from then on). Current is the value the last yield gave.
  • Iterator() on an iterator gives itself, so a loop over an iterator that was already advanced walks what is left of it.
  • An iterator runs once. Call the function again to start over.
  • The function's parameters, and this in a method, are kept in the iterator object for as long as it lives.
Code When
AN2110 A function with yield does not give an Iterator<T>.
AN2111 yield is inside a lambda (it belongs to a function's own body).
AN2112 A function with yield returns a value: it ends with yield break; or return;.
AN2113 yield is inside a catch.

async functions

A function or method marked async gives a Task (no result) or a Task<T> (a result of type T), both in System.Threading.Tasks. Inside it, await task waits for another task without blocking the others, and is the task's result (nothing, for a Task).

AntrilRun
import System;
import System.Threading.Tasks;

public class Downloader
{
	str name;

	public:
		new(str name) { this.name = name; }

		async Get(i32 size) : Task<str>
		{
			await Task.Delay(5);
			return name + ":" + size;
		}
}

async Twice() : Task<i32>
{
	i32 a = await Task<i32>.FromResult(20);
	i32 b = await Task.Run<i32>(() => 1 + 1);
	return a + b;
}

async Loud() : Task
{
	await Task.Yield();
	throw new InvalidOperationError("boom");
}

Main()
{
	Downloader d = new Downloader("file");
	Task<str> t = d.Get(3);
	Console.WriteLine(t.IsCompleted);
	Console.WriteLine(t.Result);
	Console.WriteLine(Twice().Result);
	Task bad = Loud();
	try
	{
		bad.Wait();
	}
	catch (InvalidOperationError e)
	{
		Console.WriteLine("caught " + e.Message + " " + bad.IsFaulted);
	}
}
Terminal
larva run
false
file:3
22
caught boom true
  • async goes on free functions, static functions and methods of classes and values. The body runs until its first await of a task that is not complete, and the caller gets the task at that point.
  • return value; in an async function completes its Task<T>; a plain Task returns nothing.
  • Task<T> needs a T that can be copied (T : copy).
  • An exception thrown in an async function is kept in its task, which is then faulted. await, Wait() and Result throw it again, as it was.
Code When
AN2114 An async function gives something other than Task or Task<T>.
AN2115 await outside an async function, or in a lambda inside one.
AN2116 await inside a catch.
AN2117 await on something that is not a Task or Task<T>.
AN1026 An async function without a body.

Tasks

Member What it does
Task.Delay(ms) A task that completes after ms milliseconds.
Task.Yield() Lets the other ready tasks run before continuing.
Task.WhenAll(tasks) A task that completes when every task of the array has.
Task.CompletedTask A Task that is already complete.
Task<T>.FromResult(value) A Task<T> that is already complete with value.
Task.Run(f), Task.Run<T>(f) Runs the function f on the thread pool: a task that completes when it returns (faulted if it throws).
t.Wait() Blocks until t completes; throws its exception if it failed.
t.Result Waits, then gives the value of a Task<T>.
t.IsCompleted, t.IsFaulted Whether it has finished, and whether it failed.

The scheduler

Async functions take turns on one thread. System.Threading.Tasks.Scheduler runs the ready tasks in order, then the timers that are due, and sleeps when nothing is ready until the next timer. Wait() and Result run the scheduler until their task is complete, which is how a program that is not async itself, such as Main, drives its tasks:

AntrilRun
import System;
import System.Threading.Tasks;

async Tick(str name, i32 times) : Task
{
	loop (i32 i = 0..times)
	{
		Console.WriteLine(name + " " + i);
		await Task.Yield();
	}
}

Main()
{
	Task[] both = [Tick("a", 2), Tick("b", 2)];
	Task.WhenAll(both).Wait();
}
Terminal
larva run
a 0
b 0
a 1
b 1

Only Task.Run uses other threads; its result comes back to the scheduler's thread. For other work on several threads, see System.Threading in the standard library.

Streams

System.IO streams (FileStream, MemoryStream, BufferedStream, StreamReader, StreamWriter) are synchronous.

Warning

Not yet available: asynchronous stream members (ReadAsync, WriteAsync, CopyToAsync…). Wrap a synchronous call in Task.Run to move it off the scheduler's thread.

Next: Reactive values.

Was this page helpful?

No address is stored; your user name is, if you are signed in.

Esc