This project is read-only.

Hello World

Consider the following method which yields parts of the string "Hello World":

        private static IEnumerable<string> HelloWorld()
        {
            yield return "Hello";
            yield return " ";
            yield return "World";
        }


We could evaluate this immediately and output the complete string as follows:

        foreach (string s in HelloWorld())
            Console.Write(s);


This would output the string Hello World to the console.

With TaskBuilder, we instead link HelloWorld and Console.Write and let Console.Write ask for the next word asynchronously:

        using (TB.Scope())
        {
            var helloWorld = TB.Func(HelloWorld);
            TB.Act(Console.Write, helloWorld);
        }


Here we wrap the reference to HelloWorld in a function (Func) operator and pass it to an action (Act) operator. The Console.Write action operator requests a string from the HelloWorld function operator, the HelloWorld enumerator yields the next string and it is passed to Console.Write to output it, then the process repeats, until HelloWorld yields no more strings.

Both the enumerator returned by HelloWorld and Console.Write are executed as asynchronous Tasks. The TaskBuilder code takes care of creating the necessary tasks and kicking off their execution so the program author does not have to worry about creating specific tasks - only defining the functional relationships.

This is a very simple example. Data flow can also branch (the output of a method is sent to several methods) and merge (the output of several methods is sent to one method). TaskBuilder creates tasks that wait until all the inputs are ready and then execute.

Joining

Join All

We can bring together two inputs and pass them to a single method using a join. Consider the following simple methods:

        private IEnumerable<string> InputA()
        {
            yield return "Hello";
        }

        private IEnumerable<string> InputB()
        {
            yield return "World";
        }

        private void Output(Tuple<string, string> inputs)
        {
            Console.WriteLine("{0} {1}", inputs.Item1, inputs.Item2);
        }


Suppose that we want the strings produced by InputA and InputB to be passed to Output, we can use the All join operator as follows:

        TB.Act(Output, TB.All(TB.Func(InputA), TB.Func(InputB)));


This will cause Output to request from InputA and InputB and to execute Output when the inputs are ready. In this case that will happen immediately, but for a more complex example the inputs may arrive at different times and we don't want to block a thread waiting for them. This produces the output:

Hello World

Join Any

An alternative way of joining two sources is to trigger when either one of them arrives and this can be done by replacing All with Any:

        TB.Act(Output, TB.Any(TB.Func(InputA), TB.Func(InputB)));


In this case we need to handle nulls in our output method because only one input will be provided for each call to output:

        private void Output(Tuple<string, string> inputs)
        {
            Console.WriteLine("{0} {1}", inputs.Item1 ?? "-", inputs.Item2 ?? "-");
        }


This produces the output:

Hello -
- World

Group All

If we know that all the types are the same from the various inputs then we can use a simpler operator (GroupAll), which puts the inputs into an array rather than a Tuple:

        TB.Act(Output, TB.GroupAll(TB.Func(InputA), TB.Func(InputB)));


This requires us to change our Output method as follows:

        private void Output(string[] inputs)
        {
            Console.WriteLine("{0} {1}", inputs[0], inputs[1]);
        }


The console output is:

Hello World

Group Any

The equivalent Any case becomes even simpler where the types are the same by using the GroupAny operator. In this case the inputs are provided one at a time and because all the inputs are strings, the Output method simply takes a single string parameter. In fact, Console.WriteLine already has such an overload so we can call it directly:

        TB.Act(Console.WriteLine, TB.GroupAny(TB.Func(InputA), TB.Func(InputB)));


This produces the console output:

Hello
World

Branching

As well as joining inputs, it is useful to branch the output of a method to several others. This can be achieved using the Branch operator:

        var inputs = TB.Branch(TB.GroupAny(TB.Func(InputA), TB.Func(InputB)));
        TB.Act(Console.WriteLine, inputs);
        TB.Act(Console.WriteLine, inputs);


The Branch operator ensures that its input is replicated to each consumer that is connected to it. In this example, for simplicity, we send the inputs to the same method twice but more usually these would be different methods that require the same inputs. The above program generates the following console output:

World
Hello
Hello
World


(Note that the ordering may change depending on how the tasks are scheduled).

Functions

We have already seen that a generator function can be wrapped in a Func operator to enable it to be called asynchronously but what if we want to do the same with a transfer function? Consider the following transfer function that modifies a string to append " World" to the end:

        private string Modify(string input)
        {
            return input + " World";
        }


The syntax for invoking this asynchronously with TaskBuilder is essentially the same as for the generator function but with an extra parameter providing the input source:

        TB.Act(Console.WriteLine, TB.Func(Modify, TB.Func(InputA)));


This instructs TaskBuilder to hook up the output of InputA to the input of the Modify method and the output of the Modify method to Console.WriteLine, producing the following unsurprising result:

Hello World

Forward declarations

Sometimes it is necessary to pipe the output of a method back into its input and therefore it is necessary to pass a producer as an input before it exists:

        // error, 'input' is not assigned yet! ...
        var input = TB.Branch(TB.GroupAny(input, TB.Func(InputA)));


To achieve this, TaskBuilder provides a Point object that acts as a temporary placeholder for the producer and can be assigned later:

        var p = TB.Point<string>();
        // this is ok, p has been assigned ...
        var input = TB.Branch(TB.GroupAny(p, TB.Func(InputA)));
        p.Attach(input);


In this example, p is created as a valid producer Point and can be passed in to other operators without a problem. However, p must be attached to a real producer using its Attach method, as shown in the third line here, or the program will fail at runtime.

Because the input is looped around in the above example, it will execute continuously and attaching a Console.WriteLine method to the branch point input as follows:

         TB.Act(Console.WriteLine, input);


Yields the following endlessly repeating console output:

Hello
Hello
Hello
Hello
Hello
...

Pushed inputs

The inputs used so far have been pulled from an enumerator. It is also possible to push an input by declaring an Input<T> and calling Write(t) on it:

        var thrdIn = TB.Input<string>();
        var t = new Thread(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    thrdIn.Write("Hello");
                    Thread.Sleep(1000);
                    thrdIn.Write(" World\n");
                    Thread.Sleep(1000);
                }
            });
        t.Start();
        TB.Act(Console.Write, thrdIn);


This program creates and starts a thread that periodically writes "Hello" and " World\n" to the Input<string> called thrdIn. That input is then passed in the usual way to TB.Act to hook it up to Console.Write resulting in the following output:

Hello World
Hello World
Hello World
...

Last edited Apr 4, 2015 at 9:39 AM by jaorme, version 14