Nothing here names a widget, a tag or a terminal. A vocabulary (packages/tea-term,
packages/tea-dom) implements Tree for its node type and gets the rest:
use tea_core/tree.{Tree, Rel, Unrelated, Same, SelfDiffers}
use tea_core/diff.{diff, apply}
impl[M] Tree[Widget[M]] {
fn kids(w: Widget[M]) -> List[Widget[M]] = ...
fn rekid(w: Widget[M], ks: List[Widget[M]]) -> Widget[M] = ...
fn relate(old: Widget[M], new: Widget[M]) -> Rel = ...
}
The impl lives in the module that declares the vocabulary (the orphan rule is per module), and one impl covers every message type.
The Tree contract#
kids(w)is a node's children in order; a patch'spathindexes into it.rekid(w, ks)rebuilds a node around new children and must be total.relate(old, new)is asked only about a pair already found unequal by==(anEqbound on the reconciler):Sameto diff the children in place,SelfDifferswhen the node's own data changed too,Unrelatedto replace the subtree. It must account for every difference between the pair.key(w) -> Option[String]names a node among its siblings. It defaults toNone.
Diffing#
diff(old, new) is a list of patches and apply(old, patches) replays it:
apply(old, diff(old, new)) == new, and equal trees diff to []. A patch is
an address (path) and an op: Replace, SetSelf (a whole node whose own
data replaces the target's, children kept), AppendKids, TruncateKids,
and for keyed lists InsertKid, RemoveKid, MoveKid. An unchanged sibling
is never mentioned, and patches apply in emission order.
Children pair by key when every child has one and no two share it, so a deletion in the middle is one op and a moved child is moved, not rebuilt. Otherwise they pair by index, and a middle deletion rewrites the tail.
Walking#
fold_preorder(w, init, f) visits parents before children, left to right,
passing f the accumulator, the node and its address. Event routing in a
vocabulary is written on it.
trait App#
trait App[M] has type Msg, type View, effect E, update and view.
update answers (M, Cmd[M.Msg]) and its row is !M.E, which defaults to
!(): an impl that says nothing has a pure update, and one that needs io
writes effect E = !io. A generic driver can do nothing with an A.View, so
drivers take the view function as a parameter.
Commands#
A Cmd is data: NoCmd, SendMsg(msg) (one more message in this same turn),
BatchCmd(cmds) and Fetch(url, tag). cmd.fold_msg(m, msg, update) runs a
turn: the message, then every commanded message first in, first out. More than
CMD_FOLD_LIMIT commanded messages or fetches in one turn panics rather than
truncating.
Fetch only describes a request: the host does the getting and answers in a
later turn, with the same tag, as Ok(body) or Err(reason) (a network
failure or a non-2xx status is an Err, not a crash). A driver that can fetch
calls cmd.fold_cmds, which answers the model and the requested FetchReqs;
fold_msg has no host to ask and panics on a turn that commanded one.
Subscriptions#
Sub[M] is data too: Tick(every_ms, msg) names the message a timer means.
The model declares its subscriptions (subs(m)), the driver owns all timing
and re-reads the declaration after every update, and a due timer's message is
taken from the declaration current when it fires. elapse is handed the
milliseconds the driver measured; a timer that missed whole periods fires once
and keeps its phase.
Changes between versions: CHANGELOG.md. Design background: `docs/dom-bridge-design.md` (in Chinese).
API#
tea_core/app
App
trait App[M] {
type
type
fn update[M: App](m: M, msg: M.Msg) -> (M, Cmd[M.Msg]) !M.E
fn view[M: App](m: M) -> M.View
}
An application: update folds one message into the model and says what
else the turn asks for, view describes the whole screen for a model. Both
pure, so both are testable with ==.
tea_core/cmd
本模块登记的 impl(全程序可见,不必 use):Show[Cmd[M]]、Show[FetchReq]
Cmd
type Cmd[M] = NoCmd | SendMsg(msg: M) | BatchCmd(cmds: List[Cmd[M]]) | Fetch(url: String, tag: String)
What a turn asks for besides a new model.
NoCmd is nothing, SendMsg is one more message for this turn, and
BatchCmd is several commands in order. These are Elm's Cmd.none,
Cmd.msg and Cmd.batch; the names differ because None is already
Option's and a constructor that shadows it in every importing module is
not worth the resemblance.
Fetch(url, tag) asks the host to GET url and to answer with a supply
carrying tag, which the application's own function turns into a message
for a later turn. The guest only describes the request; it is not run
here and nothing about its answer is known in the turn that asked. tag is
the application's name for the request (the same url may be asked for twice
for different reasons) and is returned verbatim.
FetchReq
type FetchReq = { url: String, tag: String }
One fetch a turn asked for, as the host receives it.
cmd.CMD_FOLD_LIMIT
const CMD_FOLD_LIMIT: Int
How many commanded messages one turn may fold before it is judged a loop.
The bound is on the total messages a turn's commands add, not on how deeply they nest: a batch that fans out is the same runaway as a message that re-issues itself, and counting rounds would leave the first one unbounded. Sixty-four is far past any hand-written chain (the one client in tree issues a chain of length one) and far short of a wait a user would sit through.
cmd.msgs
fn msgs[M](c: Cmd[M]) -> List[M]
The messages a command asks for, left to right, batches flattened.
cmd.fetches
fn fetches[M](c: Cmd[M]) -> List[FetchReq]
The fetches a command asks for, left to right, batches flattened.
cmd.fold_msg
fn fold_msg[Mo, M, !e](m: Mo, msg: M, step: fn(Mo, M) -> (Mo, Cmd[M]) !e) -> Mo !e
Fold one message into the model, then everything its commands asked for, and answer the model the turn ends with.
step is the application's update; a driver passes its own so that the
fold is written once for every vocabulary rather than once per driver.
A driver with no host to fetch for uses this one, and a turn that commanded
a Fetch panics here (see the header).
cmd.fold_msgs
fn fold_msgs[Mo, M, !e](m: Mo, queue: List[M], step: fn(Mo, M) -> (Mo, Cmd[M]) !e) -> Mo !e
fold_msg for messages that arrive together, such as the timers a poll
found due. The list is not itself commanded, so it does not count against
CMD_FOLD_LIMIT; what the turn commands does.
cmd.fold_cmds
fn fold_cmds[Mo, M, !e](m: Mo, msg: M, step: fn(Mo, M) -> (Mo, Cmd[M]) !e) -> (Mo, List[FetchReq]) !e
fold_msg for a driver that can fetch: the model the turn ends with, and
every fetch its commands asked for, in the order they were asked.
tea_core/diff
本模块登记的 impl(全程序可见,不必 use):Show[Op[W]]
Op
type Op[W] = Replace(w: W) | SetSelf(w: W) | AppendKids(ws: List[W]) | TruncateKids(keep: Int) | InsertKid(at: Int, w: W) | RemoveKid(at: Int) | MoveKid(from: Int, to: Int)
One edit operation. Replace swaps a whole subtree, SetSelf takes a
node's own data and keeps the children already in place, the two tail ops
grow or shrink a node's child list, and the three keyed ops edit it in the
middle.
SetSelf carries a whole node where an attribute-carrying op would carry
attributes, because core has no way to name a vocabulary's attribute type
(tree.dawn, wall one). The payload is not a copy -- it is the node diff
was handed -- and it cannot be normalised to "the node without its
children" either, since a one-child wrapper has no spelling for "no child".
MoveKid is the one op a host must not implement as a removal and a
rebuild. Its whole value is that the node the host already has is the node
that ends up at to.
Patch
type Patch[W] = { path: List[Int], op: Op[W] }
One edit at one address. path is the chain of child indices from the root,
where a child index is an index into kids.
diff.diff
fn diff[W: Tree + Eq](old: W, new: W) -> List[Patch[W]]
Every difference between old and new, addressed from the root. Equal
trees produce the empty list.
diff.apply
fn apply[W: Tree](w: W, ps: List[Patch[W]]) -> W
Replay patches in order. An address that does not exist in w leaves the
node unchanged: apply stays total, and the roundtrip tests are what keep
diff from ever emitting such an address.
tea_core/sub
本模块登记的 impl(全程序可见,不必 use):Show[Sub[M]]
Sub
type Sub[M] = Tick(every_ms: Int, msg: M)
What a model can ask the driver for: a message every every_ms
milliseconds, as the driver counts them (tea_term counts monotonic
time). Intervals below 1ms behave as 1ms (see intervals).
Pending
type Pending = { every_ms: Int, waited_ms: Int }
One armed timer: its interval and how much waiting it has accumulated.
sub.intervals
fn intervals[M](subs: List[Sub[M]]) -> List[Int]
The interval of each subscription, clamped to at least 1ms so a zero or negative interval cannot spin the poll loop.
sub.poll_ms
fn poll_ms(ps: List[Pending]) -> Option[Int]
How long the driver may block before the nearest timer comes due, or
None when there are no timers and it may block on input forever.
sub.elapse
fn elapse(ps: List[Pending], ms: Int) -> (List[Pending], List[Int])
Account ms of elapsed time to every timer. Returns the advanced timers
and the indices (declaration order) of those that came due. A timer fires
once per call and keeps what it waited past its last boundary, so a late
poll drifts rather than bursts: periods missed whole are skipped, not
owed.
sub.due_msgs
fn due_msgs[M](subs: List[Sub[M]], due: List[Int]) -> List[M]
The messages the due indices mean, read from the declaration current at fire time. An index the declaration no longer covers is dropped.
tea_core/tree
本模块登记的 impl(全程序可见,不必 use):Show[Rel]
Rel
type Rel = Unrelated | Same | SelfDiffers
How a vocabulary says two nodes at the same address are related. Closed, and about semantics rather than hosts: the reconciler asks only "may I diff these in place, and did the node's own data change".
Tree
trait Tree[W] {
fn kids[W: Tree](w: W) -> List[W]
fn rekid[W: Tree](w: W, ks: List[W]) -> W
fn relate[W: Tree](old: W, new: W) -> Rel
fn key[W: Tree](w: W) -> Option[String]
}
The contract. Four members: walk children, decide a pair's fate, rebuild a
node around new children, and name a node among its siblings. Only the first
three are obligations -- key has a default, so a vocabulary that has no
notion of identity says nothing and gets the pairing it had before the
member existed.
fn kids[W: Tree](w: W) -> List[W]-
This node's children in order; a leaf has none.
diffpairs these by index andapplyaddresses through them, so an index in aPatch.pathis an index into exactly this list. fn rekid[W: Tree](w: W, ks: List[W]) -> W-
wwith its children replaced. Must be total: a leaf handed children is expected to ignore them and return itself, which is what keepsapplytotal against an address that does not exist. fn relate[W: Tree](old: W, new: W) -> Rel-
Whether the pair may be diffed in place, and whether the node's own data (everything that is not a child) changed. Only ever called on a pair the reconciler has already found unequal, so a vocabulary never has to re-compare fields the
==shortcut already settled. fn key[W: Tree](w: W) -> Option[String]-
This node's identity among its siblings, or
Nonewhen it has none.diffpairs a child list by key when every child answers one and no two answer the same, and by index otherwise, so this member is what turns a deletion in the middle of a list into a deletion rather than a rewrite of everything after it.Stringand not a vocabulary-chosen type: wall one above rules out an unbounded projection, so the identity has to be built from a type core already knows. The default body is what keeps the other three members the whole of what an unkeyed vocabulary owes.
tea_core/walk
walk.fold_preorder
fn fold_preorder[W: Tree, A](w: W, init: A, f: fn(A, W, List[Int]) -> A) -> A
Pre-order fold with the path in hand: f sees the accumulator, the node,
and the node's address from the root. The root's address is the empty path.