do
import coimport std.io
echo : {e : HFunctor} -> {{io : stdio <: e}} -> Co e Unitecho = do input <- readStdin let n = strLength input print ("read " ^ debug n ^ " bytes")A do block is a computation written as statements:
x <- erunseand names its resultx;(a, b) <- etakes the result apart;- a statement
erunse; let x = enamesein the statements after it;let (a, b) = etakes it apart;- the last statement is the block’s result.
x <- e is bind e (\x -> ...), using whatever bind and pure are
in scope. No Monad instance is needed. A pattern that binds must
match every value (Patterns that bind).
Statements
-- | >>> sumEvens [1, 2, 4, 2000000, 6]-- | 6sumEvens : List Int -> IntsumEvens xs = run do let mut total = 0 for x in xs do if x % 2 /= 0 then continue if x > 1000000 then break total := total + x return total
-- | >>> firstLong ["a", "abcd", "xyz"]-- | Just ("abcd")firstLong : List String -> Maybe StringfirstLong ws = run do for w in ws do if strLength w > 3 then return (Just w) Nothinglet mut x = edeclares a variable, andx := esets it. Its type can be given:let mut x : T = e.while c do ...repeats whilecholds.for p in e do ...runs once for each element ofe, matched byp.breakleaves the innermost loop;continuestarts its next turn.return eends the whole block withe.- An
ifamong the statements may have noelse. The arms of aniforcaseamong the statements are statements too. let p = e else sbindsp’s names for the statements after it whenematchesp, and runssotherwise.s, a statement or the statements under it, must leave the block: it ends withreturn,break,continueorabandon, or aniforcasewhose every arm does. An action does not leave: a throw isreturn (<- throw e), and for a value or to go on, write acase. Herepmay fail to match; apthat cannot is let be, itselsenever run.let Just x = lookup k m else return 0let (Just a, b) = pair elseprint "no a"continue(<- c)runscwhere it is written, before the statement it is in:print (<- readStdin).
A lambda cannot set the block’s variables, and cannot use (<- c),
return, break or continue: its body runs later. It sees each
variable’s value at the time the lambda is made.
for goes through anything with an
Iterate instance, such as a list, a
string (each Rune) or range a b.
Pure code
run do ... writes pure code as statements, as the examples above do.
run : Id a -> a uses the identity monad.
Loops compile to tail calls, so they do not grow the stack, in pure code
or in Co.
try
import exception
parsed : {e : HFunctor} -> {{ex : exception String <: e}} -> String -> Co e Intparsed s = case parseInt s of Just n -> pure n Nothing -> throw ("not a number: " ^ s)
total : {e : HFunctor} -> {{ex : exception String <: e}} -> {{io : stdio <: e}} -> List String -> Co e Inttotal words = do let mut sum = 0 for w in words do try n <- parsed w sum := sum + n catch (why : String) -> print why return sumtry runs its statements with the exception effect, and needs
import exception.
- A throw inside is caught by the
catcharm under it, which runs instead. - The arm’s type,
catch (why : String), says which throws it catches. It can also be given:try (err := String). - The statements and the
catcharm belong to the block: they can set its variables,break,continueandreturn. - A throw abandons what it is thrown in, so the
catcharm sees the variables as they were when thetrybegan.
Releasing a resource is not try’s job: a resource is held by a with
of its own effect, such as with f := File.open path do of std.file.
with
import custodianimport exceptionimport std.process
said : {e : HFunctor} -> {{sys : process <: e}} -> {{ex : exception ProcessError <: e}} -> {{cu : custodian <: e}} -> Invocation -> Co e Stringsaid c = do let mut out = "" with withProcess c as p do r <- wait p out := r.out return outwith e as x do runs the statements under it as the body of the
with e, such as withProcess c, with x
what it holds. It is e (\x -> do ...), written as a
statement. Where e hands its body a tuple, with e as (a, b) do
takes it apart: e (\p -> case p of (a, b) -> do ...).
xis in scope in the statements under it alone; aWith s r, as awithmade byscopedholds, cannot leave them (E0311).- The statements belong to the block, as a
try’s do: they can set its variables,break,continueandreturn. The thing is let go of on the way out. - As the block’s last statement, its value is the block’s; otherwise it is dropped.
- A
withthrows what it could not acquire: atryaround it catches that. A failure to release is its custodian’s (below).
What a with holds, and custodians
What a with like withProcess holds lives as long as the program can
reach it. When its body ends, the with lets go of its hold:
- where it held the thing alone, the thing is ended there;
- where the body gave it to another, a thread spawned in the body
given the
With s rby value, it is ended once the last holder lets go of it. Thewithdoes not wait.
A thing that fails to end, now or later, is not thrown to the
with’s caller. It is a failure of the thing’s custodian (below);
outside every custodian’s with, that is the program’s failure: it is
printed and the program ends, as with an uncaught failure.
So a thread can use a command or a connection after the with that
made it has ended. On the C backend “lets go” is the runtime’s count of
references, so a thing is ended as soon as its last holder is done
with it. JavaScript keeps no such count; a with there lets go of its
thing to the collector. A GPU device or buffer is the exception: like a
file, it is released as its block is left.
A with that releases by a computation (resource’s held: a port of
a standard stream, the page’s document) has the other policy: it
releases as its block is left, whoever holds the thing still. Its
release is kept by the program’s top level while the block runs. Every
with of a thing asks for {{cu : custodian <: e}}, as said does
above, and a main that uses one has custodian in its world. Where a throw heard outside the block
leaves it, or a handler drops the continuation, the release still runs:
- on the C backend, at the next operation of the domain that started
the
with; - in JavaScript, when the collector reaches what held it.
A release that fails then is a failure of its custodian, as any other.
withStdin, withStdout and withStderr give the body a port of its
own, a copy of the stream’s descriptor, and close it as the block is
left. std.io’s printing and the stream itself go on as before. A
withDocument lets go of the page’s document as its block is left,
but that closes nothing. The document is the page’s, and the program
still has it wherever it passed it on: in a handler set on an element
and run later, or to another effect by value.
There are custodians, as in Racket (the custodian module). A
custodian is a runtime object that the program holds as a value. Every
thing a with holds is in the custody of one, and with k := Custodian.custody do is how a thing comes to be in k’s:
import bytes as Bytesimport custodian as Custodianimport exceptionimport std.ioimport std.net as Net
served : {s : Type} -> With s Net.TcpListener -> Co (exception Net.NetError <+> (stdio <+> Net.net <+> Custodian.custodian)) Unitserved l = do with k := Custodian.contained do with Net.accept l as c do _ <- Net.write c (Bytes.fromString "hello") pure () served lwith k := Custodian.custody doruns its body withka new custodian, under the one current, andkcurrent in the body. The things the body’swiths hold are ink’s custody, and the custodians made in the body are underk. Outside every suchwith, the root’s custodian is current.- It is Racket’s
parameterizeof the current custodian,custodied k c. The body runs in the block’s world, with no effect added: the onecustodianthere is handled by the runtime, whose monad carries the custodian current, as Reader’s does, and runs the body withk. So a throw, a continuation resumed later, or one resumed twice each keep the custodian current where they were made. Awithof a custodian may be written in any world that hascustodian, inside another’s body too. with k := Custodian.contained dois the same, andkcontains its failures (below).Custodian.manage c stopgives it something to run at its shutdown;Custodian.makemakes a custodian under the one it is made in;Custodian.under c, one underc, shut down with it;Custodian.shutdown cends the things in its custody, runs everything it was given, once and in order, then does the same for the custodians under it. It comes to the reasons of the given things that failed.
A with does not shut its custodian down as its body ends: each thing
is let go of by its own with, as above. A custodian manages
resources, not threads. A thread that took a thing in a custodian’s
custody that was then shut down finds the thing ended at its next
operation on it (a socket’s EBADF), and handles that itself. The
thing is ended, not freed: whoever still holds it holds a closed
socket, not freed memory.
A thing that fails to end, whether its with, its last holder or a
shutdown ends it, is a failure of its custodian. The custodian is shut
down, with the custodians under it, and the failure goes on to the one
it is under. A custodian made by contained stops it there: the
failure is printed, and the program goes on. At the root, the failure
is the program’s: it is printed and the program ends. So a server that
serves each connection in a contained custodian loses only that
connection when its close fails. What the custodians were given runs
on the domain that made them, at its next operation.
What starts to run later keeps the custodian current where it was started, as Racket’s threads do. A domain spawned in a custodian’s body has that custodian current.
A cml thread is a user of a custodian, not one of its things. What it
carries is which custodian its withs give to: at first the one the
withs gave to where it was spawned. No shutdown stops a thread. A
thread’s world, CML.cml w <+> custodian <+> w, has a custodian of
the scheduler’s, and that one covers the one outside. CML.scheduled
runs in the scheduler’s world w itself, Co (CML.cml w <+> custodian <+> w) a -> Co w (Maybe a), and w need not name a custodian: the
scheduler reaches the runtime’s custodians through the domains’
(Domain.rooted), or, where w names one, the one current where it
runs. So a main that starts cml names no custodian, whether by
scheduled or by with _ := CML.threads do: main : Co (stdio <+> Net.net <+> Domain.domain) Unit. The custodian in the threads’ world
is then the only one, and the threads’ withs, a failure that
cascades, and one that reaches the root, which ends the program, are as
in a world that names it. Thread code is written for any world e,
asking for {{cm : CML.cml w <: e}} and {{cu : custodian <: e}}, as
other code is; the concrete world appears only where the program
starts cml.
So a with k := Custodian.custody do in a cml thread is cml’s to
answer. The body stays in the thread. It waits at a channel as
anywhere else, and the other threads run meanwhile. For the extent of
the body, the thread’s withs give to k, and after it they give to
the one before. That goes with the thread’s continuation wherever its
turns are taken, so another thread’s withs never give to k, and a
throw that leaves the body leaves k too. cml passes the custodian’s
other operations on to the one outside, with k current for them: a
thing given to it, a custodian made, a release kept. One thread can
accept connections and serve each in a contained custodian of its
own:
import bytes as Bytesimport cml as CMLimport coimport custodian as Custodianimport exceptionimport resourceimport std.net as Net
serving : {e w : HFunctor} -> {s : Type} -> {{cm : CML.cml w <: e}} -> {{cu : Custodian.custodian <: e}} -> {{nt : Net.net <: e}} -> {{ex : exception Net.NetError <: e}} -> With s Net.TcpListener -> CML.Chan w Int -> Co e Unitserving l ch = do with k := Custodian.contained do with Net.accept l as c do n <- CML.recv ch _ <- Net.write c (Bytes.fromString (debug n)) pure () serving l chA boc behaviour is a user of a custodian too. It takes the custodian
current where it is spawned, and its body runs with that one current:
the things its withs hold are in its custody, and the behaviours it
spawns take it as well. An act awaited by BOC.await, and the
behaviour after it, take the one current at the await, and the act is
one of its things: shut down while the act still waits, the custodian
gives the act up, and the behaviour after it is never spawned.
BOC.awaitFirst a b races two acts so, in a custodian of their own
that the first to come to its value shuts down. A behaviour
runs to its end without waiting, so a with of a custodian in its body
is the body’s own, for its extent. The custodian is the one of the
scheduler’s world, so BOC.scheduled asks for custodian <: w, and a
main that runs it names custodian in its world. A behaviour spawned
in a contained custodian loses only that custodian when its file
fails to close:
import boc as BOCimport coimport custodian as Custodianimport exceptionimport std.file as Fileimport std.filesystemimport std.path
saved : {e w : HFunctor} -> {{bc : BOC.boc w <: e}} -> {{cu : Custodian.custodian <: e}} -> {{disk : filesystem <: w}} -> {{cw : Custodian.custodian <: w}} -> BOC.Cown w Int -> Path -> Co e Unitsaved c path = do with k := Custodian.contained do with n := c do r <- attempt FileError (do with f := File.open (access := Write { exists = Append }) path do File.writeString f (debug n ^ "\n")) pure ()What a custodian is given is higher-order. It is a computation of the
world it was given in, with the handlers between there and the
program’s top level already applied, so it runs after their scope may
have ended. An attempt there turns a throw into abandon, and the
shutdown says that in its list.
A custodian the program no longer reaches, and that has no living thing in its custody, is let go of by the runtime, and what it was given is dropped, not run. On the C backend that is at once. In a page it is whenever JavaScript’s collector comes to it.
with, of bindings
import boc as BOC
paid : {e w : HFunctor} -> {{bc : BOC.boc w <: e}} -> BOC.Cown w Int -> BOC.Cown w Int -> Int -> Co e Unitpaid from to n = do with a := from b := to do a := a - n b := b + nwith x := M do binds names, each to a value: one binding, or more, each
on a line of its own under the first, do at the end of the last. The
whole group is handed to the handler that owns what they are bound to,
which decides what the names are in the body, when and where the body
runs, and what happens at its end: here one behaviour on both cowns, a
and b what they hold, written back when it ends; with
std.file’s open path, a file opened, the body run with it,
and closed; with boc’s r := BOC.await act last, act run outside
every behaviour, and the body a behaviour once it has come to r; with
cml’s _ := CML.threads, the body cml’s main thread, in a world with
cml, it and the threads it spawns scheduled on domains, and the with
done once they are, no thread of it left running after it; with
std.domain’s s := Domain.scope, the body run here and every domain
spawned in s waited for at its end
(a scope of domains).
- The names are the body’s variables: they can be set,
a := a - n. The handler is told which ones the body sets, and is given what they are at its end beside the body’s value. _ := Mbinds no name.- The body is a block of its own: it sets no variable of the block
around, nor leaves it by
break,continueorreturn; areturnin it ends the body. - The world the body runs in is the binder’s to say. A file’s body is
in the block’s world. A cown’s body, or an await’s, is a behaviour,
run later on another domain: it is in the scheduler’s world,
BOC.boc w <+> w, and none of the handlers around thewith(atry, anotherwith’s resource) is around it. What it needs, it opens itself or is given as a value. cml’s threads are the same: aCML.spawntakes a computation ofCML.cml w <+> Custodian.custodian <+> w. - Its value is what the handler says: a file’s
withcomes to what its body came to, a cown’s to()(the behaviour runs later), cml’s threads’ toJustmain’s answer (Nothingwhere every thread waits).x <- with ... dobinds it. A thread or behaviour that failed does not stop the others: cml’swithfails at its end as the first thread that failed did. - A handler is a pair of instances, co’s
Binder(of what the names are bound to: what they are in the body, the world it runs in, and what thewithcomes to) andHolder(how it runs in the world), and thewithis co’sholding.
The body runs in the world of the block around, e, unless the
Binder says another, by its field World: a function from e to the
body’s world. What the names are, its field Contents, is given e
too, and the body’s region: a name can be of a type that says the
world around, as std.domain’s scope is. A handler that gives its body
a count of its own:
import state
data Counter = MkCounter Int
counter : Int -> Countercounter k = MkCounter k
instance instanceBinderCounter : Binder CounterinstanceBinderCounter = { Contents = \w s -> Int , World = \w -> state Int <+> w , Result = \r -> (r, Int) }
instance instanceHolderCounter : {e : HFunctor} -> Holder e Counter (\s -> Int) (\w -> state Int <+> w) (\r -> (r, Int))instanceHolderCounter = { held = \c written body -> case c of MkCounter k -> bind (runState k (body (s := Unit) k)) (\p -> pure (fst (fst p), snd p)) }
tens : {e : HFunctor} -> {{io : stdio <: e}} -> Co e Unittens = do r <- with n := counter 5 do put (n + 1) m <- get print ("counted " ^ debug m) put (m * 10) pure "done" print (fst r ^ " at " ^ debug (snd r))The body of with n := counter 5 do runs in state Int <+> e: n is
the count it starts from, and get and put are its own count’s. The
holder’s held is given the body as a computation of that world, and
handles what it has beyond e, here by runState; the with comes to
("done", 60). The body’s evidence is found in its world
(giving the evidence): an effect the
world adds is the body’s own, even where e has it too, and print
is e’s stdio, through the io in scope, with nothing written. A
world \w -> stdio <+> w makes a with whose holder sends what its
body prints elsewhere, to a file or a buffer.
with, where a condition holds
import boc as BOC
-- | `n` taken from the stock once it has `n`: until then, the behaviour-- | lets go of the cown and waits for another to write ittaken : {e w : HFunctor} -> {{bc : BOC.boc w <: e}} -> BOC.Cown w Int -> Int -> Co e Unittaken stock n = do with s := stock where s >= n do s := s - nwith x := M where p do is a with of bindings with a condition:
where p on a line of its own under the bindings, indented, and do
on the next, under with. What the names are bound to is taken, as
without it, and p is checked of them. Where p holds, the body runs.
Where it does not, everything is let go of, untouched, and taken again
later, p checked again, until it holds.
pis an expression of the names, aBool: no action in it, no(<- c). It may name other values of the block around, which do not change while it waits; only the names can.- The handler is told which names
preads, as it is told which the body sets: a change to one of those is what can makephold, so it is those it waits on. - How it waits, and what “later” is, is the handler’s. boc’s cowns:
the behaviour lets go of its cowns and waits until a behaviour writes
one its condition reads, then is queued again on all of them, behind
that one (the paper’s
whenever). A behaviour still waiting once every other is done is dropped, its body never run: nothing is left to write its cowns. One that waits may wait long: those queued before it again go first each time. - Only a handler that can let go and take again has the clause. Beside
its
Holder, it is an instance of co’sWaiter, whoseheldWhereis given whatheldis, which namespreads (aBooleach) andp. Awith ... whereof a handler without one, a file or a scope of domains, is a type error (E0341).
A scope of domains
import std.domain as Domain
-- | `n` rounds of `step`, fewer where the scope is cancelledrounds : {e w : HFunctor} -> {s : Type} -> {{dm : Domain.domain <: e}} -> Domain.Scope s w -> Int -> Co e Unit -> Co e Unitrounds sc 0 step = pure ()rounds sc n step = do stop <- Domain.cancelled sc if stop then pure () else do step rounds sc (n - 1) step
both : {e : HFunctor} -> {{dm : Domain.domain <: e}} -> Co e Unit -> Co e Unit -> Co e Unitboth one other = do with s := Domain.scope do _ <- Domain.spawn s (rounds s 1000 one) _ <- Domain.spawn s (rounds s 1000 other) pure ()with s := Domain.scope do is structured concurrency, as Rust’s
std::thread::scope: its body runs here, and Domain.spawn s c spawns
c on a domain of its own, one of the scope’s. At the body’s end the
with waits for every domain spawned in s; the body is free to do
other work before. Domain.join gives a domain’s answer.
- A domain is spawned in a scope, and only there: there is no
spawnof any world. The body runs in the block’s world with the scope’s own effect,Domain.scoped e <+> e, andDomain.spawn s cis that effect’s.cis a computation ofe, the world around thewith, which has noscoped: a domain cannot spawn in its own scope, so none is spawned after the body ends, and none outlives thewith. A domain that wants domains of its own opens a scope of its own. - A domain of the scope that fails fails the scope: once all of them
have ended, the
withfails as the first to fail did (aDomain.triedaround it catches that). Where the body fails, its domains are waited for too, and thewithfails as the body did. - Cancelling is cooperative. The scope carries a cancel mark, set by
Domain.cancel s, or when one of its domains fails, or its body does. A domain looks at it where it chooses,Domain.cancelled s, and winds up as it chooses: hereroundsstops between steps. Thewithstill waits for it, and a scope that is only cancelled comes to its body’s value. - No domain is stopped from outside. Stopped at an arbitrary operation,
it would leave a
Domain.lockit holds locked, and the release of a resource it holds (awith’s, aheld’s) would not run, its continuation dropped: that is why cancelling a thread of the system from outside is unsafe. A domain waiting in a call of the system, or in a loop with no operation, stops only where it looks at the mark; a pure loop that never ends is std.fuel’s to stop (fuel). scannot leave the body: its type,Domain.Scope s e, is of thewith’s region. A body left by a throw handled outside thewith(anexception’s) does not wait: its domains run on.
with and as are keywords: neither is a
name.