Sharing types between Tool modules
This guide is non-normative. It shows practice; it creates no semantics and
states no guarantee. Modules owns imports,
versions and cycles, types owns pub
and opaque, tools owns which types
decoding may build, and the runtime ABI
owns the decoding table.
Keep modules that share types in one repository
Tool modules often share types. A catalog returns a Document, a search Tool
returns lists of them, and an editor takes one back:
// github.com/acme/docs/document.flow
pub type Document { pub id: Text, pub title: Text, pub body: Text }
// github.com/acme/docs/catalog.flow
use "./document"
pub type CatalogError { NotFound(Text), Unavailable }
@tool pub flow read(id: Text) -> Result<document.Document, CatalogError>
// github.com/acme/docs/search.flow
use "./document"
pub type SearchError { BadQuery(Text), Unavailable }
@tool pub flow find(query: Text) -> Result<List<document.Document>, SearchError>Open in playground →Put such modules in one repository and import each other with relative paths. Within a repository, modules may import each other in cycles, and they are released together under one version (modules).
Splitting them across repositories brings two problems:
- Cycles across repositories are refused. If
cataloglives in one repository andsearchin another, and each needs a type from the other, no version of either could be released first. The program is refused. - A type is tied to its repository's version. A program uses one version of each repository (modules). Types shared across repositories make every consumer resolve both at compatible versions, and a major release of the one holding the types forces a release of the other.
When types must be shared across repositories, put them in a repository of their own that imports nothing from its users, and let every Tool repository import it. The dependency then runs one way.
Host types follow the same rule. A @tool type is nominal and belongs to the
module that declares it (tools), so
the Tools that make a Page and the Tools that take one back should live where
they can all name that one declaration.
pub and opaque for types that cross
A type that crosses a Tool boundary is data, and a type a reply builds must be
one decoding can build: no opaque part anywhere inside it
(tools). Visibility otherwise works
as usual:
- Mark fields
pubwhen other modules read them. A record with a private field can still be decoded and sent to a Tool; private fields hide how a value is built, not what crosses. Outside its module such a record can be read through its public fields but not built raw or rebuilt with..base. - A
pubenum exposes every constructor. Reply enums are usuallypub, since callers match on them. - Use
opaquefor values minted after a check, and only for those. An opaque type cannot come from any decoder, its own module's included, so a Tool reply orjson.decodecan never forge one.
// github.com/acme/review/review.flow
pub type Plan { pub steps: List<Text>, pub owner: Text }
pub opaque type Approved { Approved(Plan) }
pub flow approve(plan: Plan, verdict: Verdict) -> Option<Approved> = match verdict {
Accepted => Some(Approved(plan)),
Rejected(_) => None,
}Open in playground →A model or a person replies with a Plan or a Verdict, which decode. Only
approve makes an Approved, and a function that takes one knows the check
happened in this run. A checkpoint holding an Approved is rebuilt on resume
as the program made it; that is not decoding
(history).
Do not put an opaque type inside a reply type. The declaration is refused, because the reply could never decode. Return the plain value and convert it through the owning module's function.
Decoding rules in practice
Decoding is strict, directed by the declared type, and never repairs a value (abi). Some habits follow:
- Make a field
Optionif a provider may omit it. A missing field fails decoding unless its type isOption, andnullis accepted only there. - Extra fields in a reply are ignored. A provider can add fields without breaking older programs. A run's input is stricter: an undeclared field refuses the run.
- New constructors are not ignored. An unknown constructor name is refused,
so a reply using a constructor an older program does not declare comes back
as
BadReply. Add constructors to a reply enum as a deliberate, version-raising change, and handleBadReplywhere it matters. - Numbers are read exactly. An
Intfield needs a whole value, so3.5fails to decode as one. UseDecimalfor amounts that may carry fractions. - Nothing is coerced. The text
"42"is not anInt, and no default is filled in. Ask the provider for the declared form rather than widening the type. - A decoded value is only well-formed. Decoding proves Flow can interpret
the value, not that it is true. Check what matters before acting on it, and
record the check in an
opaquetype if later code must rely on it.
When the shape is genuinely unknown, decode into json.Value or keep the text,
and decode the parts you need later with json.decode at a declared type
(stdlib).