Implementation of EXPath Tasks in pure portable XQuery 3.1.
This is a reference implementation for our paper Task Abstraction for XPath Derived Languages. Lockett and Retter, 2019 which was presented at XML Prague 2019.
It is worth explicitly restating that this implementation does not provide asynchronous processing, instead all asynchrnous functions will be executed synchronously.
Download the task.xq
file for use with your favourite XPDL processor.
From your main XQuery simply import the module like so:
import module namespace task = "http://expath.org/ns/task" at "task.xq";
You may need to adjust the location hint after the at
, refer to the documentation of your XPDL (XPath Derived Language) processor.
-
Create a task from a pure value and run it
task:RUN-UNSAFE( task:value(1234) )
- Create a task from a pure value and run it (fluent syntax)
task:value("123") ? RUN-UNSAFE()
- Create a task from a pure value and run it (fluent syntax)
-
Create a task from a function and run it
task:RUN-UNSAFE( task:of(function() { 1 + 2 }) )
- Create a task from a function and run it (fluent syntax)
task:of(function() { 1 + 2 }) ? RUN-UNSAFE()
- Create a task from a function and run it (fluent syntax)
All examples from herein use the fluent syntax, as we believe it makes it easier for a developer to parse the intention of the code.
-
Using bind to transform a value
task:value(123) ?bind(function($i) { task:value($i || "val1") }) ?bind(function($i) { task:value($i || "val2") }) ?RUN-UNSAFE()
-
Using fmap to perform a function
task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " adam")) ? RUN-UNSAFE()
-
Composing two tasks with
bind
:- You should never have more than one call to
RUN-UNSAFE
, i.e. DO NOT DO THIS:
task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? RUN-UNSAFE() , task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? RUN-UNSAFE()
- Instead, you can compose the tasks with bind:
task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? bind(function($hello) { task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? fmap(function($goodbye) {($hello, $goodbye)}) }) ? RUN-UNSAFE()
- The longer form if you like variable bindings:
let $task-hello := task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) let $task-goodbye := task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) return $task-hello ?bind(function($hello){ $task-goodbye ?fmap(function($goodbye){ ($hello, $goodbye)})}) ? RUN-UNSAFE()
- Or alternatively shorter syntax by partially applying
fn:insert-before
as:
task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? bind(function($hello) { task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? fmap(fn:insert-before(?, 0, $hello)) }) ? RUN-UNSAFE()
- Or if you need an array instead of a sequence to preserve isolation of the results:
task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? bind(function($hello) { task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? fmap(function($goodbye) {[$hello, $goodbye]}) }) ? RUN-UNSAFE()
- Or alternatively shorter syntax by partially applying
array:append
:
task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? bind(function($hello) { task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? fmap(array:append([$hello], ?)) }) ? RUN-UNSAFE()
- The longer form for returning an array if you like variable bindings:
let $task-hello := task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) let $task-goodbye := task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) return $task-hello ?bind(function($hello){ $task-goodbye ?fmap(function($goodbye){ [$hello, $goodbye]})}) ? RUN-UNSAFE()
- You should never have more than one call to
-
Composing two or more tasks with
sequence
:- Using the
task:sequence
function syntax:
let $task-hello := task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) let $task-goodbye := task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? fmap(fn:tokenize(?, " ")) ? fmap(array:append([], ?)) return task:sequence(($task-hello, $task-goodbye)) ? RUN-UNSAFE()
- Using the
sequence
fluent syntax:
let $task-hello := task:value("hello") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) let $task-goodbye := task:value("goodbye") ? fmap(upper-case#1) ? fmap(concat(?, " debbie")) ? fmap(fn:tokenize(?, " ")) ? fmap(array:append([], ?)) return $task-hello ? sequence($task-goodbye) ? RUN-UNSAFE()
- Using the
- Remember that an "Async" is just a reference to an asynchronous computation.
-
Asynchronously executing a task where you don't care about the result
let $some-task := task:value("hello") ? fmap(upper-case#1) ? fmap(http:post("google.com", ?)) ? async() return $some-task ? RUN-UNSAFE()
-
Asynchronously executing a task, when you do care about the result, you have to wait upon the asynchronous computation
task:value("hello") ? fmap(upper-case#1) ? async() ? bind(task:wait#1) ? RUN-UNSAFE()
-
Asynchronous equaivalent to fork-join, where you don't care about the results :)
let $char-to-int := function($s as xs:string) as xs:integer { fn:string-to-codepoints($s)[1] }
let $int-to-char := function($i as xs:integer) as xs:string { fn:substring(fn:codepoints-to-string($i), 1, 1) }
let $square := function($i as xs:integer) as xs:integer { $i * $i }
let $async-task1 := task:of(function(){ 1 to 10 })
? fmap(function($ii) { $ii ! $square(.) })
? async()
let $async-task2 := task:of(function(){ $char-to-int("a") to $char-to-int("z") })
? fmap(function($ii) { $ii ! (. - 32) })
? fmap(function($ii) { $ii ! $int-to-char(.)})
? async()
return
$async-task1
?sequence($async-task2)
?RUN-UNSAFE()
- Asynchronous equaivalent to fork-join, where you do care about the results using
task:wait-all
let $char-to-int := function($s as xs:string) as xs:integer { fn:string-to-codepoints($s)[1] }
let $int-to-char := function($i as xs:integer) as xs:string { fn:substring(fn:codepoints-to-string($i), 1, 1) }
let $square := function($i as xs:integer) as xs:integer { $i * $i }
let $async-task1 := task:of(function(){ 1 to 10 })
? fmap(function($ii) { $ii ! $square(.) })
? async()
let $async-task2 := task:of(function(){ $char-to-int("a") to $char-to-int("z") })
? fmap(function($ii) { $ii ! (. - 32) })
? fmap(function($ii) { $ii ! $int-to-char(.)})
? async()
return
$async-task1 ?sequence($async-task2)
?bind(task:wait-all#1)
?RUN-UNSAFE()
- Cancelling an asynchronous computation, and then starting another asynchronous computation
task:value("hello")
? fmap(upper-case#1)
? async()
? bind(task:cancel#1)
? then(task:of(function(){ (1 to 10 )}))
? async()
? bind(task:wait#1)
? RUN-UNSAFE()
- Constructing an Error. No error happens, because the task has not been executed yet!
task:of(function() {
fn:error(xs:QName("adt:adam1"))
})
- Simply constructing an Error using
task:error
.
task:error(xs:QName("adt:adam1"), "Boom!", ())
- Raises an error, beccause the task is executed! :)
task:error(xs:QName("adt:adam1"), "Boom!", ())
?RUN-UNSAFE()
- Using catches-recover to recover from an error
let $local:mission-failed-err := xs:QName("local:mission-failed-err")
return
task:value("all your base...")
?fmap(fn:upper-case#1)
?fmap(fn:tokenize(?, " "))
?fmap(function($strings){ $strings = "BELONGS" })
?fmap(function($b) { if($b) then "BASES OWNED!" else fn:error($local:mission-failed-err)})
?catches-recover($local:mission-failed-err, function() {
"MISSION FAILED! YOU OWN ZERO BASES!!!"
})
?RUN-UNSAFE()
- Using catch to handle any error
let $local:mission-failed-err := xs:QName("local:mission-failed-err")
return
task:value("all your base...")
?fmap(fn:upper-case#1)
?fmap(fn:tokenize(?, " "))
?fmap(function($strings){ $strings = "BELONGS" })
?fmap(function($b) { if($b) then "BASES OWNED!" else fn:error($local:mission-failed-err)})
?catch(function($code, $description, $value) {
task:value("(" || $code || ") MISSION FAILED! YOU OWN ZERO BASES!!!")
})
?RUN-UNSAFE()
- Using catch to manually handle a specific error :)
let $local:mission-failed-err := xs:QName("local:mission-failed-err")
return
task:value("all your base...")
?fmap(fn:upper-case#1)
?fmap(fn:tokenize(?, " "))
?fmap(function($strings){ $strings = "BELONGS" })
?fmap(function($b) { if($b) then "BASES OWNED!" else fn:error($local:mission-failed-err)})
?catch(function($code, $description, $value) {
if ($code eq $local:mission-failed-err) then
task:value("MISSION FAILED! YOU OWN ZERO BASES!!!")
else
(: forward any other the error... :)
task:error($code, $description, $value)
})
?RUN-UNSAFE()
- Using catch to handle a specific error (similar to previous, but some other error occurs earlier)
let $local:mission-failed-err := xs:QName("local:mission-failed-err")
return
task:value("all your base...")
?fmap(fn:upper-case#1)
(: inject some critical error :)
?then(task:error((), "BOOM!", ()))
?fmap(fn:tokenize(?, " "))
?fmap(function($strings){ $strings = "BELONGS" })
?fmap(function($b) { if($b) then "BASES OWNED!" else fn:error($local:mission-failed-err)})
?catch(function($code, $description, $value) {
if ($code eq $local:mission-failed-err) then
task:value("MISSION FAILED! YOU OWN ZERO BASES!!!")
else
(: forward any other the error... :)
task:error($code, $description, $value)
})
?RUN-UNSAFE()