After the announcement of C # 5.0 with its async and await, I had a slight interest in asynchronous programming, digging towards which was the origin of this text.
In the process of reading the article, you learn / mind.
- What is a monad on the example of Maybe.
- What is CPS and the continuation monad.
- How to associate it with the Task class from the TPL.
Monads
The monad is essentially composed of three parts.
- A container that contains something.
- The function with which you can put something in a container.
- The function that allows you to pull something out of the container and apply another function that will return the new container.
Usually these two functions are called return and bind, but in Sharp we will call them To [Monad Name] and SelectMany.
Maybe monad
Consider the Maybe monad, the twin brother of Nullable. A container that can either contain a value or contain nothing.
public class Maybe<T> { public static readonly Maybe<T> Nothing = new Maybe<T>(); public T Value { get; private set; } public bool HasValue { get; private set; } private Maybe() { HasValue = false; } public Maybe(T value) { Value = value; HasValue = true; } public override string ToString() { if(HasValue) return Value.ToString(); return "Nothing"; } }
One of the useful properties of this monad, if somewhere in the expression met Nothing, then the whole expression will be Nothing.Here are our two functions.
public static class MaybeEx { public static Maybe<T> ToMaybe<T>(this T value) { return new Maybe<T>(value); } public static Maybe<U> SelectMany<T, U>(this Maybe<T> m, Func<T, Maybe<U>> k) { if (m.HasValue) return k(m.Value); return Maybe<U>.Nothing; } }
With ToMaybe, everything is clear, with SelectMany, it seems, too, take the container m, and apply the function k to its contents if there is a value.
Suppose we want to add two numbers
var a = 1.ToMaybe(); var b = Maybe<int>.Nothing; Func<int, Maybe<int>> k = x => b.SelectMany(y => (y + x).ToMaybe()); var result = a.SelectMany(k);
We see that before folding we had to use SelectMany to pull the values out of the container twice. To get rid of the clutter, there is sugar in some languages, in Haskell do notation, F # computation expressions, well, we can remember that the expression
from x in a from y in b select x + y;
it
a.SelectMany(x => b, (x, y) => x + y);
That is, by implementing an additional SelectMany view
public static Maybe<V> SelectMany<T, U, V>( this Maybe<T> m, Func<T, Maybe<U>> k, Func<T, U, V> s) { return m.SelectMany(x => k(x).SelectMany(y => s(x, y).ToMaybe())); }
which just in itself also contains that double disclosure of containers, can write operations with Maybe in the form.
var result = from x in 1.ToMaybe() from y in 2.ToMaybe() select x + y;
CPS (Continuation-passing style)
')
CPS is essentially work with callbacks, that is, instead of using the normal calculation order
var x = GetPage("http://habrahabr.ru"); var y = Translate(x); Display(y);
Each function has an additional parameter to which we pass the function to continue the calculation.
GetPage("http://habrahabr.ru", x => Translate(x, y => Display(y)));
Often used if the function makes an asynchronous request somewhere and we do not want the thread to idle or to get a gui.
Mixing the monad and CPS we get
Continuation monad
A more or less standard version of the monad, this is a function that accepts another function as input, we restrict ourselves to the simple case when the output from the continuation is void. Then the monad can be represented as such delegates.
public delegate void Cont<T>(Action<T> k);
Functions at the input is a function k (continued), into which something like T is already transmitted. Here are our three familiar functions.
public static class ContEx { public static Cont<T> ToCont<T>(this T value) { return k => k(value); } public static Cont<T1> SelectMany<T, T1>(this Cont<T> c, Func<T, Cont<T1>> f) { return k => c(r => f(r )(k)); } public static Cont<T2> SelectMany<T, T1, T2>(this Cont<T> c, Func<T, Cont<T1>> f, Func<T, T1, T2> s) { return c.SelectMany(x => f(x).SelectMany(y => s(x, y).ToCont())); } }
ToCont - in the continuation of k just pass the value. SelectMany - we pull out r from the monad, pass it to the function f and call the new monad with the general continuation k.
Task
It's time to remember what Task is. This is a container that contains a certain action in itself, the result of which can be obtained using the Result property by blocking the stream, or using the ContinueWith method in CPS.
Create a task.
var task = Task<int>.Factory.StartNew(() => 10);
We get the result
task.ContinueWith(task => Display(task.Result));
Once there is a CPS in Task, you can try one to convert to another.
public static Cont<T> ToCont<T>(this Task<T> task) { return k => task.ContinueWith(task => k(task.Result)); }
Example task
Suppose we have three asynchronous services. Get the content of the page by url, translate the text and make an analysis of the text the result of which will be a number.
public static Task<string> GetPage(string url) { return Task<string>.Factory.StartNew(() => { Console.WriteLine("get page start " + url); Thread.Sleep(3000); Console.WriteLine("get page complete"); return "page content"; }); } public static Task<string> Translate(string text) { return Task<string>.Factory.StartNew(() => { Console.WriteLine("translate start"); Thread.Sleep(3000); Console.WriteLine("translate complete"); return "text translation"; }); } public static Task<int> Analyse(string text) { return Task<int>.Factory.StartNew(() => { Console.WriteLine("analyse start"); Thread.Sleep(3000); Console.WriteLine("analyse complete"); return 100; }); }
Imagine in your mind how the passage through these three functions in CPS will look like, horrified, and immediately use the link, converting the tasks into monads.
var result = from x in GetPage("www.example.com").ToCont() from y in Translate(x).ToCont() from z in Analyse(y).ToCont() select z; result(Display);
If we need to get the translated page by url somewhere else, it can be put into a separate method
private static Cont<string> GetPageTranslation(string url) { return from x in GetPage(url).ToCont() from y in Translate(x).ToCont() select y; } var result = from x in GetPageTranslation("http://habrahabr.ru") from a in Analyse(x).ToCont() select a; result(Display);
Since ToCont is used everywhere, then why not try to get rid of the redundant entity.
public static Task<T1> SelectMany<T, T1>(this Task<T> c, Func<T, Task<T1>> f) { return c.ContinueWith(task => f(task.Result)).Unwrap(); } public static Task<T2> SelectMany<T, T1, T2>(this Task<T> c, Func<T, Task<T1>> f, Func<T, T1, T2> s) { return c.ContinueWith(t => f(t.Result).ContinueWith(x => s(t.Result, x.Result))).Unwrap(); }
Unwrap is needed to deploy a type.
Task<Task<T>>
which is obtained by combining two tasks.
Now removing all ToCont, you can get the same result.
private static Task<string> GetTranslation(string url) { return from x in GetPage(url) from y in Translate(x) select y; }
So, we can assume that Task is essentially a continuation monad.
Well and for example we will write function which returns for the list of url, the list of values Analyze. Think how it looked in CPS.
private static Task<IEnumerable<int>> Analyse(IEnumerable<string> list) { var result = from url in list select from x in GetTranslation(url) from a in Analyse(x) select a; return Task.Factory.ContinueWhenAll(result.ToArray(), x => x.Select(y => y.Result)); }
ContinueWhenAll - waiting for all tasks to complete, to get all the results.
var urls = new[] {"url1", "url2", "url3"}; var r = Analyse(urls); r.ContinueWith(x => Console.WriteLine(x.Result.Sum()));
Conclusion
It is unlikely that all of this has any practical meaning, therefore this text can be considered as information for reflection from the category of abnormal programming. Also, everyone can reflect on the interfaces IObservable and IObserver and the method IObserver.OnNext (T value)
PS For good, it wouldn’t hurt to touch on the very same async with await, but I don’t have the desire to put ctp, so I have quite superficial ideas about them, but I think the point is not much different from the above, i.e. with tasks, but instead of a link, the computation flow conversion is used by the compiler.