Parallel execution

The par(b=worker_call, threads) clause on a for loop runs a function on every element of a collection in parallel and gives you the results one by one in the loop body.

Why parallel loops?

When you have a large collection and each element can be processed independently — image filters, score calculations, data transforms — par() splits the work across CPU cores automatically. You move the per-element work into a function and name it in the clause; the loop body then reads that function's result instead of computing it.

Syntax

`for a in src par(b = func(a), N) { body }`

src is any source a for loop accepts: a vector, a range, a text, or a keyed collection. See "Order" below for what a hash source changes.

Two call forms

A function whose first parameter is self answers both forms, as it does everywhere else: par(b=get(a), 4) and par(b=a.get(), 4) are one call. A free function answers only Form 1 — written as a method on the element, the compiler says the method is unknown.

What a worker may do

The compiler holds two of these for you, and you hold the third:

The compiler does refuse the case it can see: a worker that writes to state captured from around the loop.

Order

The body sees results in the order the source produced them, whatever order the threads finished in — so for a vector, a range, a text or a sorted, par gives exactly what the sequential loop gives, and N is a speed knob that never changes the answer.

A hash is the one exception, and it is worth knowing before you rely on it: par walks it in storage order rather than key order, so the results arrive in a different order from the plain for loop over the same hash — and in a different order on each run. Iterate a sorted instead when the order matters.

struct Score {
  value: integer
}
struct ScoreList {
  items: vector < Score >
}
struct DoubledScore {
  label: text,
  doubled: integer
}
fn double_score(thr_r: const Score) -> integer {
  thr_r.value * 2
}
fn scale_score(thr_r: const Score, thr_factor: integer) -> integer {
  thr_r.value * thr_factor
}
fn make_doubled(thr_r: const Score) -> DoubledScore {
  DoubledScore { label: "v{thr_r.value}", doubled: thr_r.value * 2 }
}
fn get_value(self: const Score) -> integer {
  self.value
}
fn make_scores() -> ScoreList {
  thr_q = ScoreList { };
  thr_q.items +=[Score {value: 10 }, Score {value: 20 }, Score {value: 30 }];
  thr_q
}
fn main() {

Free function (Form 1)

Each Score's value is doubled by double_score across 4 threads. The loop body sees b with the doubled value, in original order.

  q = make_scores();
  sum = 0;
  for thr_a in q.items par(thr_b = double_score(thr_a), 4) {
    sum += thr_b;
  }

10*2 + 20*2 + 30*2 = 120

  assert(sum == 120, "parallel double: sum == 120");

Extra Arguments

The worker function can take extra arguments from the calling scope, as long as each is a number, a boolean or a character. Here scale_score(a, factor) passes factor=3 to every worker.

  q1b = make_scores();
  factor = 3;
  scaled_sum = 0;
  for thr_a in q1b.items par(thr_b = scale_score(thr_a, factor), 4) {
    scaled_sum += thr_b;
  }

10*3 + 20*3 + 30*3 = 180

  assert(scaled_sum == 180, "extra arg: scaled sum == 180");

Struct Return

A worker returns whatever its own type says: a number, a boolean, a character, a text, a struct, or a vector. Text and struct results are deep-copied, so they stay valid after the worker thread exits.

  q2 = make_scores();
  labels = "";
  for thr_sa in q2.items par(thr_ds = make_doubled(thr_sa), 1) {
    labels += "{thr_ds.label},";
  }
  assert(labels == "v10,v20,v30,", "struct return: {labels}");

Method call (Form 2)

get_value takes self, so it is a method and b=a.get_value() is the spelling that reaches it.

  q3 = make_scores();
  total = 0;
  for thr_a in q3.items par(thr_b = thr_a.get_value(), 4) {
    total += thr_b;
  }
  assert(total == 60, "method call: total == 60");

Any for-source

A range is a source like any other, and a worker over plain numbers wants no const.

  thr_range_sum = 0;
  for thr_i in 0..5 par(thr_d = thr_double(thr_i), 4) {
    thr_range_sum += thr_d;
  }

(0+1+2+3+4) * 2

  assert(thr_range_sum == 20, "range source: {thr_range_sum}");

Empty Vector

An empty vector is safe — the loop body never executes, no threads are spawned.

  empty = ScoreList { };
  thr_n = 0;
  for thr_a in empty.items par(thr_b = double_score(thr_a), 1) {
    thr_n += 1;
  }
  assert(thr_n == 0, "empty: 0 iterations");

Sequential fallback

Using par(..., 1) runs the worker on a single thread — useful for debugging. The answer is the same one every thread count gives; only the parallelism changes.

  q4 = make_scores();
  seq_sum = 0;
  for thr_a in q4.items par(thr_b = double_score(thr_a), 1) {
    seq_sum += thr_b;
  }
  assert(seq_sum == 120, "sequential par(1): same result");
}
fn thr_double(thr_n: integer) -> integer {
  thr_n * 2
}