You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
450 lines
38 KiB
450 lines
38 KiB
<html> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>C# Promises</title> |
|
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABL1BMVEUAAAAAAAAAAAAAAAAAAAAQAAABBBwZAAAAFAUUAAAADwMAAAAAAAAAAAAAAAAAAAAAAAAABgIuAAAsAAAANQwAGAUAAg0AAAAAAAAAAAAAAAAAAAAAwzIAvzAEOvwEPP8ESP0EQ/0EP/0ABQvtAAADMfgAHpkA1DkAzjcAyzYAyDUAuC0AaBj4AADyAADpAADmAADjAADWAADLAABAAAAFS/8DNP8FTf4DNvwEOPYDK/ICIroCEF0A0DkAvC8AnyYAnCUAkCIAdxwAAhgAQQ8bBAECBAB4AQD7AADwAADfAADcAAC5AAC0AACIAABOAAADMf8DJcwCIbACHaMCGYsBD0EBDTYAlSEAjSEASREASBEAAw8TAgwAMQsCJQkALwgAQAT0AADhAABjAABeAABD9A36AAAAHHRSTlMAGK5/eP7B1syvpoloYFUTAfnr6ujkvaaUb10gmPaQKAAAAOZJREFUGNM1j9dWwlAQRSeFSBGw13tBTQA1HSLSpPdmV3r//29gILDfzj7rrDUDGxif2+l0+xjYcgT8WbGQyxWUc942l9XqpCfLWUrLHkA4q9NsWwqlhJCyF/cnpqrWpUqekOen7DEDN11N0+b1WhHzY2TAg6stSY2GWlMwv0X6LBzEdF2XjMqYYqbiBQrTMGJWqTTLy//TbwFcneYiOkonk+k/8fczxYK32zLfwy939w/BePzjhwfmtBV1BF9RBBKJlOjHw5Z7EQhlDgG5Wu0moYzH/u7aIX5hHR5ib3PLsYLAcn5A1tqnJNnWRf14AAAAAElFTkSuQmCC"> |
|
<style type="text/css"> |
|
body { |
|
font-family: sans-serif; |
|
margin: 0; |
|
padding: 0; |
|
} |
|
|
|
* { box-sizing: border-box; } |
|
|
|
#title { |
|
position: absolute; |
|
top: 0; left: 0; right: 0; height: 40px; |
|
border-bottom: 1px solid black; |
|
font-size: 28px; |
|
padding: 4px; |
|
} |
|
|
|
#toc { |
|
position: absolute; |
|
top: 40px; left: 0; bottom: 0; width: 200px; |
|
overflow: auto; |
|
padding: 4px; |
|
} |
|
|
|
#toc ul { |
|
padding: 0px 15px; |
|
font-size: 80%; |
|
margin: 5px; |
|
} |
|
#toc .sidebar-header-2 { |
|
margin-left: 10px; |
|
list-style-type: circle; |
|
} |
|
#toc .sidebar-header-3 { |
|
margin-left: 20px; |
|
list-style-type: circle; |
|
} |
|
|
|
#content { |
|
position: absolute; |
|
top: 40px; right: 0; bottom: 0; left: 200px; |
|
overflow: auto; |
|
border-left: 1px solid black; |
|
padding: 0px 18px; |
|
} |
|
|
|
code { |
|
background: #EEE; |
|
border: 1px solid #CCC; |
|
padding: 0 2px; |
|
margin: 0 2px; |
|
} |
|
|
|
pre code { |
|
display: block; |
|
margin: 2px; |
|
} |
|
|
|
h1 { |
|
font-size: 1.5em; |
|
border-bottom: 1px solid gray; |
|
} |
|
|
|
h2 { font-size: 1.2em; } |
|
h3 { font-size: 1.0em; } |
|
|
|
</style> |
|
|
|
</head> |
|
<body> |
|
|
|
<div id="title">C# Promises</div> |
|
|
|
<div id="toc"><ul class="nav nav-list"> |
|
<li class="sidebar-header-1"><a href="#c#-promises">C# Promises</a></li> |
|
<li class="sidebar-header-1"><a href="#c-sharp-promise">C-Sharp-Promise</a></li> |
|
<li class="sidebar-header-2"><a href="#recent-updates">Recent Updates</a></li> |
|
<li class="sidebar-header-2"><a href="#contents">Contents</a></li> |
|
<li class="sidebar-header-2"><a href="#understanding-promises">Understanding Promises</a></li> |
|
<li class="sidebar-header-2"><a href="#promises/a+-spec">Promises/A+ Spec</a></li> |
|
<li class="sidebar-header-2"><a href="#getting-the-dll">Getting the DLL</a></li> |
|
<li class="sidebar-header-2"><a href="#getting-the-code">Getting the Code</a></li> |
|
<li class="sidebar-header-2"><a href="#creating-a-promise-for-an-async-operation">Creating a Promise for an Async Operation</a></li> |
|
<li class="sidebar-header-2"><a href="#creating-a-promise,-alternate-method">Creating a Promise, Alternate Method</a></li> |
|
<li class="sidebar-header-2"><a href="#waiting-for-an-async-operation-to-complete">Waiting for an Async Operation to Complete</a></li> |
|
<li class="sidebar-header-2"><a href="#chaining-async-operations">Chaining Async Operations</a></li> |
|
<li class="sidebar-header-2"><a href="#transforming-the-results">Transforming the Results</a></li> |
|
<li class="sidebar-header-2"><a href="#error-handling">Error Handling</a></li> |
|
<li class="sidebar-header-2"><a href="#unhandled-errors">Unhandled Errors</a></li> |
|
<li class="sidebar-header-2"><a href="#promises-that-are-already-resolved/rejected">Promises that are already Resolved/Rejected</a></li> |
|
<li class="sidebar-header-2"><a href="#interfaces">Interfaces</a></li> |
|
<li class="sidebar-header-2"><a href="#combining-multiple-async-operations">Combining Multiple Async Operations</a></li> |
|
<li class="sidebar-header-2"><a href="#chaining-multiple-async-operations">Chaining Multiple Async Operations</a></li> |
|
<li class="sidebar-header-2"><a href="#racing-asynchronous-operations">Racing Asynchronous Operations</a></li> |
|
<li class="sidebar-header-2"><a href="#chaining-synchronous-actions-that-have-no-result">Chaining Synchronous Actions that have no Result</a></li> |
|
<li class="sidebar-header-2"><a href="#promises-that-have-no-results-(a-non-value-promise)">Promises that have no Results (a non-value promise)</a></li> |
|
<li class="sidebar-header-2"><a href="#convert-a-value-promise-to-a-non-value-promise">Convert a value promise to a non-value promise</a></li> |
|
<li class="sidebar-header-2"><a href="#running-a-sequence-of-operations">Running a Sequence of Operations</a></li> |
|
<li class="sidebar-header-2"><a href="#combining-parallel-and-sequential-operations">Combining Parallel and Sequential Operations</a></li> |
|
<li class="sidebar-header-2"><a href="#examples">Examples</a></li> |
|
</ul> |
|
|
|
</div> |
|
|
|
<div id="content"><h1 id="c#-promises"><a class="header-link" href="#c#-promises"></a>C# Promises</h1> |
|
<p>This documents the promise library used in ZFBrowser. This library has been modified, <a href="Readme.html#promises">details</a>.</p> |
|
<p>Original documentation follows:</p> |
|
<h1 id="c-sharp-promise"><a class="header-link" href="#c-sharp-promise"></a>C-Sharp-Promise</h1> |
|
<p><a href="https://promisesaplus.com/"> |
|
<img src="https://promisesaplus.com/assets/logo-small.png" alt="Promises/A+ logo" |
|
title="Promises/A+ 1.0 compliant" align="right" /> |
|
</a></p> |
|
<p>Promises library for C# for management of asynchronous operations.</p> |
|
<p>Inspired by Javascript promises, but slightly different.</p> |
|
<p>Used by <a href="https://github.com/Real-Serious-Games/C-Sharp-Promise">Real Serious Games</a> for building serious games and simulations on Unity3d.</p> |
|
<p>If you are interested in using promises for game development and Unity please see <a href="http://www.what-could-possibly-go-wrong.com/promises-for-game-development/">this article</a>.</p> |
|
<h2 id="recent-updates"><a class="header-link" href="#recent-updates"></a>Recent Updates</h2> |
|
<ul class="list"> |
|
<li>8 March 2015<ul class="list"> |
|
<li><em>Transform</em> function has been renamed to <em>Then</em> (another overload of <em>Then</em>).</li> |
|
</ul> |
|
</li> |
|
</ul> |
|
<h2 id="contents"><a class="header-link" href="#contents"></a>Contents</h2> |
|
<!-- START doctoc generated TOC please keep comment here to allow auto update --> |
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> |
|
<p><strong>Table of Contents</strong> <em>generated with <a href="https://github.com/thlorenz/doctoc">DocToc</a></em></p> |
|
<ul class="list"> |
|
<li><a href="#understanding-promises">Understanding Promises</a></li> |
|
<li><a href="#promisesa-spec">Promises/A+ Spec</a></li> |
|
<li><a href="#getting-the-dll">Getting the DLL</a></li> |
|
<li><a href="#getting-the-code">Getting the Code</a></li> |
|
<li><a href="#creating-a-promise-for-an-async-operation">Creating a Promise for an Async Operation</a></li> |
|
<li><a href="#creating-a-promise-alternate-method">Creating a Promise, Alternate Method</a></li> |
|
<li><a href="#waiting-for-an-async-operation-to-complete">Waiting for an Async Operation to Complete</a></li> |
|
<li><a href="#chaining-async-operations">Chaining Async Operations</a></li> |
|
<li><a href="#transforming-the-results">Transforming the Results</a></li> |
|
<li><a href="#promises-that-are-already-resolvedrejected">Promises that are already Resolved/Rejected</a></li> |
|
<li><a href="#interfaces">Interfaces</a></li> |
|
<li><a href="#combining-multiple-async-operations">Combining Multiple Async Operations</a></li> |
|
<li><a href="#chaining-multiple-async-operations">Chaining Multiple Async Operations</a></li> |
|
<li><a href="#racing-asynchronous-operations">Racing Asynchronous Operations</a></li> |
|
<li><a href="#chaining-synchronous-actions-that-have-no-result">Chaining Synchronous Actions that have no Result</a></li> |
|
<li><a href="#promises-that-have-no-results-a-non-value-promise">Promises that have no Results (a non-value promise)</a></li> |
|
<li><a href="#convert-a-value-promise-to-a-non-value-promise">Convert a value promise to a non-value promise</a></li> |
|
<li><a href="#running-a-sequence-of-operations">Running a Sequence of Operations</a></li> |
|
<li><a href="#combining-parallel-and-sequential-operations">Combining Parallel and Sequential Operations</a></li> |
|
<li><a href="#examples">Examples</a></li> |
|
</ul> |
|
<!-- END doctoc generated TOC please keep comment here to allow auto update --> |
|
<h2 id="understanding-promises"><a class="header-link" href="#understanding-promises"></a>Understanding Promises</h2> |
|
<p>To learn about promises:</p> |
|
<ul class="list"> |
|
<li><a href="http://en.wikipedia.org/wiki/Futures_and_promises">Promises on Wikpedia</a></li> |
|
<li><a href="https://www.promisejs.org/">Good overview</a></li> |
|
<li><a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise">Mozilla</a></li> |
|
</ul> |
|
<h2 id="promises/a+-spec"><a class="header-link" href="#promises/a+-spec"></a>Promises/A+ Spec</h2> |
|
<p>This promise library conforms to the <a href="https://promisesaplus.com/">Promises/A+ Spec</a> (at least, as far as is possible with C#):</p> |
|
<h2 id="getting-the-dll"><a class="header-link" href="#getting-the-dll"></a>Getting the DLL</h2> |
|
<p>The DLL can be installed via nuget. Use the Package Manager UI or console in Visual Studio or use nuget from the command line.</p> |
|
<p>See here for instructions on installing a package via nuget: <a href="http://docs.nuget.org/docs/start-here/using-the-package-manager-console">http://docs.nuget.org/docs/start-here/using-the-package-manager-console</a></p> |
|
<p>The package to search for is <em>RSG.Promise</em>.</p> |
|
<h2 id="getting-the-code"><a class="header-link" href="#getting-the-code"></a>Getting the Code</h2> |
|
<p>You can get the code by cloning the github repository. You can do this in a UI like SourceTree or you can do it from the command line as follows:</p> |
|
<pre class="hljs"><code>git <span class="hljs-keyword">clone</span> <span class="hljs-title">https</span>://github.com/Real-Serious-Games/C-Sharp-Promise.git</code></pre><p>Alternately, to contribute please fork the project in github.</p> |
|
<h2 id="creating-a-promise-for-an-async-operation"><a class="header-link" href="#creating-a-promise-for-an-async-operation"></a>Creating a Promise for an Async Operation</h2> |
|
<p>Reference the DLL and import the namespace:</p> |
|
<pre class="hljs"><code>using RSG<span class="hljs-comment">;</span></code></pre><p>Create a promise before you start the async operation:</p> |
|
<pre class="hljs"><code><span class="hljs-keyword">var</span> promise = <span class="hljs-keyword">new</span> Promise<<span class="hljs-keyword">string</span>>();</code></pre><p>The type of the promise should reflect the result of the async op.</p> |
|
<p>Then initiate your async operation and return the promise to the caller.</p> |
|
<p>Upon completion of the async op the promise is resolved:</p> |
|
<pre class="hljs"><code>promise.Resolve(myValue)<span class="hljs-comment">;</span></code></pre><p>The promise is rejected on error/exception:</p> |
|
<pre class="hljs"><code>promise.Reject(myException)<span class="hljs-comment">;</span></code></pre><p>To see it in context, here is an example function that downloads text from a URL. The promise is resolved when the download completes. If there is an error during download, say <em>unresolved domain name</em>, then the promise is rejected:</p> |
|
<pre class="hljs"><code><span class="hljs-function"><span class="hljs-keyword">public</span> IPromise<<span class="hljs-keyword">string</span>> <span class="hljs-title">Download</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> url</span>) |
|
</span>{ |
|
<span class="hljs-keyword">var</span> promise = <span class="hljs-keyword">new</span> Promise<<span class="hljs-keyword">string</span>>(); <span class="hljs-comment">// Create promise.</span> |
|
<span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> client = <span class="hljs-keyword">new</span> WebClient()) |
|
{ |
|
client.DownloadStringCompleted += <span class="hljs-comment">// Monitor event for download completed.</span> |
|
(s, ev) => |
|
{ |
|
<span class="hljs-keyword">if</span> (ev.Error != <span class="hljs-literal">null</span>) |
|
{ |
|
promise.Reject(ev.Error); <span class="hljs-comment">// Error during download, reject the promise.</span> |
|
} |
|
<span class="hljs-keyword">else</span> |
|
{ |
|
promise.Resolve(ev.Result); <span class="hljs-comment">// Downloaded completed successfully, resolve the promise.</span> |
|
} |
|
}; |
|
|
|
client.DownloadStringAsync(<span class="hljs-keyword">new</span> Uri(url), <span class="hljs-literal">null</span>); <span class="hljs-comment">// Initiate async op.</span> |
|
} |
|
|
|
<span class="hljs-keyword">return</span> promise; <span class="hljs-comment">// Return the promise so the caller can await resolution (or error).</span> |
|
}</code></pre><h2 id="creating-a-promise,-alternate-method"><a class="header-link" href="#creating-a-promise,-alternate-method"></a>Creating a Promise, Alternate Method</h2> |
|
<p>There is another way to create a promise that replicates the JavaScript convention of passing a <em>resolver</em> function into the constructor. The resolver function is passed functions that resolve or reject the promise. This allows you to express the previous example like this:</p> |
|
<pre class="hljs"><code>var promise = new Promise<string>((<span class="hljs-name">resolve</span>, reject) => |
|
{ |
|
using (<span class="hljs-name">var</span> client = new WebClient()) |
|
{ |
|
client.DownloadStringCompleted += // Monitor event for download completed. |
|
(<span class="hljs-name">s</span>, ev) => |
|
{ |
|
if (<span class="hljs-name">ev</span>.Error != null) |
|
{ |
|
reject(<span class="hljs-name">ev</span>.Error)<span class="hljs-comment">; // Error during download, reject the promise.</span> |
|
} |
|
else |
|
{ |
|
resolve(<span class="hljs-name">ev</span>.Result)<span class="hljs-comment">; // Downloaded completed successfully, resolve the promise.</span> |
|
} |
|
}<span class="hljs-comment">;</span> |
|
|
|
client.DownloadStringAsync(<span class="hljs-name">new</span> Uri(<span class="hljs-name">url</span>), null)<span class="hljs-comment">; // Initiate async op.</span> |
|
} |
|
})<span class="hljs-comment">;</span></code></pre><h2 id="waiting-for-an-async-operation-to-complete"><a class="header-link" href="#waiting-for-an-async-operation-to-complete"></a>Waiting for an Async Operation to Complete</h2> |
|
<p>The simplest usage is to register a completion handler to be invoked on completion of the async op:</p> |
|
<pre class="hljs"><code>Download(<span class="hljs-string">"http://www.google.com"</span>) |
|
.Done(<span class="hljs-function"><span class="hljs-params">html</span> =></span> |
|
Console.WriteLine(html) |
|
);</code></pre><p>This snippet downloads the front page from Google and prints it to the console.</p> |
|
<p>For all but the most trivial applications you will also want to register an error hander:</p> |
|
<pre class="hljs"><code>Download(<span class="hljs-string">"http://www.google.com"</span>) |
|
.Catch(<span class="hljs-function"><span class="hljs-params">exception</span> =></span> |
|
Console.WriteLine(<span class="hljs-string">"An exception occured while downloading!"</span>) |
|
) |
|
.Done(<span class="hljs-function"><span class="hljs-params">html</span> =></span> |
|
Console.WriteLine(html) |
|
);</code></pre><p>The chain of processing for a promise ends as soon as an error/exception occurs. In this case when an error occurs the <em>Catch</em> handler would be called, but not the <em>Done</em> handler. If there is no error, then only <em>Done</em> is called.</p> |
|
<h2 id="chaining-async-operations"><a class="header-link" href="#chaining-async-operations"></a>Chaining Async Operations</h2> |
|
<p>Multiple async operations can be chained one after the other using <em>Then</em>:</p> |
|
<pre class="hljs"><code>Download(<span class="hljs-string">"http://www.google.com"</span>) |
|
.Then(<span class="hljs-function"><span class="hljs-params">html</span> =></span> |
|
<span class="hljs-keyword">return</span> Download(ExtractFirstLink(html)) <span class="hljs-comment">// Extract the first link and download it.</span> |
|
) |
|
.Catch(<span class="hljs-function"><span class="hljs-params">exception</span> =></span> |
|
Console.WriteLine(<span class="hljs-string">"An exception occured while downloading!"</span>) |
|
) |
|
.Done(<span class="hljs-function"><span class="hljs-params">firstLinkHtml</span> =></span> |
|
Console.WriteLine(firstLinkHtml) |
|
);</code></pre><p>Here we are chaining another download onto the end of the first download. The first link in the html is extracted and we then download that. <em>Then</em> expects the return value to be another promise. The chained promise can have a different <em>result type</em>.</p> |
|
<h2 id="transforming-the-results"><a class="header-link" href="#transforming-the-results"></a>Transforming the Results</h2> |
|
<p>Sometimes you will want to simply transform or modify the resulting value without chaining another async operation.</p> |
|
<pre class="hljs"><code>Download(<span class="hljs-string">"http://www.google.com"</span>) |
|
<span class="hljs-keyword"> .Then</span>(html => |
|
<span class="hljs-built_in"> return </span>ExtractAllLinks(html)) // Extract all links<span class="hljs-built_in"> and </span>return an<span class="hljs-built_in"> array </span>of strings. |
|
) |
|
<span class="hljs-keyword"> .Done</span>(links => // The input here is an<span class="hljs-built_in"> array </span>of strings. |
|
foreach (var link in links) |
|
{ |
|
Console.WriteLine(link); |
|
} |
|
);</code></pre><p>As is demonstrated the type of the value can also be changed during transformation. In the previous snippet a <code>Promise<string></code> is transformed to a <code>Promise<string[]></code>.</p> |
|
<h2 id="error-handling"><a class="header-link" href="#error-handling"></a>Error Handling</h2> |
|
<p>An error raised in a callback aborts the function and all subsequent callbacks in the chain:</p> |
|
<pre class="hljs"><code>promise.Then(<span class="hljs-function"><span class="hljs-params">v</span> =></span> Something()) <span class="hljs-comment">// <--- An error here aborts all subsequent callbacks...</span> |
|
.Then(<span class="hljs-function"><span class="hljs-params">v</span> =></span> SomethingElse()) |
|
.Then(<span class="hljs-function"><span class="hljs-params">v</span> =></span> AnotherThing()) |
|
.Catch(<span class="hljs-function"><span class="hljs-params">e</span> =></span> HandleError(e)) <span class="hljs-comment">// <--- Until the error handler is invoked here.</span></code></pre><h2 id="unhandled-errors"><a class="header-link" href="#unhandled-errors"></a>Unhandled Errors</h2> |
|
<p>When <code>Catch</code> is omitted exceptions go silently unhandled. This is an acknowledged issue with the Promises pattern.</p> |
|
<p>We handle this in a similar way to the JavaScript <a href="http://documentup.com/kriskowal/q">Q</a> library. The <code>Done</code> method is used to terminate a chain, it registers a default catch handler that propagates unhandled exceptions to a default error handling mechanism that can be hooked into by the user.</p> |
|
<p>Terminating a Promise chain using <code>Done</code>:</p> |
|
<pre class="hljs"><code>promise.Then(<span class="hljs-function"><span class="hljs-params">v</span> =></span> Something()) |
|
.Then(<span class="hljs-function"><span class="hljs-params">v</span> =></span> SomethingElse()) |
|
.Then(<span class="hljs-function"><span class="hljs-params">v</span> =></span> AnotherThing()) |
|
.Done(); <span class="hljs-comment">// <--- Terminate the pipeline and propagate unhandled exceptions.</span></code></pre><p>To use the <code>Done</code> you must apply the following rule: When you get to the end of a chain of promises, you should either return the last promise or end the chain by calling <code>Done</code>.</p> |
|
<p>To hook into the unhandled exception stream:</p> |
|
<pre class="hljs"><code><span class="hljs-attr">Promise.UnhandledException +</span>=<span class="hljs-string"> Promise_UnhandledException;</span></code></pre><p>Then forward the exceptions to your own logging system:</p> |
|
<pre class="hljs"><code><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Promise_UnhandledException</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, ExceptionEventArgs e</span>) |
|
</span>{ |
|
Log.Error(e.Exception, <span class="hljs-string">"An unhandled proimses exception occured!"</span>); |
|
}</code></pre><h2 id="promises-that-are-already-resolved/rejected"><a class="header-link" href="#promises-that-are-already-resolved/rejected"></a>Promises that are already Resolved/Rejected</h2> |
|
<p>For convenience or testing you will at some point need to create a promise that <em>starts out</em> in the resolved or rejected state. This is easy to achieve using <em>Resolved</em> and <em>Rejected</em> functions:</p> |
|
<pre class="hljs"><code><span class="hljs-keyword">var</span> resolvedPromise = Promise<<span class="hljs-keyword">string</span>>.Resolved(<span class="hljs-string">"some result"</span>); |
|
|
|
<span class="hljs-keyword">var</span> rejectedPromise = Promise<<span class="hljs-keyword">string</span>>.Rejected(someException);</code></pre><h2 id="interfaces"><a class="header-link" href="#interfaces"></a>Interfaces</h2> |
|
<p>The class <em>Promise<T></em> implements the following interfaces:</p> |
|
<ul class="list"> |
|
<li><code>IPromise<T></code> Interface to await promise resolution.</li> |
|
<li><code>IPendingPromise<T></code> Interface that can resolve or reject the promise.</li> |
|
</ul> |
|
<h2 id="combining-multiple-async-operations"><a class="header-link" href="#combining-multiple-async-operations"></a>Combining Multiple Async Operations</h2> |
|
<p>The <em>All</em> function combines multiple async operations to run in parallel. It converts a collection of promises or a variable length parameter list of promises into a single promise that yields a collection.</p> |
|
<p>Say that each promise yields a value of type <em>T</em>, the resulting promise then yields a collection with values of type <em>T</em>.</p> |
|
<p>Here is an example that extracts links from multiple pages and merges the results:</p> |
|
<pre class="hljs"><code><span class="hljs-keyword">var</span> urls = <span class="hljs-keyword">new</span> List<string>(); |
|
urls.Add(<span class="hljs-string">"www.google.com"</span>); |
|
urls.Add(<span class="hljs-string">"www.yahoo.com"</span>); |
|
|
|
<span class="hljs-built_in">Promise</span><string[]> |
|
.All(<span class="hljs-function"><span class="hljs-params">url</span> =></span> Download(url)) <span class="hljs-comment">// Download each URL.</span> |
|
.Then(<span class="hljs-function"><span class="hljs-params">pages</span> =></span> <span class="hljs-comment">// Receives collection of downloaded pages.</span> |
|
pages.SelectMany( |
|
<span class="hljs-function"><span class="hljs-params">page</span> =></span> ExtractAllLinks(page) <span class="hljs-comment">// Extract links from all pages then flatten to single collection of links.</span> |
|
) |
|
) |
|
.Done(<span class="hljs-function"><span class="hljs-params">links</span> =></span> <span class="hljs-comment">// Receives the flattened collection of links from all pages at once.</span> |
|
{ |
|
foreach (<span class="hljs-keyword">var</span> link <span class="hljs-keyword">in</span> links) |
|
{ |
|
Console.WriteLine(link); |
|
} |
|
});</code></pre><h2 id="chaining-multiple-async-operations"><a class="header-link" href="#chaining-multiple-async-operations"></a>Chaining Multiple Async Operations</h2> |
|
<p>The <em>ThenAll</em> function is a convenient way of chaining multiple promise onto an existing promise:</p> |
|
<pre class="hljs"><code>promise |
|
.Then(<span class="hljs-function"><span class="hljs-params">result</span> =></span> SomeAsyncOperation(result)) <span class="hljs-comment">// Chain a single async operation</span> |
|
.ThenAll(<span class="hljs-function"><span class="hljs-params">result</span> =></span> <span class="hljs-comment">// Chain multiple async operations.</span> |
|
<span class="hljs-keyword">new</span> IPromise<string>[] <span class="hljs-comment">// Return an enumerable of promises.</span> |
|
{ |
|
SomeAsyncOperation1(result), |
|
SomeAsyncOperation2(result), |
|
SomeAsyncOperation3(result) |
|
} |
|
) |
|
.Done(<span class="hljs-function"><span class="hljs-params">collection</span> =></span> ...); <span class="hljs-comment">// Final promise resolves</span> |
|
<span class="hljs-comment">// with a collection of values</span> |
|
<span class="hljs-comment">// when all operations have completed.</span></code></pre><h2 id="racing-asynchronous-operations"><a class="header-link" href="#racing-asynchronous-operations"></a>Racing Asynchronous Operations</h2> |
|
<p>The <em>Race</em> and <em>ThenRace</em> functions are similar to the <em>All</em> and <em>ThenAll</em> functions, but it is the first async operation that completes that wins the race and it's value resolves the promise.</p> |
|
<pre class="hljs"><code>promise |
|
.Then(<span class="hljs-function"><span class="hljs-params">result</span> =></span> SomeAsyncOperation(result)) <span class="hljs-comment">// Chain an async operation.</span> |
|
.ThenRace(<span class="hljs-function"><span class="hljs-params">result</span> =></span> <span class="hljs-comment">// Race multiple async operations.</span> |
|
<span class="hljs-keyword">new</span> IPromise<string>[] <span class="hljs-comment">// Return an enumerable of promises.</span> |
|
{ |
|
SomeAsyncOperation1(result), |
|
SomeAsyncOperation2(result), |
|
SomeAsyncOperation3(result) |
|
} |
|
) |
|
.Done(<span class="hljs-function"><span class="hljs-params">result</span> =></span> ...); <span class="hljs-comment">// The result has come from whichever of</span> |
|
<span class="hljs-comment">// the async operations completed first.</span></code></pre><h2 id="chaining-synchronous-actions-that-have-no-result"><a class="header-link" href="#chaining-synchronous-actions-that-have-no-result"></a>Chaining Synchronous Actions that have no Result</h2> |
|
<p>The <em>Then</em> function can be used to chain synchronous operations that yield no result.</p> |
|
<pre class="hljs"><code><span class="hljs-keyword">var</span> promise = ... |
|
promise |
|
.Then(<span class="hljs-function"><span class="hljs-params">result</span> =></span> SomeAsyncOperation(result)) <span class="hljs-comment">// Chain an async operation.</span> |
|
.Then(<span class="hljs-function"><span class="hljs-params">result</span> =></span> Console.WriteLine(result)) <span class="hljs-comment">// Chain a sync operation that yields no result.</span> |
|
.Done(<span class="hljs-function"><span class="hljs-params">result</span> =></span> ...); <span class="hljs-comment">// Result from previous ascync operation skips over the *Do* and is passed through.</span></code></pre><h2 id="promises-that-have-no-results-(a-non-value-promise)"><a class="header-link" href="#promises-that-have-no-results-(a-non-value-promise)"></a>Promises that have no Results (a non-value promise)</h2> |
|
<p>What about a promise that has no result? This represents an asynchronous operation that promises only to complete, it doesn't promise to yield any value as a result. I call this a non-value promise, as opposed to a value promise, which is a promise that does yield a value. This might seem like a curiousity but it is actually very useful for sequencing visual effects.</p> |
|
<p><code>Promise</code> is very similar to <code>Promise<T></code> and implements the similar interfaces: <code>IPromise</code> and <code>IPendingPromise</code>.</p> |
|
<p><code>Promise<T></code> functions that affect the resulting value have no relevance for the non-value promise and have been removed.</p> |
|
<p>As an example consider the chaining of animation and sound effects as we often need to do in <em>game development</em>:</p> |
|
<pre class="hljs"><code>RunAnimation(<span class="hljs-string">"Foo"</span>) <span class="hljs-regexp">//</span> RunAnimation returns a promise that |
|
.Then(<span class="hljs-function"><span class="hljs-params">()</span> =></span> RunAnimation(<span class="hljs-string">"Bar"</span>)) <span class="hljs-regexp">//</span> <span class="hljs-keyword">is</span> resolved <span class="hljs-keyword">when</span> the animation <span class="hljs-keyword">is</span> complete. |
|
.Then(<span class="hljs-function"><span class="hljs-params">()</span> =></span> PlaySound(<span class="hljs-string">"AnimComplete"</span>));</code></pre><h2 id="convert-a-value-promise-to-a-non-value-promise"><a class="header-link" href="#convert-a-value-promise-to-a-non-value-promise"></a>Convert a value promise to a non-value promise</h2> |
|
<p>From time to time you might want to convert a value promise to a non-value promise or vice versa. Both <code>Promise</code> and <code>Promise<T></code> have overloads of <code>Then</code> and <code>ThenAll</code> that do this conversion. You just need to return the appropriate type of promise (for <code>Then</code>) or enumerable of promises (for <code>ThenAll</code>).</p> |
|
<p>As an example consider a recursive link extractor and file downloader function:</p> |
|
<pre class="hljs"><code><span class="hljs-keyword">public</span> IPromise DownloadAll(<span class="hljs-built_in">string</span> url) |
|
{ |
|
<span class="hljs-keyword">return</span> DownloadURL(url) <span class="hljs-comment">// Yields a value, the HTML text downloaded.</span> |
|
.Then(html => ExtractLinks(html)) <span class="hljs-comment">// Convert HTML into an enumerable of links.</span> |
|
.ThenAll(links => <span class="hljs-comment">// Process each link.</span> |
|
{ |
|
<span class="hljs-comment">// Determine links that should be followed, then follow them.</span> |
|
<span class="hljs-built_in">var</span> linksToFollow = links.<span class="hljs-keyword">Where</span>(<span class="hljs-keyword">link</span> => IsLinkToFollow(<span class="hljs-keyword">link</span>)); |
|
<span class="hljs-built_in">var</span> linksFollowing = linksToFollow.<span class="hljs-keyword">Select</span>(<span class="hljs-keyword">link</span> => DownloadAll(<span class="hljs-keyword">link</span>)); |
|
|
|
<span class="hljs-comment">// Determine links that are files to be downloaded, then download them.</span> |
|
<span class="hljs-built_in">var</span> linksToDownload = links.<span class="hljs-keyword">Where</span>(<span class="hljs-keyword">link</span> => IsLinkToDownload(<span class="hljs-keyword">link</span>)); |
|
<span class="hljs-built_in">var</span> linksDownloading = linksToDownload.<span class="hljs-keyword">Select</span>(<span class="hljs-keyword">link</span> => DownloadFile(<span class="hljs-keyword">link</span>)); |
|
|
|
<span class="hljs-comment">// Return an enumerable of promises.</span> |
|
<span class="hljs-comment">// This combines the recursive link following and any files we want to download.</span> |
|
<span class="hljs-comment">// Because we are returning an enumerable of non-value promises, the resulting</span> |
|
<span class="hljs-comment">// chained promises is also non-value.</span> |
|
<span class="hljs-keyword">return</span> linksToFollow.Concat(linksDownloading); |
|
}); |
|
}</code></pre><p>Usage:</p> |
|
<pre class="hljs"><code>DownloadAll(<span class="hljs-string">"www.somewhere.com"</span>) |
|
.Done(<span class="hljs-function"><span class="hljs-params">()</span> =></span> |
|
Console.WriteLine(<span class="hljs-string">"Recursive download completed."</span>); |
|
);</code></pre><h2 id="running-a-sequence-of-operations"><a class="header-link" href="#running-a-sequence-of-operations"></a>Running a Sequence of Operations</h2> |
|
<p>The <code>Sequence</code> and <code>ThenSequence</code> functions build a single promise that wraps multiple sequential operations that will be invoked one after the other.</p> |
|
<p>Multiple promise-yielding functions are provided as input, these are chained one after the other and wrapped in a single promise that is resolved once the sequence has completed.</p> |
|
<pre class="hljs"><code>var sequence = Promise.Sequence( |
|
<span class="hljs-function"><span class="hljs-params">()</span> =></span> RunAnimation(<span class="hljs-string">"Foo"</span>), |
|
<span class="hljs-function"><span class="hljs-params">()</span> =></span> RunAnimation(<span class="hljs-string">"Bar"</span>), |
|
<span class="hljs-function"><span class="hljs-params">()</span> =></span> PlaySound(<span class="hljs-string">"AnimComplete"</span>) |
|
);</code></pre><p>The inputs can also be passed as a collection:</p> |
|
<pre class="hljs"><code><span class="hljs-built_in">var</span> operations = <span class="hljs-params">...</span> |
|
<span class="hljs-built_in">var</span> sequence = Promise.Sequence(operations);</code></pre><p>This might be used, for example, to play a variable length collection of animations based on data:</p> |
|
<pre class="hljs"><code> <span class="hljs-keyword">var</span> animationNames = ... variable length array <span class="hljs-keyword">of</span> animation names loaded <span class="hljs-keyword">from</span> data... |
|
var animations = animationNames.Select(<span class="hljs-function"><span class="hljs-params">animName</span> =></span> (Func<IPromise>)(<span class="hljs-function"><span class="hljs-params">()</span> =></span> RunAnimation(animName))); |
|
<span class="hljs-keyword">var</span> sequence = <span class="hljs-built_in">Promise</span>.Sequence(animations); |
|
sequence |
|
.Done(<span class="hljs-function"><span class="hljs-params">()</span> =></span> |
|
{ |
|
<span class="hljs-comment">// All animations have completed in sequence.</span> |
|
});</code></pre><p>Unfortunately we find that we have reached the limits of what is possible with C# type inference, hence the use of the ugly cast <code>(Func<IPromise>)</code>.</p> |
|
<p>The cast can easily be removed by converting the inner anonymous function to an actual function which I'll call <code>PrepAnimation</code>:</p> |
|
<pre class="hljs"><code>private Func<IPromise> PrepAnimation(string animName) |
|
{ |
|
<span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-params">()</span> =></span> RunAnimation(animName); |
|
} |
|
|
|
<span class="hljs-keyword">var</span> animations = animationNames.Select(<span class="hljs-function"><span class="hljs-params">animName</span> =></span> PrepAnimation(animName)); |
|
<span class="hljs-keyword">var</span> sequence = <span class="hljs-built_in">Promise</span>.Sequence(animations); |
|
sequence |
|
.Done(<span class="hljs-function"><span class="hljs-params">()</span> =></span> |
|
{ |
|
<span class="hljs-comment">// All animations have completed in sequence.</span> |
|
});</code></pre><p>Holy cow... we've just careened into <a href="http://en.wikipedia.org/wiki/Functional_programming">functional programming</a> territory, herein lies very powerful and expressive programming techniques.</p> |
|
<h2 id="combining-parallel-and-sequential-operations"><a class="header-link" href="#combining-parallel-and-sequential-operations"></a>Combining Parallel and Sequential Operations</h2> |
|
<p>We can easily combine sequential and parallel operations to build very expressive logic.</p> |
|
<pre class="hljs"><code>Promise.Sequence( // Play operations <span class="hljs-number">1</span> <span class="hljs-keyword">and</span> <span class="hljs-number">2</span> sequently. |
|
() => Promise.All( // Operation <span class="hljs-number">1</span>: Play <span class="hljs-built_in">animation</span> <span class="hljs-keyword">and</span> sound <span class="hljs-built_in">at</span> same <span class="hljs-built_in">time</span>. |
|
RunAnimation(<span class="hljs-string">"Foo"</span>), |
|
PlaySound(<span class="hljs-string">"Bar"</span>) |
|
), |
|
() => Promise.All( |
|
RunAnimation(<span class="hljs-string">"One"</span>), // Operation <span class="hljs-number">2</span>: Play <span class="hljs-built_in">animation</span> <span class="hljs-keyword">and</span> sound <span class="hljs-built_in">at</span> same <span class="hljs-built_in">time</span>. |
|
PlaySound(<span class="hljs-string">"Two"</span>) |
|
) |
|
);</code></pre><p>I'm starting to feel like we are defining behavior trees.</p> |
|
<h2 id="examples"><a class="header-link" href="#examples"></a>Examples</h2> |
|
<ul class="list"> |
|
<li>Example1<ul class="list"> |
|
<li>Example of downloading text from a URL using a promise.</li> |
|
</ul> |
|
</li> |
|
<li>Example2<ul class="list"> |
|
<li>Example of a promise that is rejected because of an error during</li> |
|
<li>the async operation.</li> |
|
</ul> |
|
</li> |
|
<li>Example3<ul class="list"> |
|
<li>This example downloads search results from google then transforms the result to extract links.</li> |
|
<li>Includes both error handling and a completion handler.</li> |
|
</ul> |
|
</li> |
|
<li>Example4<ul class="list"> |
|
<li>This example downloads search results from google, extracts the links and follows only a single first link, downloads its then prints the result.</li> |
|
<li>Includes both error handling and a completion handler.</li> |
|
</ul> |
|
</li> |
|
<li>Example5<ul class="list"> |
|
<li>This example downloads search results from google, extracts the links, follows all (absolute) links and combines all async operations in a single operation using the All function.</li> |
|
</ul> |
|
</li> |
|
</ul> |
|
</div> |
|
|
|
|
|
</body></html> |
|
|
|
|