Lesson 07 — Optional, required and sealed
Goal: by the end of this lesson you can tell, on any card, which inputs a fn can run without, which it insists on, and which nobody below it may touch — and you can set each of those on your own fns.
Concepts introduced: optional slot (:required false), the
required ratchet, final value, sealed slot (:terminal), the
lock badge 🔒 and its popover.
Three decisions that live beside a value
A slot carries more than what is bound to it. Three author decisions ride along, and each shows on the card:
| Decision | Where it is declared | On the card |
|---|---|---|
| Optional — the fn runs without it | :required false on the slot's declaration |
on a fn you own, the + is dimmed and the faint 🔓 says Optional |
| Required here — a descendant insists on an inherited optional | :required true on that fn's binding |
the + is solid again; the badge says required since … |
| Sealed — no descendant may bind this slot | :terminal true on a binding |
a 🔒 on the label; descendants get a lock where the + would be |
And one that needs no flag at all: a value, once set, is final.
Optional
A base function's argument declared {:type :int :required false}
may be left unbound: the implementation has a fallback. subs is one
— end defaults to the string's length:
{:name :subs
:args {:string {:type :text}
:start {:type :int}
:end {:type :int :required false}}}
On a card of your own the free end is drawn like any free slot, but
its + is dimmed, and the small lock after its type chip — 🔓,
faint until you hover the label or focus the button — reads
Optional — the fn runs without it. (A package fn's card shows neither
+ nor badge for a free slot — it is not yours to bind; extend it first.) The
Run form (lesson 15) lists it the same way. Bind it or don't; either
runs.
Optional is a property of the slot's declaration, so it is the
declaring fn's to change: on a slot you own (a renamed view from
{:as …}, lesson 05, or a field of your own type row) the popover
offers Optional as a checkbox. On a slot a base fn declares it is
just a fact you read.
Required — a one-way ratchet
Your fn may need what its parent could live without. Tick Require
it here and every fn from yours downwards must supply end: the
+ is solid, and a run without it fails with
:execution-error/missing-required-arg. In fns.edn:
{:name :cut-to
:parent :subs
:args {:end {:required true}}}
It only goes one way. Once an ancestor made a slot required, no descendant can make it optional again — the popover shows already required above and leaves the box disabled. Loosening would break every caller that trusted the tighter contract; tightening breaks nobody above you.
Final values
Bind string to "graphden" on your fn and extend that fn: on the
child's card string is not there. Not hidden — final. A value an
ancestor set is the ancestor's decision; the editor offers no + for
it and the API refuses a binding on it (value-override). Want a
different string? Extend a different fn, or the base. This is the LEGO
rule from lesson 03: arguments are not overridden, a different
behaviour is a different fn-def — which is also why the standard
library composes by parents (json-ok-response is
json-content-type + ok-response) rather than by re-binding.
Lists are the exception that isn't: an inherited list stays open for appending unless it is closed (lesson 06), because appending extends the ancestor's decision instead of replacing it.
Sealed — final before the value
A template slot — left free on purpose so callers fill it — can
still be one you do not want descendants to fill: the fn that
extends yours should pass it on, not decide it. Click the 🔓, tick
Seal against descendants, Save. The badge is 🔒; on every
child's card the slot shows a lock instead of a +, with the reason
on hover (Sealed in cut — descendants cannot bind this slot), and
the server refuses the write behind it (terminal-seal).
A seal is the explicit form of what a value does implicitly, and it
is yours to lift — untick it where you set it. The sealing fn may
still bind its own slot; a descendant can neither bind nor unseal it.
In fns.edn:
{:name :cut
:parent :subs
:args {:end {:terminal true}}}
Reading the badge
One glyph, three facts, everywhere a slot is drawn:
🔓, faint — nothing decided, or only Optional (the slot's own declaration); on your own fn, click to decide.🔒— something is in force. Hover: Sealed here or Sealed in cut; List closed in …; required since …. The popover repeats the inherited ones read-only and lets you change the ones that are yours.- A lock where a
+should be — sealed, or a closed list, above you. Hover it for who, and where to lift it.
Try it
Prefer to be shown? This lesson exists as a guided in-editor tour: open the demo with the tour running (no sign-up), or pick “Interactive tutorial” in the editor's account menu.
Run a fn without its optional input, seal a slot, then require it:
- Type
subsin the Explorer filter and click thesubsrow (core.strings). Three free slots;endis declared optional — you will see it on your own child next. ⋯→ Extend →tutorial-cut→ Save.- Click the faint
🔓besideendand inspect the restrictions. Require it here makes this input mandatory; Seal against descendants prevents children from binding it. Leave both unchecked and click Cancel before binding the other inputs. +onstring→ Bind literal →graphden→ Save;+onstart→5→ Save. Leaveendalone.⋯→ ▶ Run → Run:"den"—enddefaulted to the end of the string.- Click the
🔓afterend, tick Seal against descendants, Save. The badge closes. ⋯→ Extend →tutorial-cut-more→ Save. On the child,stringandstartare gone — final — andendshows a lock where its+would be. Hover it.- On the
tutorial-cutancestor row:⋯→ Inheritance… → Go to source. This opens the exact source function. Open its🔒: untick Seal, tick Require it here, Save. - Open
tutorial-cut-moreonce more: the+onendis back, and no longer dimmed — fromtutorial-cutdown,endmust be supplied. - On the
tutorial-cutancestor row:⋯→ Inheritance… → Go to source. Sealendonce more and leave requiredness unchanged. Return totutorial-cut-more: its inheritedendis locked. - On its
tutorial-cutancestor row:⋯→ Inheritance…. Set Variation name to_tutorial-cut-local, then click Create variation for tutorial-cut-more. The new source opens. It is a sibling oftutorial-cut; the child now inherits from it and keeps its own composition. - On
_tutorial-cut-local, untick only Seal against descendants forendand save. Keep requiredness unchanged. Return totutorial-cut-more: its+is back, and the input remains required. Opentutorial-cutseparately: the original seal is still closed. - On the child's
_tutorial-cut-localancestor row, open Inheritance… → Choose ancestor…. These are recommended ancestors checked by the server, rather than a complete catalogue of compatible functions. Cancel to keep the current parent. Existing parent editing remains available for other functions and multiple parents.
A variation changes tutorial-cut-more for all its consumers in this
branch. It preserves tutorial-cut and its other children. A copied own
seal can be lifted on the new source; restrictions inherited above that
source remain closed. Shared slot declarations retain their identities,
so creating a variation does not give them independent requiredness.
For one explicit own reference binding, the same menu can create a sibling and replace that binding atomically. This changes the binding's owner and all consumers of that owner. Inherited bindings and list items are outside this operation; unavailable actions show the server's reason.
What we glossed over
- The Run form — how optional and required inputs are asked for at run time: lesson 15.
- Closed lists — the list slot's own seal: lesson 06.
- Descriptions on slots — the
ibeside the lock, lesson 02. - Types as constraints — narrowing a slot's type in a descendant is the other ratchet; lesson 08.