Friday, March 12, 2010

Linq to Tasks

One of the more interesting properties of the Linq syntax is that, at heart, it has nothing to do with sequences or XML or SQL. Linq is a really syntax for performing operations with monads, and you can add your own types to it relatively easily. (For the uninitiated: a monad is essentially a wrapper class plus a way to transform functions to use wrapped values instead of normal values). Linq works with sequences because sequences are an extremely common Monad, and so the linq-to-list stuff is implemented as part of .Net. In this post, I'm going to show you an example of adding your own Linq-to-X functionality.

Tasks are a new feature in .Net 4.0. A task represents an asynchronous operation, such as a function running on another thread, and provides methods for working with the eventual result. You can use the ContinueWith methods to chain tasks together, feeding the eventual output of one task into the input of another task. For our purposes, the important thing is that tasks are a monad: they wrap a value, and you can modify functions to operate on tasks instead of normal values (using ContinueWith).

Since tasks are a type of monad, we can use them with Linq. All we have to do is define the necessary extension methods: Select and SelectMany. Select is the function used when you have a single 'From' line in your query, while SelectMany is used when there are multiple 'From' lines. Our task-version of Select will take a task and a function, and return a task for the function's eventual result after being given the task's eventual value. SelectMany is essentially the same thing, but with two tasks and two functions.

    Imports System.Threading.Tasks

Public Module LinqToTasks
<Extension()>
Public Function [Select](Of TArg, TResult)(ByVal task As Task(Of TArg),
ByVal func As Func(Of TArg, TResult)) As Task(Of TResult)
Return task.ContinueWith(Function(t) func(t.Result), TaskContinuationOptions.OnlyOnRanToCompletion)
End Function

<Extension()<
Public Function SelectMany(Of TArg, TMid, TReturn)(ByVal task As Task(Of TArg),
ByVal projection1 As Func(Of TArg, Task(Of TMid)),
ByVal projection2 As Func(Of TArg, TMid, TReturn)) As Task(Of TReturn)
Return task.Select(Function(value1) projection1(value1).
Select(Function(value2) projection2(value1, value2))).Unwrap
End Function
End Module

I know that doesn't seem like a lot of code, but that's all of it. We can now use the linq syntax on tasks. But keep in mind that we didn't implement many of the operators, like "where" (what would that even mean for a task?). Here are some examples of using the linq syntax on tasks:

Example #1: Returns a task that evaluates to 35 after five seconds:

        Public  Function TaskTest() As  Task(Of Int32)
Dim t = New TaskCompletionSource(Of Int32)
Call New System.Threading.Thread(Sub()
System.Threading.Thread.Sleep(5000)
t.SetResult(5)
End Sub).Start()
Return From value In t.Task
Let squared = value * value
Let doubled = value + value
Select squared + doubled
End Function

Example #2: Returns a task that evaluates to 20 after ten seconds:

Public Function TaskTest2() As Task(Of Int32)
Dim t = New TaskCompletionSource(Of Task(Of Int32))
Call New System.Threading.Thread(Sub()
System.Threading.Thread.Sleep(5000)
Dim t2 = New TaskCompletionSource(Of Int32)
t.SetResult(t2.Task)
System.Threading.Thread.Sleep(5000)
t2.SetResult(1)
End Sub).Start()
Return From taskValue In t.Task
From value In taskValue
Select value * 20
End Function

As you have seen, allowing linq syntax on a type is as easy as implementing extension methods for the operators. A good learning exercise is to implement the linq operators for the non-generic version of IEnumerable (hint: all the functions involve enumerating). There are tons of types you can linq-ify: collection types, nullables, Lazy<t>, delegates like Func<t>, etc, etc, etc and etc. Try not to overdo it.

No comments:

Post a Comment