Gforth provides flat closures (called closures in the following). Closures are similar to quotations (see Quotations), but the execution token (xt) that represents a closure does not just refer to code, but also to data. Running the code of a closure definition creates a closure data structure (also referred to as “closure”), that is represented by an execution token. The closure data structure needs to be allocated somewhere, and in Gforth this memory is managed explicitly.
As an example, consider a word that sums up the results of a function
( n -- r)
across a range of input values:
: sum {: limit start xt -- r :} 0e limit start ?do i xt execute f+ loop ;
You can add up the values of the functon 1/n for n=1..10 with:
11 1 [: s>f -1e f** ;] sum f.
Yes, you can do it shorter and more efficiently with 1/f
, but
bear with me. If you want to add up 1/n^2, you can write
11 1 [: s>f -2e f** ;] sum f.
Now if you want to deal with additional exponents and these exponents are known at compile time, you can create a new quotation for every exponent. But you may prefer to provide an exponent and produce an xt without having to write down a quotation every time. If the value of the exponent is only known at run-time, producing such an xt is possible in Forth, but even more involved, and consumes dictionary memory (with limited deallocation options). Closures come to the rescue:
: 1/n^r ( r -- xt; xt execution: n -- r1 ) fnegate [f:h ( n -r ) s>f fswap f** ;] ; 11 1 3e 1/n^r dup >r sum f. r> free-closure 11 1 0.5e 1/n^r dup >r sum f. r> free-closure
When 1/n^r
runs, it creates a closure that incorporates a
floating-point number (indicated by the f
in [f:h
), in
particular the value -r. It also references the code between
[f:h
and ;]
. The memory for the closure comes from the
heap, i.e. allocate
d memory (indicated by the h
in
[f:h
). 1/n^r
produces an xt representing this closure.
This xt is then passed to sum
and execute
d there.
When the closure is executed (in sum
), -r is pushed (in
addition to the n that has already been pushed before the
execute
) and the code of the closure is run.
The code above shows a pure-stack closure (no locals involved).
Pure-stack closures start with a word with the naming scheme
[T:A
where the type T can be n
(cell),
d
(double-cell), or f
(FP). The allocator A can be
l
(local), d
(dictionary), h
(heap), or
h1
: Allocate
the closure on the heap and free
it
after the first execution; this is used for passing data to another
task with send-event
(see Message queues). A pure-stack
closure consumes one T from a stack at closure creation time (when
the code containing the closure definition is run), and pushes an xt.
After creating the closure, execution continues behind the ;]
.
When the xt is executed (directly with execute
or indirectly
through, e.g., compile,
or a deferred word), it pushes the
stack item that was consumed at closure creation time and then runs
the code inside the closure definition (up to the ;]
). You can
deallocate heap-allocated closures with
free-closure
( xt – ) gforth-1.0 “free-closure”
Free the heap-allocated closure xt.
Like a quotation, a (flat) closure cannot access locals of the enclosing definition(s).
The words for starting pure-stack closure definitions are:
[n:l
( compilation – colon-sys; run-time: n – xt ; xt execution: – n ) gforth-1.0 “open-bracket-n-colon-l”
[d:l
( compilation – colon-sys; run-time: d – xt ; xt execution: – d ) gforth-1.0 “open-bracket-d-colon-l”
[f:l
( compilation – colon-sys; run-time: r – xt ; xt execution: – r ) gforth-1.0 “open-bracket-r-colon-l”
[n:d
( compilation – colon-sys; run-time: n – xt ; xt execution: – n ) gforth-1.0 “open-bracket-n-colon-d”
[d:d
( compilation – colon-sys; run-time: d – xt ; xt execution: – d ) gforth-1.0 “open-bracket-d-colon-d”
[f:d
( compilation – colon-sys; run-time: r – xt ; xt execution: – r ) gforth-1.0 “open-bracket-r-colon-d”
[n:h
( compilation – colon-sys; run-time: n – xt ; xt execution: – n ) gforth-1.0 “open-bracket-n-colon-h”
[d:h
( compilation – colon-sys; run-time: d – xt ; xt execution: – d ) gforth-1.0 “open-bracket-d-colon-h”
[f:h
( compilation – colon-sys; run-time: r – xt ; xt execution: – r ) gforth-1.0 “open-bracket-r-colon-h”
[n:h1
( compilation – colon-sys; run-time: n – xt ; xt execution: – n ) gforth “open-bracket-n-colon-h1”
[d:h1
( compilation – colon-sys; run-time: d – xt ; xt execution: – d ) gforth “open-bracket-d-colon-h1”
[f:h1
( compilation – colon-sys; run-time: r – xt ; xt execution: – r ) gforth “open-bracket-r-colon-h1”
If you want to pass more than one stack item from closure creation to execution time, defining more such words becomes unwieldy, and the code inside the closure definition might have to juggle many stack items, so Gforth does not provide such additional words. Instead, Gforth offers flat closures that define locals. Here’s the example above, but using locals-defining closures:
: 1/n^r ( r -- xt; xt execution: n -- r1 ) fnegate [{: f: -r :}h s>f -r f** ;] ;
The number, types, and order of the locals are used for specifying how
many and which stack items are consumed at closure creation time. At
closure execution time these values become the values of the locals.
The locals definition ends with a word with a naming scheme
:}A
, where A specifies where the closure is allocated;
in addition to l
(local), d
(dictionary), h
(heap), or h1
(heap, free
on first execution).
Note that the locals are still strictly local to one execution of the
xt, and any changes to the locals (e.g., with to
) do not change
the values stored in the closure; i.e., in the next execution of the
closure the locals will be initialized with the values that closure
creation consumed.
[{:
( compilation – hmaddr u latest wid 0 ; instantiation ... – xt ) gforth-1.0 “start-closure”
Starts a closure. Closures started with [{:
define
locals for use inside the closure. The locals-definition part
ends with :}l
, :}h
, :}h1
, :}d
or :}xt
. The rest of the closure definition is Forth
code. The closure ends with ;]
. When the closure
definition is encountered during execution (closure creation
time), the values going into the locals are consumed, and an
execution token (xt) is pushed on the stack; when that
execution token is executed (with execute
, through
compile,
or a deferred word), the code in the closure is
executed (closure execution time). If the xt of a closure is
executed multiple times, the values of the locals at the start
of code execution are those from closure-creation time,
unaffected by any locals-changes in earlier executions of the
closure.
:}l
( hmaddr u latest latestnt wid 0 a-addr1 u1 ... – ) gforth-1.0 “close-brace-locals”
Ends a closure’s locals definition. The closure will be allocated on the locals stack.
:}d
( hmaddr u latest latestnt wid 0 a-addr1 u1 ... – ) gforth-1.0 “colon-close-brace-d”
Ends a closure’s locals definition. The closure will be allocated in the dictionary.
:}h
( hmaddr u latest latestnt wid 0 a-addr1 u1 ... – ) gforth-1.0 “colon-close-brace-h”
Ends a closure’s locals definition. At the run-time of the
surrounding definition this allocates the closure on the heap;
you are then responsible for deallocating it with
free-closure
.
:}h1
( hmaddr u latest latestnt wid 0 a-addr1 u1 ... – ) gforth-1.0 “colon-close-brace-h-one”
Ends a closure’s locals definition. The closure is deallocated
after the first execution, so this is a one-shot closure,
particularly useful in combination with send-event
(see Message queues).
:}xt
( hmaddr u latest latestnt wid 0 a-addr1 u1 ... – ) gforth-1.0 “colon-close-brace-x-t”
Ends a closure’s locals definition. The closure will be allocated by
the xt on the stack, so the closure’s run-time stack effect is (
... xt-alloc -- xt-closure )
.
>addr
( xt-varue – addr ) gforth-experimental “to-addr”
Obtain the address addr of the varue xt-varue
If you look at closures in other languages (e.g., Scheme), they are quite different: data is passed by accessing and possibly changing locals of enclosing definitions (lexical scoping). Gforth’s closures are based on the flat closures used in the implementation of Scheme, so by writing the code appropriately (see the following subsections) you can do the same things with Gforth’s closures as with lexical-scoping closures.
In our programming we have not missed lexical scoping, except when
trying to convert code (usually textbook examples) coming from another
language. I.e., in our experience flat closures are as useful and
similarly convenient as lexical scoping. For comparison, if Gforth
supported lexical scoping instead of flat closures, the definition of
1/n^r
might look as follows:
\ this does not work in Gforth: : 1/n^r ( r -- xt; xt execution: n -- r1 ) fnegate {: -r :} [:h s>f -r f** ;] ;
But if you want to know how to convert lexical scoping to Gforth’s flat closures, the following subsections explain it.