Ship

Book · One-shot values

One-shot values

Draft

Coordinate fibers with a value and its unique resolver.

On this page · 3 sections

One resolver

OneShot.make() returns an aliased value for waiters and a unique resolver for its producer. Resolving consumes that capability, so checked Ship cannot publish twice.

let publishpublish: fn(resolver: OneShot.Resolver(Int) @ unique, value: Int) -> Unit = fn(resolverresolver: OneShot.Resolver(Int): OneShot.Resolver(Int) @ unique, valuevalue: Int) {
  OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(resolverresolver: OneShot.Resolver(Int), valuevalue: Int);
};

One Resolution

test✓
  let (~valuevalue: OneShot(Int), ~resolverresolver: OneShot.Resolver(Int)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Int), resolver: OneShot.Resolver(Int) @ unique)();
  publishpublish: fn(resolver: OneShot.Resolver(Int) @ unique, value: Int) -> Unit(resolverresolver: OneShot.Resolver(Int), 4141: Int);
  ✓valuevalue: OneShot(Int): OneShot(Int);
  expect ✓await valuevalue: OneShot(Int) == 4141: Int;

Waiting without blocking

Waiting parks only the current fiber. Another child can make progress and complete the value on the same event loop.

Waiting Fiber

test✓
  let (~value: startedstarted: OneShot(Unit), ~resolver: startstart: OneShot.Resolver(Unit)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Unit), resolver: OneShot.Resolver(Unit) @ unique)();
  let (~value: releaserelease: OneShot(Int), ~resolver: release_withrelease_with: OneShot.Resolver(Int)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Int), resolver: OneShot.Resolver(Int) @ unique)();
  let childchild: Async.Task(Int) = spawn {
    OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Unit) @ unique, value: Unit @ global) -> Unit(startstart: OneShot.Resolver(Unit), ());
    await releaserelease: OneShot(Int) + 11: Int
  };
  await startedstarted: OneShot(Unit);
  OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(release_withrelease_with: OneShot.Resolver(Int), 4141: Int);
  expect ✓await childchild: Async.Task(Int) == 4242: Int;

A callback waits the same way. twice never waits on its own; the fiber that calls it parks wherever its callback does.

let twicetwice: fn(step: fn() -> Int) -> Int = fn(stepstep: fn() -> Int) { stepstep: fn() -> Int() + stepstep: fn() -> Int() };

Waiting Inside A Callback

test✓
  let (~value: startedstarted: OneShot(Unit), ~resolver: startstart: OneShot.Resolver(Unit)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Unit), resolver: OneShot.Resolver(Unit) @ unique)();
  let (~value: releaserelease: OneShot(Int), ~resolver: release_withrelease_with: OneShot.Resolver(Int)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Int), resolver: OneShot.Resolver(Int) @ unique)();
  let childchild: Async.Task(Int) = spawn {
    OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Unit) @ unique, value: Unit @ global) -> Unit(startstart: OneShot.Resolver(Unit), ());
    twicetwice: fn(step: fn() -> Int) -> Int(() => await releaserelease: OneShot(Int))
  };
  await startedstarted: OneShot(Unit);
  OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(release_withrelease_with: OneShot.Resolver(Int), 2020: Int);
  expect ✓await childchild: Async.Task(Int) == 4040: Int;

The resolver remains unique across control-flow paths. Every path may consume it once; no path can consume it twice.

let resolve_branchresolve_branch: fn(flag: Bool) -> Int = fn(flagflag: Bool) {
  let (~valuevalue: OneShot(Int), ~resolverresolver: OneShot.Resolver(Int)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Int), resolver: OneShot.Resolver(Int) @ unique)();
  if flagflag: Bool {
    OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(resolverresolver: OneShot.Resolver(Int), 11: Int);
  } else {
    OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(resolverresolver: OneShot.Resolver(Int), 22: Int);
  }
  await valuevalue: OneShot(Int)
};

let resolve_matchresolve_match: fn(flag: Bool) -> Int = fn(flagflag: Bool) {
  let (~valuevalue: OneShot(Int), ~resolverresolver: OneShot.Resolver(Int)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Int), resolver: OneShot.Resolver(Int) @ unique)();
  match flagflag: Bool {
    | True -> OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(resolverresolver: OneShot.Resolver(Int), 33: Int)
    | False -> OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(resolverresolver: OneShot.Resolver(Int), 44: Int)
  }
  await valuevalue: OneShot(Int)
};

let resolve_if_letresolve_if_let: fn(candidate: Option(Int)) -> Int = fn(candidatecandidate: Option(Int)) {
  let (~valuevalue: OneShot(Int), ~resolverresolver: OneShot.Resolver(Int)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Int), resolver: OneShot.Resolver(Int) @ unique)();
  if let Some(itemitem: Int) = candidatecandidate: Option(Int) {
    OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(resolverresolver: OneShot.Resolver(Int), itemitem: Int);
  } else {
    OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Int) @ unique, value: Int @ global) -> Unit(resolverresolver: OneShot.Resolver(Int), 55: Int);
  }
  await valuevalue: OneShot(Int)
};

Resolution Paths

test✓
  expect ✓(resolve_branchresolve_branch: fn(flag: Bool) -> Int(TrueTrue: Bool), resolve_branchresolve_branch: fn(flag: Bool) -> Int(FalseFalse: Bool)) == (11: Int, 22: Int);
  expect ✓(resolve_matchresolve_match: fn(flag: Bool) -> Int(TrueTrue: Bool), resolve_matchresolve_match: fn(flag: Bool) -> Int(FalseFalse: Bool)) == (33: Int, 44: Int);
  expect ✓(resolve_if_letresolve_if_let: fn(candidate: Option(Int)) -> Int(Some(66: Int)), resolve_if_letresolve_if_let: fn(candidate: Option(Int)) -> Int(None)) == (66: Int, 55: Int);

Completion and join order

Completion order and join order are independent. A later child may finish first; joining the earlier child still returns its own value.

type OrderProbe = {
  mut events: Vector(String);
}

let recordrecord: fn(probe: Book.Concurrency.Coordination.OrderProbe @ global, event: String @ global) -> Unit = fn(probeprobe: Book.Concurrency.Coordination.OrderProbe, eventevent: String) {
  probe.events := Vector.pushpush: pure fn!('a)(items: Vector(String), item: String) -> Vector(String)(probeprobe: Book.Concurrency.Coordination.OrderProbe.eventsevents: Vector(String), eventevent: String);
};

Completion Before Join

test✓
  let probeprobe: Book.Concurrency.Coordination.OrderProbe: OrderProbe @ global = OrderProbe { events = []; };
  let (~value: release_firstrelease_first: OneShot(Unit), ~resolver: first_withfirst_with: OneShot.Resolver(Unit)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Unit), resolver: OneShot.Resolver(Unit) @ unique)();
  let (~value: release_secondrelease_second: OneShot(Unit), ~resolver: second_withsecond_with: OneShot.Resolver(Unit)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Unit), resolver: OneShot.Resolver(Unit) @ unique)();
  let (~value: second_finishedsecond_finished: OneShot(Unit), ~resolver: finish_secondfinish_second: OneShot.Resolver(Unit)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Unit), resolver: OneShot.Resolver(Unit) @ unique)();
  let firstfirst: Async.Task(Int) = spawn {
    await release_firstrelease_first: OneShot(Unit);
    recordrecord: fn(probe: Book.Concurrency.Coordination.OrderProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Coordination.OrderProbe, "first""first": String);
    11: Int
  };
  let secondsecond: Async.Task(Int) = spawn {
    await release_secondrelease_second: OneShot(Unit);
    recordrecord: fn(probe: Book.Concurrency.Coordination.OrderProbe @ global, event: String @ global) -> Unit(probeprobe: Book.Concurrency.Coordination.OrderProbe, "second""second": String);
    OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Unit) @ unique, value: Unit @ global) -> Unit(finish_secondfinish_second: OneShot.Resolver(Unit), ());
    22: Int
  };
  OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Unit) @ unique, value: Unit @ global) -> Unit(second_withsecond_with: OneShot.Resolver(Unit), ());
  await second_finishedsecond_finished: OneShot(Unit);
  OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Unit) @ unique, value: Unit @ global) -> Unit(first_withfirst_with: OneShot.Resolver(Unit), ());
  expect ✓await firstfirst: Async.Task(Int) == 11: Int;
  expect ✓await secondsecond: Async.Task(Int) == 22: Int;
  expect ✓probeprobe: Book.Concurrency.Coordination.OrderProbe.eventsevents: Vector(String) == ["second""second": String, "first""first": String];

Joining a later-spawned child does not implicitly join or cancel its older sibling.

Join Later Child First

test✓
  let (~value: release_firstrelease_first: OneShot(Unit), ~resolver: first_withfirst_with: OneShot.Resolver(Unit)) = OneShot.makemake: pure fn!('a)() -> (value: OneShot(Unit), resolver: OneShot.Resolver(Unit) @ unique)();
  let firstfirst: Async.Task(Int) = spawn {
    await release_firstrelease_first: OneShot(Unit);
    11: Int
  };
  let secondsecond: Async.Task(Int) = spawn {
    22: Int
  };
  expect ✓await secondsecond: Async.Task(Int) == 22: Int;
  OneShot.resolveresolve: pure fn!('a)(resolver: OneShot.Resolver(Unit) @ unique, value: Unit @ global) -> Unit(first_withfirst_with: OneShot.Resolver(Unit), ());
  expect ✓await firstfirst: Async.Task(Int) == 11: Int;

OneShot stores successful coordination values only. Producer errors use the owning fiber's structural error channel instead of becoming a second future-shaped failure container.