# Parallel map and reduce over lists and arrays, a worker pool, select over channels, and timeouts.
import Base
# import bend-kit-concurrency@0.2.0.0/concurrency.bend as Conc
# ys = Conc.par_map(~U32, ~U32, ~work, 8, xs)
#
# par_map and par_reduce are pure. They split the input into at most `workers`
# balanced parts (rounded down to a power of two) and run the parts as parallel
# calls, so a native build spreads them over its `--threads`. Results keep the
# input order. par_reduce needs an associative f; the list form also needs z to
# be f's identity, since each part starts from z.
#
# pool, select, and timeout are IO, on Base's Chan, IO.spawn and IO.fork. Base
# has no cancellation: an abandoned computation keeps running, and the program
# exits only when every computation is done.
# The length of xs, beside xs.
def count.cons(-A: Type, h: A, r: Nat & List) -> Nat & List:
(n, t) = r
(1n+n, h <> t)
def count(-A: Type, xs: List) -> Nat & List:
match xs:
case Nil{}:
(0n, Nil{})
case h <> t:
count.cons(A, h, count(A, t))
# The first n elements of xs, and the rest.
def split.cons(-A: Type, h: A, r: List & List) -> List & List:
(l, rest) = r
(h <> l, rest)
def split(-A: Type, xs: List, n: Nat) -> List & List:
match xs n:
case Nil{} _:
(Nil{}, Nil{})
case h <> t 0n:
(Nil{}, h <> t)
case h <> t 1n+p:
split.cons(A, h, split(A, t, p))
# lr holds two halves of nl and nr elements. Depth d runs 2^(d+1) parallel parts.
def par_map.list.go(
~A: Type, ~B: Type, ~f: A -> B, d: Nat, +nl: Nat, +nr: Nat, lr: List & List
) -> List:
match d:
case 0n:
(l, r) = lr
lo hi = List.map(~A, ~B, ~f, l) List.map(~A, ~B, ~f, r)
List.append(&1, B, lo, hi)
case 1n+ +p:
(l, r) = lr
+hl = Nat.div(nl, 2n)
+hr = Nat.div(nr, 2n)
ls = split(A, l, hl)
rs = split(A, r, hr)
lo hi = par_map.list.go(~A, ~B, ~f, p, hl, Nat.sub(nl, hl), ls) par_map.list.go(~A, ~B, ~f, p, hr, Nat.sub(nr, hr), rs)
List.append(&1, B, lo, hi)
def par_map.list.top(~A: Type, ~B: Type, ~f: A -> B, d: Nat, nx: Nat & List) -> List:
match d:
case 0n:
(n, xs) = nx
List.map(~A, ~B, ~f, xs)
case 1n+p:
(+n, xs) = nx
+h = Nat.div(n, 2n)
par_map.list.go(~A, ~B, ~f, p, h, Nat.sub(n, h), split(A, xs, h))
# List.map(f, xs), on at most `workers` parallel parts.
def par_map(~A: Type, ~B: Type, ~f: A -> B, workers: U32, xs: List) -> List:
par_map.list.top(~A, ~B, ~f, U32.log2(workers), count(A, xs))
def fold(~A: Data, ~f: A -> A -> A, xs: List, acc: A) -> A:
match xs:
case Nil{}:
acc
case h <> t:
fold(~A, ~f, t, f(acc, h))
def par_reduce.list.go(
~A: Data, ~f: A -> A -> A, d: Nat, +nl: Nat, +nr: Nat, +z: A, lr: List & List
) -> A:
match d:
case 0n:
(l, r) = lr
lo hi = fold(~A, ~f, l, z) fold(~A, ~f, r, z)
f(lo, hi)
case 1n+ +p:
(l, r) = lr
+hl = Nat.div(nl, 2n)
+hr = Nat.div(nr, 2n)
ls = split(A, l, hl)
rs = split(A, r, hr)
lo hi = par_reduce.list.go(~A, ~f, p, hl, Nat.sub(nl, hl), z, ls) par_reduce.list.go(~A, ~f, p, hr, Nat.sub(nr, hr), z, rs)
f(lo, hi)
def par_reduce.list.top(~A: Data, ~f: A -> A -> A, d: Nat, +z: A, nx: Nat & List) -> A:
match d:
case 0n:
(n, xs) = nx
fold(~A, ~f, xs, z)
case 1n+p:
(+n, xs) = nx
+h = Nat.div(n, 2n)
par_reduce.list.go(~A, ~f, p, h, Nat.sub(n, h), z, split(A, xs, h))
# The left fold of xs from z, on at most `workers` parallel parts. f must be
# associative and z its identity.
def par_reduce(~A: Data, ~f: A -> A -> A, workers: U32, +z: A, xs: List) -> A:
par_reduce.list.top(~A, ~f, U32.log2(workers), z, count(A, xs))
# An Array is a balanced tree, so each ANode above depth d is a parallel call.
def par_map.array.go(~A: Type, ~B: Type, ~f: A -> B, a: Array, d: Nat) -> Array:
match a d:
case ALeaf{x} _:
ALeaf{f(x)}
case ANode{xs, ys} 0n:
ANode{par_map.array.go(~A, ~B, ~f, xs, 0n), par_map.array.go(~A, ~B, ~f, ys, 0n)}
case ANode{xs, ys} 1n+ +p:
lo hi = par_map.array.go(~A, ~B, ~f, xs, p) par_map.array.go(~A, ~B, ~f, ys, p)
ANode{lo, hi}
# f on every slot of a, on at most `workers` parallel parts. Slots keep their index.
def par_map.array(~A: Type, ~B: Type, ~f: A -> B, workers: U32, a: Array) -> Array:
par_map.array.go(~A, ~B, ~f, a, U32.log2(workers))
def par_reduce.array.go(~A: Type, ~f: A -> A -> A, a: Array, d: Nat) -> A:
match a d:
case ALeaf{x} _:
x
case ANode{xs, ys} 0n:
f(par_reduce.array.go(~A, ~f, xs, 0n), par_reduce.array.go(~A, ~f, ys, 0n))
case ANode{xs, ys} 1n+ +p:
lo hi = par_reduce.array.go(~A, ~f, xs, p) par_reduce.array.go(~A, ~f, ys, p)
f(lo, hi)
# The slots of a combined in index order with an associative f, on at most
# `workers` parallel parts. An Array is never empty, so no identity is needed.
def par_reduce.array(~A: Type, ~f: A -> A -> A, workers: U32, a: Array) -> A:
par_reduce.array.go(~A, ~f, a, U32.log2(workers))
# A pool worker between channel steps. It owns its state s alone.
type Worker<-S: Type, -A: Type, -B: Type> is Type:
WIdle{s: S}
WGot{s: S, got: Maybe<&1, A & Chan(B)>}
WDone{sb: S & B, reply: Chan(B)}
# Takes jobs until the queue is closed and empty, then answers its state.
# Each job takes three steps, so 3 * (jobs + 1) fuel is enough.
def pool.worker(
~S: Type, ~A: Type, ~B: Type, ~work: S -> A -> IO(S & B), fuel: Nat,
+q: Chan(A & Chan(B)), w: Worker
) -> IO(S):
match fuel:
case 0n:
IO.die(S, 1, "Conc.pool: a worker ran out of fuel")
case 1n+p:
match w:
case WIdle{s}:
do IO:
got : Maybe<&1, A & Chan(B)> <- Chan.recv(A & Chan(B), q)
pool.worker(~S, ~A, ~B, ~work, p, q, WGot{s, got})
case WGot{s, got}:
match got:
case None{}:
IO.pure(S, s)
case Some{(a, reply)}:
do IO:
sb : S & B <- work(s, a)
pool.worker(~S, ~A, ~B, ~work, p, q, WDone{sb, reply})
case WDone{sb, reply}:
(s, b) = sb
do IO:
sent : Result<&1, &1, B, Unit> <- Chan.send(B, reply, b)
pool.worker(~S, ~A, ~B, ~work, p, q, WIdle{s})
# Queues each job with its own reply channel, and answers the replies in order.
def pool.put(~A: Type, ~B: Type, xs: List, +q: Chan(A & Chan(B))) -> IO(List):
match xs:
case Nil{}:
IO.pure(List, Nil{})
case x <> rest:
do IO>:
reply : Chan(B) <- Chan.new(B, 1)
+r : Chan(B) = reply
sent : Result<&1, &1, A & Chan(B), Unit> <- Chan.send(A & Chan(B), q, (x, r))
replies : List <- pool.put(~A, ~B, rest, q)
return r <> replies
def pool.start(
~S: Type, ~A: Type, ~B: Type, ~work: S -> A -> IO(S & B), states: List,
+fuel: Nat, +q: Chan(A & Chan(B))
) -> IO(List):
match states:
case Nil{}:
IO.pure(List, Nil{})
case s <> rest:
do IO>:
c : Chan(S) <- IO.fork(S, pool.worker(~S, ~A, ~B, ~work, fuel, q, WIdle{s}))
cs : List <- pool.start(~S, ~A, ~B, ~work, rest, fuel, q)
return c <> cs
def pool.join(-A: Type, cs: List) -> IO(List):
match cs:
case Nil{}:
IO.pure(List, Nil{})
case c <> rest:
do IO>:
x : A <- IO.join(A, c)
xs : List <- pool.join(A, rest)
return x <> xs
def pool.run(
~S: Type, ~A: Type, ~B: Type, ~work: S -> A -> IO(S & B), states: List,
nx: Nat & List
) -> IO(List & List):
(+n, xs) = nx
do IO & List>:
queue : Chan(A & Chan(B)) <- Chan.new(A & Chan(B), U32.from_nat(n))
+q : Chan(A & Chan(B)) = queue
replies : List <- pool.put(~A, ~B, xs, q)
Chan.close(A & Chan(B), q)
workers : List <- pool.start(~S, ~A, ~B, ~work, states, Nat.mul(3n, 1n+n), q)
outs : List <- pool.join(B, replies)
done : List <- pool.join(S, workers)
return (done, outs)
# Runs work(s, x) for every x in xs, on one worker per state in states. A
# worker owns its state and threads it through its jobs, so an affine handle
# such as a connection pool is never shared. Answers the final states in
# worker order and the results in the order of xs.
def pool(
~S: Type, ~A: Type, ~B: Type, ~work: S -> A -> IO(S & B), states: List, xs: List
) -> IO(List & List):
match states:
case Nil{}:
IO.die(List & List, 1, "Conc.pool: no workers")
case s <> rest:
pool.run(~S, ~A, ~B, ~work, s <> rest, count(A, xs))
# A select relay between channel steps.
type Relay<-A: Type> is Type:
RWait{}
RGot{got: Maybe<&1, A>}
RSent{ok: Result<&1, &1, U32 & A, Unit>}
# Forwards each value of src to out as (i, value), then reports on done.
# It stops when src closes, or when out is closed under it.
def select.relay(
-A: Type, fuel: Nat, +i: U32, +src: Chan(A), +out: Chan(U32 & A),
+done: Chan(Unit), r: Relay
) -> IO(Result<&1, &1, Unit, Unit>):
match fuel:
case 0n:
Chan.send(Unit, done, Unit{})
case 1n+p:
match r:
case RWait{}:
do IO>:
got : Maybe<&1, A> <- Chan.recv(A, src)
select.relay(A, p, i, src, out, done, RGot{got})
case RGot{got}:
match got:
case None{}:
Chan.send(Unit, done, Unit{})
case Some{x}:
do IO>:
ok : Result<&1, &1, U32 & A, Unit> <- Chan.send(U32 & A, out, (i, x))
select.relay(A, p, i, src, out, done, RSent{ok})
case RSent{ok}:
match ok:
case Done{u}:
select.relay(A, p, i, src, out, done, RWait{})
case Fail{held}:
Chan.send(Unit, done, Unit{})
# ponytail: relay fuel is the largest Nat literal, and a value takes three
# steps, so a relay stops after about 1.4 billion values. Drop the fuel if
# Base gains an unbounded loop.
def select.start(
-A: Type, chans: List, +i: U32, +out: Chan(U32 & A), +done: Chan(Unit)
) -> IO(Nat):
match chans:
case Nil{}:
IO.pure(Nat, 0n)
case src <> rest:
do IO:
IO.spawn(Result<&1, &1, Unit, Unit>, select.relay(A, 4294967295n, i, src, out, done, RWait{}))
n : Nat <- select.start(A, rest, (i + 1 : U32), out, done)
return 1n+n
# Closes out once all n relays have reported.
def select.close(-A: Type, n: Nat, +out: Chan(U32 & A), +done: Chan(Unit)) -> IO(Unit):
match n:
case 0n:
do IO:
Chan.close(U32 & A, out)
Chan.close(Unit, done)
case 1n+p:
do IO:
got : Maybe<&1, Unit> <- Chan.recv(Unit, done)
select.close(A, p, out, done)
def select.go(-A: Type, chans: List, +out: Chan(U32 & A), +done: Chan(Unit)) ->
IO(Chan(U32 & A)):
do IO:
n : Nat <- select.start(A, chans, 0, out, done)
IO.spawn(Unit, select.close(A, n, out, done))
return out
# One channel that carries (i, value) for each value received on chans[i], in
# the order they arrive. Chan.recv on it waits for whichever source is ready
# first, and answers None once every source is closed. The selector owns the
# receiving side of its sources. Close it to stop early: each relay then drops
# the one value it holds, if any, and stops.
def select(-A: Type, chans: List) -> IO(Chan(U32 & A)):
do IO:
out : Chan(U32 & A) <- Chan.new(U32 & A, 0)
done : Chan(Unit) <- Chan.new(Unit, 0)
select.go(A, chans, out, done)
def timeout.won(-A: Type, +c: Chan(Maybe<&1, A>), got: Maybe<&1, Maybe<&1, A>>) ->
IO(Maybe<&1, A>):
match got:
case None{}:
IO.pure(Maybe<&1, A>, None{})
case Some{m}:
do IO>:
Chan.close(Maybe<&1, A>, c)
return m
# Some{result} if act finishes within ms milliseconds, else None at the
# deadline. act is not cancelled: it runs on, its late result is dropped, and
# the program waits for it (and for the timer) before it exits.
def timeout(-A: Type, ms: U32, act: IO(A)) -> IO(Maybe<&1, A>):
do IO>:
chan : Chan(Maybe<&1, A>) <- Chan.new(Maybe<&1, A>, 0)
+c : Chan(Maybe<&1, A>) = chan
IO.spawn(Result<&1, &1, Maybe<&1, A>, Unit>, IO.bind(A, Result<&1, &1, Maybe<&1, A>, Unit>, act, x => Chan.send(Maybe<&1, A>, c, Some{x})))
IO.spawn(Result<&1, &1, Maybe<&1, A>, Unit>, IO.bind(Unit, Result<&1, &1, Maybe<&1, A>, Unit>, IO.sleep(ms), u => Chan.send(Maybe<&1, A>, c, None{})))
got : Maybe<&1, Maybe<&1, A>> <- Chan.recv(Maybe<&1, A>, c)
timeout.won(A, c, got)