YOrch 1.0.0
Loading...
Searching...
No Matches
Build a Task Tree

English | 中文

YOrch uses yorch::task_tree to build a task tree. A task tree starts from one root node and then appends child nodes level by level.

The most common style is to pass a callable directly into the tree builder and describe its argument sources in the following (...):

auto tree = yorch::task_tree
.root([]() noexcept -> int {
return 7;
})()
.node<1>([](const int& value) noexcept {
return value * 2;
})(yorch::borrow_prev<int>())
.node<2>([](const int& value) noexcept {
return value + 1;
})(yorch::borrow_prev<int>());
constexpr auto value(T &&v) -> value_t< std::remove_cvref_t< T > >
Wraps a value as an owning spec.
Definition specs.hpp:179
constexpr task_tree_builder task_tree
Definition builder.hpp:48

There are two sets of parentheses:

  • .root(callable) / .node<Level>(callable) selects the callable placed on the tree.
  • The following (...) describes where that callable gets its arguments.

If the callable has no arguments, the second set of parentheses is still required and should be written as ().

Levels

root(...) registers the root task. Its level is always 0.

node<Level>(...) registers a normal node. Level is the node depth inside the tree:

  • node<1>(...) is a child of the root node.
  • node<2>(...) is a child of the nearest available first-level node.
  • Consecutive nodes on the same level are siblings.
auto tree = yorch::task_tree
.root([]() noexcept {
// level 0
})()
.node<1>([]() noexcept {
// level 1, first child of root
})()
.node<2>([]() noexcept {
// level 2, child of the previous level 1 node
})()
.node<1>([]() noexcept {
// level 1, second child of root
})();

You cannot skip levels while building a tree. For example, when the tree only has the root node, you cannot append node<2>(...) directly; a node<1>(...) must exist first.

An empty task_tree can also register the root with node<0>(...), but root(...) is usually preferred because it is clearer.

Two Registration Styles

Each node can register a task in two ways.

Register a callable directly

This is the recommended shorthand. The tree builder internally calls the corresponding task registration API, so the syntax is shorter and works well in documentation and business code.

Use root(...) and node<Level>(...) for regular tasks:

auto tree = yorch::task_tree
.root([]() noexcept -> int {
return 3;
})()
.node<1>([](const int& value) noexcept -> int {
return value + 1;
})(yorch::borrow_prev<int>());

Use root_into(...) and node_into<Level>(...) for direct-output tasks:

auto tree = yorch::task_tree
return out.success("hello");
})()
.node_into<1>(
[](const std::string& text,
return out.success(static_cast<int>(text.size()));
})(yorch::borrow_prev<std::string>());
Output sink passed to direct-output tasks.
constexpr step_result success(Args &&... args) noexcept(noexcept(emplace(std::forward< Args >(args)...)))
Represents the basic outcome of a task step.
Definition result.hpp:42

Use node_forward_prev<Level>(...) for forward-prev tasks:

auto tree = yorch::task_tree
.root([]() noexcept -> int {
return 5;
})()
.node_forward_prev<1>(
[](int& value) noexcept -> yorch::step_result {
value += 4;
})(yorch::borrow_prev_mut<int>());
static constexpr step_result success() noexcept
Creates a successful result.
Definition result.hpp:46

A forward-prev task depends on the direct parent's output, so in an executable tree it is normally used as a non-root node.

Register an already-bound task

You can also first create a task with the task registration API, bind its arguments, and then place that task into the tree.

This style is useful when task definitions and tree structure are maintained separately, or when a task must first be composed through adapters, factory functions, or local variables.

auto make_value = yorch::task([]() noexcept -> int {
return 3;
})();
auto add_one = yorch::task([](const int& value) noexcept -> int {
return value + 1;
})(yorch::borrow_prev<int>());
auto tree = yorch::task_tree
.root(make_value)
.node<1>(add_one);
constexpr auto task(F &&f)
Definition core.hpp:207

Direct-output tasks can be bound first as well:

auto emit_text = yorch::task_into(
return out.success("hello");
})();
auto read_text = yorch::task([](const std::string& text) noexcept -> int {
return static_cast<int>(text.size());
})(yorch::borrow_prev<std::string>());
auto tree = yorch::task_tree
.root_into(emit_text)
.node<1>(read_text);
constexpr auto task_into(F &&f)
Definition core.hpp:262

The task already contains its argument descriptions, so .root(task) / .node<Level>(task) do not take an extra argument list.

Member function tasks can use the same pattern. You may first create a task object with task_member(...), task_into_member(...), or task_forward_prev_member(...), then place it into the tree with the normal .root(task), .root_into(task), .node<Level>(task), or .node_into<Level>(task) APIs.

struct Worker {
int seed(int delta) noexcept {
return delta * 2;
}
int add(const int& value, int delta) noexcept {
return value + delta;
}
};
Worker worker;
auto seed = yorch::task_member(
&Worker::seed,
yorch::value(std::ref(worker)))(
auto add_one = yorch::task_member(
&Worker::add,
yorch::value(std::ref(worker)))(
yorch::borrow_prev<int>(),
auto tree = yorch::task_tree
.root(seed)
.node<1>(add_one);
constexpr auto task_member(F &&f, ReceiverSpec &&receiver_spec)
Definition core.hpp:229

Member Function Tasks

A member function cannot be passed directly as a regular callable to root(...) or node<Level>(...), because it also needs a receiver. The tree builder provides separate shorthand APIs for member functions.

Use root_member(...) and node_member<Level>(...) for regular member functions:

struct Worker {
int base = 0;
int seed(int delta) noexcept {
base += delta;
return base;
}
};
Worker worker;
auto tree = yorch::task_tree
&Worker::seed,
yorch::value(std::ref(worker)))(

Use root_into_member(...) and node_into_member<Level>(...) for direct-output member functions:

struct Worker {
int base = 0;
yorch::step_result emit_text(
int delta,
base += delta;
return out.success(std::to_string(base));
}
};
Worker worker;
auto tree = yorch::task_tree
&Worker::emit_text,
yorch::value(std::ref(worker)))(

Use node_forward_prev_member<Level>(...) for forward-prev member functions:

struct Payload {
int value = 0;
};
struct Service {
yorch::step_result bump(Payload& payload, int delta) noexcept {
payload.value += delta;
}
};
auto tree = yorch::task_tree
.root([]() noexcept -> Payload {
return Payload {6};
})()
.node_forward_prev_member<1>(
&Service::bump,
yorch::from_ctx<Service>())(
yorch::borrow_prev_mut<Payload>(),

The receiver is also an argument source description. It can come from value(...), from_ctx<T>(), borrow_prev<T>(), borrow_prev_mut<T>(), copy_prev<T>(), or consume_prev<T>().

Fanout Policy

When one parent node has multiple direct children and those children access the parent output, the fanout semantics must be considered.

The default policy is yorch::fanout_auto_policy. Usually it does not need to be written explicitly:

auto tree = yorch::task_tree
.root([]() noexcept -> int {
return 10;
})()
.node<1>([](const int& value) noexcept {
return value + 1;
})(yorch::borrow_prev<int>())
.node<1>([](int value) noexcept {
return value + 2;
})(yorch::copy_prev<int>());

Its meaning is:

  • If a parent has at most one direct child, that child may use any prev access semantic supported by the task, including borrow_prev<T>(), borrow_prev_mut<T>(), copy_prev<T>(), and consume_prev<T>().
  • If a parent has multiple direct children, any child that accesses the parent output can only use shared-read semantics by default: borrow_prev<T>() and copy_prev<T>().
  • In multi-child fanout, the default policy does not allow any child to use exclusive semantics: borrow_prev_mut<T>() or consume_prev<T>(). If one child should consume the parent output while other children use copies, select fanout_consume_with_copies_policy explicitly.

Use yorch::fanout_shared_readonly_policy when you want to explicitly state that the parent output is shared read-only by multiple children:

auto tree = yorch::task_tree
.root(
[]() noexcept -> int {
return 10;
},
.node<1>([](const int& value) noexcept {
return value + 1;
})(yorch::borrow_prev<int>())
.node<1>([](int value) noexcept {
return value + 2;
})(yorch::copy_prev<int>());
Explicit readonly fanout policy.
Definition policies.hpp:32

Use yorch::fanout_consume_with_copies_policy when one child may consume the parent output while other children use copied values:

auto tree = yorch::task_tree
.root(
[]() noexcept -> std::string {
return "payload";
},
.node<1>([](std::string text) noexcept {
return text.size();
})(yorch::copy_prev<std::string>())
.node<1>([](std::string text) noexcept {
return text.empty();
})(yorch::consume_prev<std::string>());
Mixed fanout policy that allows one consumer plus any number of copies.
Definition policies.hpp:46

Adapter

When registering a callable directly, adapters(...) can also be used on the tree builder. This is equivalent in spirit to first binding with yorch::task(..., yorch::adapters(...)) and then registering the bound task, but the syntax is more centralized.

adapters(...) is placed in the first set of tree-builder parentheses, the .root(...) / .node<Level>(...) layer. The following (...) still contains only argument source specs.

For regular callables:

.root(callable, adapters(...))(spec...)
.node<Level>(callable, adapters(...))(spec...)

If the same node also specifies a fanout policy, the order is:

.root(callable, fanout_policy, adapters(...))(spec...)
.node<Level>(callable, fanout_policy, adapters(...))(spec...)

Member functions have one extra receiver argument, so adapters(...) is placed after the receiver:

.root_member(member_ptr, receiver_spec, adapters(...))(spec...)
.node_member<Level>(member_ptr, receiver_spec, adapters(...))(spec...)

If a member function also specifies a fanout policy, the order is:

.root_member(member_ptr, receiver_spec, fanout_policy, adapters(...))(spec...)
.node_member<Level>(member_ptr, receiver_spec, fanout_policy, adapters(...))(spec...)

root_into(...), node_into<Level>(...), root_into_member(...), and node_into_member<Level>(...) follow the same parameter placement rules.

auto tree = yorch::task_tree
.root(
throw std::runtime_error("boom");
},
constexpr auto adapt_catch_as_failure() noexcept
Definition adapters.hpp:36
constexpr auto adapters(Descs &&... descs)
Definition adapters.hpp:112

adapters(...) receives concrete adapter descriptions. A policy needed by an adapter is passed to the adapter factory function, not as an independent tree-builder argument.

For example, the catch adapter has two forms:

The custom catch policy receives std::exception_ptr, must be callable with noexcept, and must return a value convertible to the task return type. For direct-output tasks, it returns yorch::step_result.

auto fallback = [](const std::exception_ptr&) noexcept {
};
auto tree = yorch::task_tree
.root(
throw std::runtime_error("boom");
},
static constexpr step_result failure() noexcept
Creates a failed result.
Definition result.hpp:51

The retry adapter policy is also passed to the adapter factory:

auto tree = yorch::task_tree
.root(
[]() noexcept -> yorch::step_result {
},
constexpr auto adapt_retry(Policy &&policy)
Definition adapters.hpp:29
Retry policy that allows a fixed number of additional retries.
Definition retry.hpp:16
static constexpr step_result retry() noexcept
Creates a retry result.
Definition result.hpp:56

When both fanout policy and adapter are present, the fanout policy is a parameter of the tree node, while policies such as retry_fixed_policy are parameters of the adapter itself:

Multiple adapters can be placed inside the same adapters(...). They are applied in written order: earlier adapters are closer to the original task, and later adapters wrap around the outside.

If a task object is already bound and then placed into the tree, adapters should be provided to yorch::task(...), yorch::task_into(...), or the corresponding member-function task registration API. .root(task) / .node<Level>(task) do not accept an additional adapters(...) argument.

Compile and Run

A task tree is only a static structure. To execute it, compile it into a plan and then run the plan:

auto plan = yorch::compile_plan(tree);
const auto result = yorch::run_plan(plan);
if (!result.ok()) {
// handle failure
}
constexpr auto compile_plan(task_tree_builder< Nodes... > &&tree)
Compiles a populated task_tree_builder into a static plan.
constexpr step_result run_plan(Plan &plan)
Executes a compiled plan using the selected serial depth-first policy.
Definition plan.hpp:50

If a task uses from_ctx<T>(), pass the context to run_plan:

struct Config {
int base = 0;
};
auto tree = yorch::task_tree
.root([](const Config& config) noexcept {
return config.base + 1;
})(yorch::from_ctx<Config>());
yorch::context<Config> ctx(Config {.base = 41});
auto plan = yorch::compile_plan(tree);
const auto result = yorch::run_plan(plan, ctx);
Statically typed context container with a compile-time schema.
Definition context.hpp:105

Summary

  • Prefer registering callables directly on the tree builder, such as .root(f)(...) and .node<1>(f)(...).
  • If you already have a bound task, place it into the tree with .root(task) and .node<Level>(task).
  • Use root_into(...) / node_into<Level>(...) for direct-output tasks.
  • Use node_forward_prev<Level>(...) for forward-prev tasks.
  • Use root_member(...), node_member<Level>(...), and their into / forward_prev variants for member functions.