Skip to content

lib: add parallel map macro#127

Open
efd6 wants to merge 1 commit into
devfrom
parallel
Open

lib: add parallel map macro#127
efd6 wants to merge 1 commit into
devfrom
parallel

Conversation

@efd6

@efd6 efd6 commented May 11, 2026

Copy link
Copy Markdown
Collaborator

See commit message for details. A detailed explanation of the mechanism is available on request.

@efd6 efd6 requested a review from a team May 11, 2026 00:07
@efd6 efd6 self-assigned this May 11, 2026
@efd6 efd6 force-pushed the parallel branch 2 times, most recently from c7aa8e1 to e982a19 Compare May 11, 2026 05:48
Add a .parallel() macro that mirrors .map() but evaluates body
expressions concurrently. Three call forms are supported:

    range.parallel(v, expr)       // apply to every element
    range.parallel(v, pred, expr) // apply where pred is true
    range.parallel(v, v2, expr)   // two-variable (index/key + value)

Concurrency is controlled by two knobs set at the environment level
via Parallel(perCall, globalCap):

  - perCall bounds goroutines per .parallel() invocation (defaults to
    GOMAXPROCS).
  - globalCap bounds total concurrent leaf evaluations across all
    .parallel() calls in the program (0 means unlimited).

When perCall == 1 the macro degenerates to a sequential comprehension
with no goroutine overhead.

Output order always matches input order. Errors are surfaced
deterministically: the first error in iteration order is returned.

The implementation uses sentinel function calls (@parallel,
@parallel_body, @parallel_pred) in the AST so that decorators can
recover the planned body and predicate Interpretables at program
construction time, working around the unexported evalFold in cel-go.
Comment thread lib/parallel.go
Comment on lines +27 to +50
// # Call forms
//
// parallel mirrors the call forms of the built-in map macro:
//
// // (1) One-variable — apply expr to every element.
// <list>.parallel(<iterVar>, <expr>) -> list<dyn>
// <map>.parallel(<iterVar>, <expr>) -> list<dyn>
//
// // (2a) Predicate filter — apply expr only where pred is true.
// <list>.parallel(<iterVar>, <pred>, <expr>) -> list<dyn>
// <map>.parallel(<iterVar>, <pred>, <expr>) -> list<dyn>
//
// // (2b) Two-variable — bind index/key and value simultaneously.
// <list>.parallel(<iterVar>, <iterVar2>, <expr>) -> list<dyn>
// <map>.parallel(<iterVar>, <iterVar2>, <expr>) -> list<dyn>
//
// Forms (2a) and (2b) are distinguished by their second argument: if it is a
// plain identifier it is treated as iterVar2 (two-variable form); otherwise it
// is treated as a boolean predicate (filter form). This is the same rule used
// by map.
//
// For list ranges in the two-variable form, iterVar is the zero-based element
// index and iterVar2 is the element value. For map ranges, iterVar is the key
// and iterVar2 is the corresponding value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have map in the name, like parallel_map or pmap, which are used in some other languages/libraries.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to have a comment that says a bit more explicitly how it differs from map.

From what I can see in the comments and explanatory notes, it's going to do the same as map, except that it runs in parallel, and if one item fails it won't cancel the others. So failures may run longer (due to processing all items rather then stopping at the first failure), but successes will likely run quicker (due to parallelism). If there are multiple failures you'll get the first one from the list/map order, not time order, matching map. Is that right? Are there any other differences.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can anything go wrong in usage, besides using a custom function that isn't safe for concurrency?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the global_concurrency setting would stop someone starting too many goroutines by nesting parallel calls. That's good.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pmap was the original name. I moved to parallel to increase friction. I think parallel_map is too far and does not significantly improve comprehensibility (if this tips a user over the edge into thinking that they understand the consequences of using it, they probably should not be using it).

It would be good to have a comment that says a bit more explicitly how it differs from map.

Semantically (ignoring parallelism), it is identical to map. In terms of wall time, yes it may run longer in the case that it fails on one and the failure should cancel the full set of evaluations. This is not semantics though.

Can anything go wrong in usage, besides using a custom function that isn't safe for concurrency?

Quicker system resource exhaustion? Otherwise no.

Comment thread lib/parallel.go

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you thought about getting it into cel-go?

@efd6 efd6 May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The use there is almost entirely absent. CEL in the conventional usage is for dynamic configuration logic, and should almost never have workloads that would be helped by this. Adding it would be an unnecessary maintenance burden on that project.

There is merit in filing an issue (marked in a TODO here) to make the work we do easier. That should be accepted, but still needs discussion with cel. I'd like to have this in place and working as an evidence point for that request before I file it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants