Skip to content

Concurrency: and groups

Sequential bindings become concurrent by joining them with and:

dashboard.hemhem
let dashboard = (id: string) => effect {
  let user  = fetchUser(id)!
  and orgs  = fetchOrgs(id)!
  and prefs = fetchPrefs(id)!
  render(user, orgs, prefs)
}
dashboard.ts — generatedts
const { user, orgs, prefs } =
  yield *
  Effect.all(
    { user: fetchUser(id), orgs: fetchOrgs(id), prefs: fetchPrefs(id) },
    { concurrency: "unbounded" },
  );

(Plain-identifier patterns get the record form, self-documenting output; destructuring patterns fall back to the positional tuple.)

Grouped bindings are simultaneous: no member’s initializer may reference a name bound in the same group, and the compiler enforces it. That check is what makes the implied concurrency sound:

let dashboard = (id: string) => effect {
  let user = fetchUser(id)!
  and orgs = fetchOrgs(user.orgId)!
  {user, orgs}
}
error[HEM2007] dashboard.hem:3:24
  |
3 |   and orgs = fetchOrgs(user.orgId)!
  |                        ^^^^ Group member 2 references sibling binding `user`. Move that dependency into a separate `let` before the group.

The same shape works on promises in plain async code (let a = await fa() and b = await fb() lowers to await Promise.all([fa(), fb()])), with one difference in behavior: on failure, effect groups interrupt their in-flight siblings (structured concurrency); promise groups reject while remaining promises continue running, because promises cannot be cancelled. Markers are all-or-nothing per group (every member !-ed, or every member awaited, or a pure group meaning “evaluate all, then bind”), the group’s binder keyword heads the whole group: use conn = Db.acquire and cache = Redis.acquire acquires concurrently into the block scope (an extension-only form: without scopes, a partially failed concurrent acquisition would leak, so core use stays out of groups), and let/use don’t mix within one group. First failure interrupts; the HEM41xx provenance still blames the exact member that introduced a requirement or error.

When one member depends on another, several approaches are available: split the dependency into a separate let (a sequential phase); name the chain and destructure it as a member (let { user, posts } = loadUserPosts(id)! and notifications = fetchNotifications(id)! for concurrent execution without new syntax); use Effect.fork + Fiber.join for ad-hoc DAGs; or use Effect.forEach with a concurrency option for collections. Plain async code follows the same patterns using standard JavaScript mechanisms: an async function for the named chain, and eager promises for fork/join operations.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close