Decorators
The @ surface from end to end: the pragma that enables it, how the
reader lowers @ lines, how the forms expand, and how to add decorator
shapes of your own.
A decorator is a leading @ line over a form:
@demand
def greet [name]:
f"hi {name}"
The default behaviour is Python’s: each decorator expression is
evaluated and applied to the definition, outermost first, so @d1 over
@d2 over foo yields (d1 (d2 foo)).
Koskari adds one thing. Certain binding forms, written body-less in decorator position, wrap the definition syntactically instead of being called at runtime, which turns the decorator prefix into a composing block:
@let [[count 0]]
def tick []:
incr! count
count
tick is a module-level function closed over a private count: the
same program as a let block containing the definition, without the
indentation and without global to push the name back out.
The pragma
The @ surface is gated by the reader/decorators pragma, on by
default. A module can turn it off:
(pragma [reader/decorators False])
after which a leading @ has no layout meaning and reads through the
ordinary atom rules. A reader built directly by tooling, rather than
through a module pragma, starts with the surface off like the other
pragma-gated reader features; readers built through
LayoutReader.from_pragma, which is the import path, the REPL, and the
lexer, get the default.
The pragma gates the reader surface only. The s-expression spelling
(decorate ...) is a macro and stays available. See
Pragmas and feature settings.
Reading
In layout mode the reader collects consecutive @ lines at one column,
reads the single layout form beneath them, and wraps the lot:
@(let [[c 0]])
@demand
def tick []: ...
;; reads as
(decorate ((let [[c 0]]) demand) (def tick [] ...))
Two conveniences follow from the layout rules. Parentheses are optional
on the decorator line, since several forms on a layout line are one
call, so @let [[c 0]] reads as @(let [[c 0]]). And the subject is
any layout form: a def, a class, a begin: block, an
if, a bare s-expression.
The reader builds the compile/decorate form and nothing more. What a
decorator means is decided by macro expansion, which is why the
s-expression spelling works with the pragma off.
The decorate fold
compile/decorate does two
jobs.
Name hoisting: when the subject is a def, var, class,
global def or global class, or the Scheme-layer define or define-global,
the name moves outside the decoration so the decorated value is what
gets bound:
(decorate [d1] (def foo [x] x))
;; expands to
(var foo (decorate [d1] (function foo [x] x)))
The fold: with a value subject, decorate peels one decorator per
expansion step into
compile/decorate-subject,
outermost first:
(decorate [d1 d2] fn)
;; expands to
(decorate-subject d1 (decorate [d2] fn))
decorate-subject is where one decorator meets its subject, and it is
the extension point.
Applying one decorator
decorate-subject dispatches on the shape of the decorator expression.
The head of the decorator says when its effect happens:
Decorator |
When the effect happens |
|---|---|
|
at decoration: evaluated and called on the subject |
|
at definition: lexical capture around the subject |
|
now: the extent spans the subject’s evaluation |
|
now: the extent spans the subject’s evaluation |
|
per call |
|
per call |
|
at definition: the function becomes the module’s entry point |
The default arm covers every ordinary runtime decorator, @demand,
@(memoize maxsize: None), @abstractmethod: the expression evaluates
and its value is called with the subject’s value.
@entry-point is a marking arm: it binds the function as the module’s
__entry__, the function the launcher calls when the module runs as a
program, and delivers it unchanged, so the def binds the same
function. See entry-point.
The graft arms match a body-less let, with, or parameterize and
complete it by splicing the subject in as its body:
(decorate-subject (let [[c 0]]) subject)
;; expands to
(let [[c 0]] subject)
Only the body-less spelling is special. A complete form with a body misses the arm and falls to the default, where it evaluates as an expression, so every decorator expression that already worked keeps its meaning, and the graft arms claim only spellings that mean nothing on their own.
Grafting fits let over a definition, since a closure is definition-time
capture, and any extent form over a plain form that runs once, now:
@parameterize [[verbose True]]
begin:
...
It does not fit with or parameterize directly over a def: the
extent would span the function’s creation, which no call observes. A
per-call extent uses the call- spelling.
The call arms wrap the subject in a forwarding function: enter the
extent, call the original with the passed arguments, close the extent,
deliver the value. The binding group sits inside the wrapper, so the
context-manager expression or the parameter values are re-evaluated on
every call, a fresh (cm) per invocation, as if the with were written
in the body. The wrapper answers for the function it wraps:
decorate-wraps, which is functools.update_wrapper, carries over
__name__, __doc__, the annotations, the __dict__ contents, and the
__wrapped__ chain.
@let [[db (connect)]]
@call-with [lock]
def handle [msg]:
...
One connection captured at definition; the lock taken and released per call. Stacked decorator lines read top to bottom as the nested blocks would.
Adding a decorator shape
Shape arms are macro arms on compile/decorate-subject, added with
extend-syntax. Added
arms are tried before the built-in ones, so an extension claims its
shape ahead of the invocation default:
extend-syntax compile/decorate-subject:
syntax-rules [my-extent]:
case (decorate-subject (my-extent group) subject):
(my-extent group subject)
That is the whole registration for a graft-style decorator: @my-extent [...] over any form now completes my-extent with the subject as its
body. A call-time variant follows the call- pattern: wrap the subject
in a lambda that establishes the extent and forwards *: args **: kwds.
Three rules keep the decorator position readable:
Claim only body-less shapes. Match
(my-extent group), one group and no body, so complete expressions keep their invocation meaning.Follow the two families. A lexical or run-now form grafts; a per-call form gets a
call-name.Match on the form’s own head. Arm literals match symbolically, like every
syntax-rulesliteral, so an aliased or module-qualified spelling of the head falls through to the default arm.
Seeing the expansion
macroexpand-1 shows the pipeline one step at a time, the hoist, the
peel, and the arm, which is the way to see why a decorator did or did
not fire:
(macroexpand-1 '(decorate ((let [[c 0]])) (function f [] c)))
;; (decorate-subject (let [[c 0]]) (decorate [] (function f [] c)))