Better Living Through Concurrency
Concurrency in Fate is accomplished via Continuations. Essentially, any code that is executed within a do
block is potentially asynchronous. To retrieve the value of the block's calculation, one has to await the result. This can be accomplished either with an await
expression or a when
clause added to do
block.
do
The do
keyword kicks off an asynchronous calculation. The Fate parser will sequence everything between the do keyword and its closing end
. The result of such a block is a Continuation that can be passed to the await
operator. The do
block comes in three flavors: do when
, do case
, and the plain old do
.
Plain old "do"
from io import print
let getHomePage = do
# perform some asynchronous work
http.get('http://www.example.org')
end
do
await getHomePage() | print
end
do when
do
can also be called with a when
clause. This clause assigns the eventual results of asynchronous calculations to variables scoped within the block.
do when content = http.get('http://www.example.org'),
{name, status} = getUser()
# content, name, and status will have already been resolved
end
Wherever possible, the assignments of a when
clause will be performed in parallel. It is only when one assignment depends on the result of a previous one that the calculations happen serially.
do when a = timeout(10), b = timeout(a * 5), c = a * 50, d = b * 5, e = 100
# above assignments are performed in three groups: [a, e], [b, c], [d]
a | print # 10
b | print # 50
c | print # 500
d | print # 250
e | print # 100
end
There are five assignments in the example above. When parsing, Fate will recognize that b and c depend on a. It will also see that d depends on b. But a and e depend on nothing. This means that Fate can perform the resolution in three steps. The first step will resolve a and e in parallel. The second will resolve b and c in parallel, and the final step will resolve d. And you didn’t have to think about it.
The when clause can also accept a single expression rather than a set of assignments.
do when timeout(5000)
print("five seconds later")
end
do case
do
with a nested series of case
clauses allows you to perform multiple simultaneous resolutions, and execute only the block for the first resolved case. Like the when
clause, each case clause can either accept a set of assignments or a single expression.
do
case x = timeout(10)
x | '% won' | print
case x = timeout(5)
x | '% won' | print
case timeout(20)
print('whole thing timed out')
end
In this case, the only thing that will be displayed is "5 won".
await
Must appear within a do
block. When an await
expression is encountered, control is passed to the asynchronous sequencer. Then, when the expression specified by await
can be resolved, it will trigger a continuation of the do
block.
let content = await http.get('http://www.example.org')
await any
A standard await
will only wait for a single calculation to be processed. The await any
modifier allows the language to monitor an array of do
blocks for completion. It will then return the first completed result.
let pages = [
'http://www.fate-lang.org',
'http://www.example.org',
'http://www.microsoft.com'
]
let requests = [for x in pages select x | http.get]
do: await any requests | print
await all
Like await any
, the await all
modifier allows the language to monitor an array of do
blocks for completion. In this case, it will wait for all to complete, and then return their results as an Array.
let pages = [
'http://www.fate-lang.org',
'http://www.example.org',
'http://www.microsoft.com'
]
let requests = [for x in pages select x | http.get]
do
for content in await all requests
content | print
end
end
awaiting right calls
Now that you've learned about the await
keyword, you may be thinking "but won't my code become a mess of 'awaits'?" Depending on what you're doing, it's quite possible. That's why Fate also includes a version of the right-call operator that allows you to easily chain the resolution of Continuations into function invocations.
import io
let double = (arr -> do: [for a in arr select a * 2])
let timeouts = (arr -> [for a in arr select io.timeout(a)])
do
[100, 200, 300] | timeouts :| double .| io.print
end
The example above is passing an array into a function that creates three separate timeout Continuations using the timeouts
lambda. The result of each element is awaited, and the resolved array is then passed into the double
lambda. The result of that function is then awaited and passed into io.print
.
Like the await
keyword, the awaiting right call operator also supports modifiers.
.| - Normal 'await' - Calls the right function with the resolved left expression
?| - 'await any' - Calls the right function with the first resolved array element
:| - 'await all' - Calls the right function with the resolved array elements