GoForge
Worker types
Classify bricks by determinism and effectfulness — and let check enforce the rules.
A worker is classified along two independent (orthogonal) axes. A worker type is the product of the two: one determinism value and one effectfulness value.
The two axes
Determinism
Whether the same inputs and world always yield the same result.
deterministic— always yields the same outputs and side-effects, given the same inputs and world.non_deterministic— does not always yield the same outputs and side-effects, given the same inputs and world.
Effectfulness
Either pure, or a set of one or more effects. An effect is any interaction
with the world beyond the worker’s declared inputs and outputs.
pure— result depends only on declared inputs and produces no side-effects. It neither reads from nor writes to the world. (Referentially transparent.)input_effect— reads from the world (a database, stdin, the clock, environment, network). Output may depend on world state, not only declared inputs.output_effect— writes to / mutates the world (a database write, stdout, sending a request).
The eight worker types
effectfulness is pure or a non-empty set of effects, giving 4 values; times
2 determinism values = 8 worker types.
| determinism | effectfulness |
|---|---|
| deterministic | pure |
| deterministic | [input_effect] |
| deterministic | [output_effect] |
| deterministic | [input_effect, output_effect] |
| non_deterministic | pure |
| non_deterministic | [input_effect] |
| non_deterministic | [output_effect] |
| non_deterministic | [input_effect, output_effect] |
Enforcement
goforge check enforces effect propagation across the dependency closure:
- a
purecomponent may not depend on an effectful brick; - a
deterministicbrick may not depend on anon_deterministicone.
goforge classify shows each brick’s declared vs. effective worker type —
the effective type being what you get after propagating through everything the
brick depends on.
goforge classify
goforge check
Inference
Since v1.1, declarations are also verified against the code itself. check and
classify infer each brick’s worker type by resolving every call all the
way down the stack until it bottoms out in a primitive whose classification is
known:
- filesystem / network / process I/O —
os,net,net/http,database/sql,os/exec,syscall, … - clock reads —
time.Now,time.Since(a deterministicinput_effect: the clock is part of the world); - entropy and scheduling —
math/rand,crypto/rand, goroutines, multi-wayselect(non-deterministic); - shared mutable state — reads and writes of package-level variables that
are mutated outside
init. Effectful means: operates on shared, mutable state, including the outside world.
Since v1.2, the analysis does not stop at the workspace boundary:
- third-party pure-Go dependencies are analyzed from source, exactly like
workspace code — the conclusion for
extlib.Fetchcomes from whatextlib.Fetchactually does, not from an assumption; - dynamic calls are resolved: calls through function values and interface method dispatch are mapped to their possible concrete targets with a whole-program VTA (variable type analysis) call graph.
Only the genuinely opaque remains assumed-worst: C code behind cgo, assembly
bodies, reflect.Value.Call, and dynamic calls with no discoverable targets.
Those widen the brick’s possible type (shown as +? in classify) and
surface as warnings, not errors. A declaration claiming effects nothing in the
code could justify earns a “consider tightening” warning.
A brick that declares pure but provably reads the clock — directly or five
frames deep inside a dependency — fails check with an inference error
naming the offending line.
goforge classify # declared | inferred | effective
goforge classify explain:user # every finding, with file:line
goforge classify explain:user :deep # follow the chain into dependency source,
# down to the primitives
goforge check :no-infer # skip inference (it requires the code to type-check)
explain :deep walks the retained per-function conclusions, so the answer to
“why is this brick effectful?” reads like a stack trace:
vendor
inferred: deterministic/[input_effect]
components/vendor/vendor.go:5: vendor.Env
calls example.com/extlib.Fetch (inferred deterministic/[input_effect])
extlib/lib.go:5: example.com/extlib.Fetch
calls example.com/extlib.helper (inferred deterministic/[input_effect])
extlib/lib.go:6: example.com/extlib.helper
calls os.Getenv (deterministic/[input_effect])