package octez-proto-libs
Fundamentals
Promises
type 'a t = 'a Lwt.t
Promises for values of type 'a
.
A promise is a memory cell that is always in one of three states:
- fulfilled, and containing one value of type
'a
, - rejected, and containing one exception, or
- pending, in which case it may become fulfilled or rejected later.
A resolved promise is one that is either fulfilled or rejected, i.e. not pending. Once a promise is resolved, its content cannot change. So, promises are write-once references. The only possible state changes are (1) from pending to fulfilled and (2) from pending to rejected.
Promises are typically “read” by attaching callbacks to them. The most basic functions for that are Lwt.bind
, which attaches a callback that is called when a promise becomes fulfilled, and Lwt.catch
, for rejection.
Promise variables of this type, 'a Lwt.t
, are actually read-only in Lwt. Separate resolvers of type 'a
Lwt.u
are used to write to them. Promises and their resolvers are created together by calling Lwt.wait
. There is one exception to this: most promises can be canceled by calling Lwt.cancel
, without going through a resolver.
We omit u
, wait
, wakeup*
and so on because these are only useful to define new synchronization primitives which the protocol doesn't need: it gets its synchronization primitives from the environment.
val return : 'a -> 'a t
Lwt.return v
creates a new promise that is already fulfilled with value v
.
This is needed to satisfy the type system in some cases. For example, in a match
expression where one case evaluates to a promise, the other cases have to evaluate to promises as well:
match need_input with
| true -> Lwt_io.(read_line stdin) (* Has type string Lwt.t... *)
| false -> Lwt.return "" (* ...so wrap empty string in a promise. *)
Another typical usage is in let%lwt
. The expression after the “in
” has to evaluate to a promise. So, if you compute an ordinary value instead, you have to wrap it:
let%lwt line = Lwt_io.(read_line stdin) in
Lwt.return (line ^ ".")
We omit fail
as well as catch
and such because we discourage the use of exceptions in the environment. The Error Monad provides sufficient primitives.
Callbacks
Lwt.bind p_1 f
makes it so that f
will run when p_1
is fulfilled.
When p_1
is fulfilled with value v_1
, the callback f
is called with that same value v_1
. Eventually, after perhaps starting some I/O or other computation, f
returns promise p_2
.
Lwt.bind
itself returns immediately. It only attaches the callback f
to p_1
– it does not wait for p_2
. What Lwt.bind
returns is yet a third promise, p_3
. Roughly speaking, fulfillment of p_3
represents both p_1
and p_2
becoming fulfilled, one after the other.
A minimal example of this is an echo program:
let () =
let p_3 =
Lwt.bind
Lwt_io.(read_line stdin)
(fun line -> Lwt_io.printl line)
in
Lwt_main.run p_3
(* ocamlfind opt -linkpkg -thread -package lwt.unix code.ml && ./a.out *)
Rejection of p_1
and p_2
, and raising an exception in f
, are all forwarded to rejection of p_3
.
Precise behavior
Lwt.bind
returns a promise p_3
immediately. p_3
starts out pending, and is resolved as follows:
- The first condition to wait for is that
p_1
becomes resolved. It does not matter whetherp_1
is already resolved whenLwt.bind
is called, or becomes resolved later – the rest of the behavior is the same. - If and when
p_1
becomes resolved, it will, by definition, be either fulfilled or rejected. - If
p_1
is rejected,p_3
is rejected with the same exception. - If
p_1
is fulfilled, with valuev
,f
is applied tov
. f
may finish by returning the promisep_2
, or raising an exception.- If
f
raises an exception,p_3
is rejected with that exception. - Finally, the remaining case is when
f
returnsp_2
. From that point on,p_3
is effectively made into a reference top_2
. This means they have the same state, undergo the same state changes, and performing any operation on one is equivalent to performing it on the other.
Syntactic sugar
Lwt.bind
is almost never written directly, because sequences of Lwt.bind
result in growing indentation and many parentheses:
let () =
Lwt_main.run begin
Lwt.bind Lwt_io.(read_line stdin) (fun line ->
Lwt.bind (Lwt_unix.sleep 1.) (fun () ->
Lwt_io.printf "One second ago, you entered %s\n" line))
end
(* ocamlfind opt -linkpkg -thread -package lwt.unix code.ml && ./a.out *)
The recommended way to write Lwt.bind
is using the let%lwt
syntactic sugar:
let () =
Lwt_main.run begin
let%lwt line = Lwt_io.(read_line stdin) in
let%lwt () = Lwt_unix.sleep 1. in
Lwt_io.printf "One second ago, you entered %s\n" line
end
(* ocamlfind opt -linkpkg -thread -package lwt_ppx,lwt.unix code.ml && ./a.out *)
This uses the Lwt PPX (preprocessor). Note that we had to add package lwt_ppx
to the command line for building this program. We will do that throughout this manual.
Another way to write Lwt.bind
, that you may encounter while reading code, is with the >>=
operator:
open Lwt.Infix
let () =
Lwt_main.run begin
Lwt_io.(read_line stdin) >>= fun line ->
Lwt_unix.sleep 1. >>= fun () ->
Lwt_io.printf "One second ago, you entered %s\n" line
end
(* ocamlfind opt -linkpkg -thread -package lwt.unix code.ml && ./a.out *)
The >>=
operator comes from the module Lwt.Infix
, which is why we opened it at the beginning of the program.
See also Lwt.map
.
We omit dont_wait
and other such functions because they are only useful in mutation-heavy loosely-synchronised code which the protocol shouldn't be.
We omit many synchronisation primitives such as choose
because they introduce non-determinism.
We omit cancelation-related primitives because we discourage Cancelation in the protocol.
Convenience
Callback helpers
Lwt.map f p_1
is similar to Lwt.bind
p_1 f
, but f
is not expected to return a promise.
This function is more convenient that Lwt.bind
when f
inherently does not return a promise. An example is Stdlib.int_of_string
:
let read_int : unit -> int Lwt.t = fun () ->
Lwt.map
int_of_string
Lwt_io.(read_line stdin)
let () =
Lwt_main.run begin
let%lwt number = read_int () in
Lwt_io.printf "%i\n" number
end
(* ocamlfind opt -linkpkg -thread -package lwt_ppx,lwt.unix code.ml && ./a.out *)
By comparison, the Lwt.bind
version is more awkward:
let read_int : unit -> int Lwt.t = fun () ->
Lwt.bind
Lwt_io.(read_line stdin)
(fun line -> Lwt.return (int_of_string line))
As with Lwt.bind
, sequences of calls to Lwt.map
result in excessive indentation and parentheses. The recommended syntactic sugar for avoiding this is the >|=
operator, which comes from module Lwt.Infix
:
open Lwt.Infix
let read_int : unit -> int Lwt.t = fun () ->
Lwt_io.(read_line stdin) >|= int_of_string
The detailed operation follows. For consistency with the promises in Lwt.bind
, the two promises involved are named p_1
and p_3
:
p_1
is the promise passed toLwt.map
.p_3
is the promise returned byLwt.map
.
Lwt.map
returns a promise p_3
. p_3
starts out pending. It is resolved as follows:
p_1
may be, or become, resolved. In that case, by definition, it will become fulfilled or rejected. Fulfillment is the interesting case, but the behavior on rejection is simpler, so we focus on rejection first.- When
p_1
becomes rejected,p_3
is rejected with the same exception. - When
p_1
instead becomes fulfilled, call the value it is fulfilled withv
. f v
is applied. If this finishes, it may either return another value, or raise an exception.- If
f v
returns another valuev'
,p_3
is fulfilled withv'
. - If
f v
raises exceptionexn
,p_3
is rejected withexn
.
We omit explicit callback registration (on_termination
and such) because it is only useful for mutation-heavy code
We omit syntax helpers because they are available through the dedicated syntax modules of the Error Monad.
Pre-allocated promises
val return_unit : unit t
Lwt.return_unit
is defined as Lwt.return
()
, but this definition is evaluated only once, during initialization of module Lwt
, at the beginning of your program.
This means the promise is allocated only once. By contrast, each time Lwt.return
()
is evaluated, it allocates a new promise.
It is recommended to use Lwt.return_unit
only where you know the allocations caused by an instance of Lwt.return
()
are a performance bottleneck. Generally, the cost of I/O tends to dominate the cost of Lwt.return
()
anyway.
In future Lwt, we hope to perform this optimization, of using a single, pre-allocated promise, automatically, wherever Lwt.return
()
is written.
val return_none : _ option t
Lwt.return_none
is like Lwt.return_unit
, but for Lwt.return
None
.
val return_nil : _ list t
Lwt.return_nil
is like Lwt.return_unit
, but for Lwt.return
[]
.
val return_true : bool t
Lwt.return_true
is like Lwt.return_unit
, but for Lwt.return
true
.
val return_false : bool t
Lwt.return_false
is like Lwt.return_unit
, but for Lwt.return
false
.
We omit state introspection because it is discouraged when not defining new synchronisation primitives which the protocol doesn't do.
val return_some : 'a -> 'a option t
Counterpart to Lwt.return_none
. However, unlike Lwt.return_none
, this function performs no optimization. This is because it takes an argument, so it cannot be evaluated at initialization time, at which time the argument is not yet available.
val return_ok : 'a -> ('a, _) Pervasives.result t
Like Lwt.return_some
, this function performs no optimization.
val return_error : 'e -> (_, 'e) Pervasives.result t
Like Lwt.return_some
, this function performs no optimization.