A parameter is Scheme’s way to implement dynamically scoped variables. It is codified in SRFI-39 and incorporated into R7RS.
Note: Starting from 0.9.13, we’re adopting SRFI-226 semantics of parameters, which behaves slightly different from our legacy parameters. See Transition to SRFI-226 parameters, for the details.
It is a special procedure created by make-parameter
. It accepts
zero or one argument. When called without an argument, it returns the
value in the current dynamic environment.
If an argument is passed, it alters its value
with the given value in the current dynamic environment,
and returns the previous value.
Using parameterize
macro, which is similar to let
syntactially,
rebinds the parameters’ value during execution of its body, dynamically
rather than lexically.
(define var (make-parameter 0)) (define (show-var) (print (var))) (show-var) ; prints 0 (parameterize ((var 1)) (show-var)) ; prints 1 (define f) (parameterize ((var 2)) (set! f (lambda () (show-var)))) (f) ; prints 0, since its out of the dynamic extent of var=2
A parameter’s value can be mutated by calling the parameter with the new value:
(define var (make-parameter 0)) (var) ; ⇒ 0 (var 1) (var) ; ⇒ 1 ;; Gauche extension: you can use set! too (set! (var) 2) (var) ; ⇒ 2
Originally, parameters in Scheme were simply a hack with an ordinary procedure
and dynamic-wind
to emulate dynamically scoped variables.
The same mechanism have been used to manage dynamically scoped states, too.
As the experience accumulated, however, it is learned that it’s better to treat dynamically scoped variables and dynamically scoped states differently; efficient implementations differ, and they have slightly different semantics with delimited continuations. SRFI-226 incorporated this distinction.
As of Gauche 0.9.13, we adopt this distinction, too. See Difference of parameters and dynamic states, for the details.
If you’re a long time Gauche user (thanks!), remember to check out the changes on 0.9.13 and how to transition to it (see Transition to SRFI-226 parameters).
• Parameters: | ||
• Dynamic states: | ||
• Difference of parameters and dynamic states: | ||
• Transition to SRFI-226 parameters: |
Because of historical reasons, parameters come with three flavors; their differences are subtle, and only visible when you use mutation in multi-thread code. If you only use parameters for dynamic binding (i.e. within R7RS spec), all flavors of parameters behave the same.
The three flavors are shared parameters, thread parametres, and legacy parameters. The first two is defined in SRFI-226 (though it doesn’t use the term “shared”, for the ordinary parameters are shared parameters). The third one is compatible to the parameters in Gauche 0.9.12 and before.
The following table shows whether a mutation in one thread is visible from another thread:
Share | Thread | Legacy | |
---|---|---|---|
Toplevel mutation visible? | yes | no | no |
Dynamic mutation visible? | yes | no | yes |
Here, “toplevel mutation” is a mutation done for a parameter
outside of any parameterize
form, and “dynamic mutation”
is a mutation done for a parameter in a dynamic scope of
parameterize
form:
(define p (make-parameter 0)) (p 1) ; toplevel mutation (parameteize ((p 2)) (p 3) ; dynamic mutaiton ) (p 4) ; toplevel mutation
For the shared and legacy parameters, dynamic mutation is only visible for the thread that shares the same dynamic binding.
[R7RS base][SRFI-226] Creates a parameter whose initial value is value. If an optional argument converter is given, it must be a procedure that takes one argument and returns one value; whenever the parameter’s value is about to change, the procedure is called with the given value, and the value the procedure returns will be the parameter’s value. The converter procedure can raise an error or reject to change the parameter’s value.
As described above (see Parameters), parameters have three
flavors; make-shared-parameter
, make-thread-parameter
,
and make-legacy-parameter
creates shared, thread, and legacy
parameters, respectively.
For the compatibility, make-parameter
behaves differently
depending on which module you uses.
make-parameter
in gauche
module works as
make-legacy-parameter
in 0.9.13, but it will be switched
to make-shared-parameter
in future versions. To keep
the legacy code working, you should either use gauche.parameter
module, or replace make-parameter
for make-legacy-parameter
.
gauche.parameter
, make-parameter
works as make-legacy-parameter
. This won’t change, since
gauche.parameter
is kept for the backward compatibility.
srfi.226
, make-parameter
is as SRFI-226
defines, that is, make-shared-parameter
.
NB: R7RS defines make-parameter
. SRFI-226 defines
make-parameter
and make-thread-parameter
.
The other two names, make-shared-parameter
and
make-legacy-parameter
, are Gauche specific.
NB: In 0.9.9 and before, this procedure returns a parameter
object, and we used object-apply
method to make it behave
like a procedure. However, R7RS explicitly defines the return
values to be a procedure; notably, portable code expects
(procedure? (make-parameter 'z))
returns #t
.
As of 0.9.10, we switched make-parameter
to return
a procedure. It won’t change the external behavior, except
when you test the parameter p with
(is-a? p <parameter>)
; you have to use parameter?
instead.
[R7RS base][SRFI-226] Evaluates body …, with change parameter param’s value to the given value within the dynamic scope of body …. Returns the value(s) of the result of the last body.
If parameterize
form is at a tail position and all params
are parameter objects , body is
also evaluated at a tail position. (This is required by SRFI-226,
though not required by R7RS).
Note that R7RS and SRFI-226 requires param to be evaluated
to a parameter. Gauche’s parameterize
has been acceping
dynamic states as well, but such usage is
deprecated; you should use temporarily
instead (see Dynamic states).
To ease transition, Gauche 0.9.13 checks if every param yields a
parameter, at runtime, and if not, it switches to the legacy implementation
which does not evaluate body at a tail position.
In future we remove this compatibility feature. If you want to
check your code is compatible to the new spec, add (use srfi.226)
to your code.
Some examples:
(define a (make-parameter 1)) (a) ⇒ 1 (a 2) ⇒ 1 (a) ⇒ 2 (parameterize ((a 3)) (a)) ⇒ 3 (a) ⇒ 2
[SRFI-226]
Returns #t
iff obj is an object created by
make-parameter
.
[SRFI-226]
Returns a parameterization object that reifies
the current dynamic bindings of parameters. It can be
passed to call-with-parameterization
to carry over
the parameterization.
[SRFI-226]
Returns #t
if obj is a parameterization,
#f
otherwise.
[SRFI-226]
Invoke thunk with a continuation same as
the call of call-with-parameterization
except its
parameterization is swapped with parameterization.
That is, if the call to this form is in a tail context,
the call to thunk is also a tail context.
This handles the parameter bindings using dynamic-wind
.
It is fully compatible with pre-0.9.13 parameterize
.
This form is mainly provided to make legacy code work.
This differs from the current parameterize
in the following points:
make-parameter
) as param, as long as it
follows the parameter protocol. Note that if you want to use
such ’parameter-like’ procedures to manage dynamic states,
you should use temporarily
below (see Dynamic states).
gauche.parameter
. Our experience
shows the feature has been hardly used, and is incompatible with SRFI-226
semantics, so we deprecated it.
Note: If you use gauche.parameter
module, it shadows built-in
parameterize
with its own parameterize
, the latter of which
is an alias of parameterize/dynwind
, so that the compatibility
is kept. New code should not use gauche.parameter
module.
See gauche.parameter
- Parameters (legacy).
A dynamic state is a state associated to a certain dynamic extent of the code execution. (In SRFI-226, it is called “parameter-like objects”).
It can be managed with a procedure that follows this protocol:
You can use temporarily
macro in place of parameterize
to
change the state dynamically.
(define state (let val 0 (case-lambda [() val] [(newval) (set! val newval)]))) (define (get-state) (state)) (get-state) ⇒ 0 (temporarily ([state 1]) (get-state)) ⇒ 1
If you just manage a value, it is not much different from a parameter
(without a converter) and parameterize
.
However, unlike parameterize
, it is guaranteed that state is
reset by calling the dynamic state with the previous value,
when the control goes out of the dynamic scope.
You can do
more than just changing the internal values inside the procedure,
such as switching state of external resource.
[SRFI-226] Evaluates procs and values. Each proc must yield a dynamic state, i.e. a procedure that takes zero or one arguments.
Then the current state each proc represents is changed to value during evaluating body …. The state is restored when the control leaves body …. The results of the last expression of body will be returned.
Whenver control trasfers out of body, or reenters to body, the state of each proc is saved and restored.
At a first grance, parameters and dynamic states look almost same; both are procedures taking zero or one arguments; when called with no arguments, it returns the “current” value; when called with one argument, it replaces the “current” value with the given argument.
The difference is that the “state” of a parameter, or a parameterization,
is tied to a continuation.
If a continuation frame where parameterize
is evaluated is
popped, the parameterization simply goes away with it, and the previous
parameterization (tied to the previous continuation) automatically
becomes visible, instead of the explicit operation of restoring
the previous value. That’s why the body of parameterize
can be evaluated in tail context—we don’t have a restore
operation to perform after the body is evaluated.
On the other hand, dynamic states rely on the ‘after’ handler
of dyanmic-wind
to restore the state, hence
the body in temporarily
can’t be evaluated in a tail context.
You want to use dynamic states when the change of state requires some actions other than storing a value, e.g. notifying the change to a resource manager. When a dynamic state procedure is called with a new value, you can take necessary actions. Restoration of parameter values after leaving a dynamic extent is implicit; you can’t know when the parameter value is restored.
Parameters can have converter procedure to coerce the value to be set. Dynamic states aren’t supposed to have the converter procedure, though. If the dynamic extent is inactivated and then resumed by continuations, the dynamic value needs to be restored without passing through the converter procedure, so it requires additional protocol, which isn’t standardized.
parameterize
, you won’t see
any changes.
gauche.parameter
, it’ll preserve
the previous behavior. This helps legacy code to keep working.
parameterize
, replace
it with temporarily
(see Dynamic states).
make-thread-parameter
and
parameterize/dynwind
, in place of
make-parameter
and parameterize
.
(see Parameters).
Here we call the parameter implementation before 0.9.13 as “legacy model”, while the one in 0.9.13 and later as “new model”.
In the legacy model, parameterization is realized by swapping
each parameter’s value in dynamic wind’s before and after thunk.
Roughly speaking, parameterize
is expanded as follows
(this assumes Gauche’s parameter protocol that returns previous
value when a new value is set. Also, we omit consideration of
converter, for simplicity):
(parameterize ((param expr)) body ...) ≡ (let ((p param) (v expr)) (dynamic-wind (lambda () (set! v (p v))) (lambda () body ...) (lambda () (set! v (p v)))))
This model directly replaces the value of the parameter in the dynamic scope of body …. So the parameter value needs to be thread-specific, otherwise the swapping of the value would be visible from other threads, which is not desirable.
Note that even parameter value is thread-specifc, you can still invoke a continuation captured in a different thread and expect the parameter values are carried over, for the switching conitnuation triggers execution of before/after thunks, which restores the values of parameters in the thread that invokes the continuation.
Also keep in mind that the body can’t be in tail context even the parameterize form is, since you have to execute after thunk after the body is evaluated.
In the new model, we keep a chain of dynamic binding frames,
and parameter value is looked up from it. The dynamic chain is a part of
continuation, so when a continuation is popped, the dynamic frame
created in the current continuation frame is also popped implicitly.
With this model, parameterize
is expanded something like this:
(parameterize ((param expr)) body ...) ≡ ((lambda (p v) (push-dynamic-frame! p v) body ...) param expr)
Here, push-dynamic-frame!
is a hypothetical procedure that pushes
new dynamic frame in the current continuation.
When the control returns to the continuation of the parameterize
form, the additional dynamic frame is popped automatically
so we don’t need a special operation. If a continuation is captured
in body, the dynamic frame chain is saved along the continuation.
When the continuation is invoked in another thread, the dynamic frame chain
is restored.
This allows the body to be run in tail context if parameterize
itself is in tail context.
If you mutate parameter value (as opposed to binding it with
parameterize
), the dynamic frame is mutated. If the frame
is shared between multiple threads, those mutations are visible immediately;
they’re pretty much the same as lexical frames, except the scopes.
SRFI-226 thread parameters are required to contain mutations within
a thread. So we create a new thread-local whenever a thread parameter
is dynamically bound by parameterize
and keep the parameter’s
value in it. The mutation on a thread
parameter is never visible from other thread, even if the dynamic
frames are shared, or a continuation having it is passed to another
thread. So, be careful using thread parameters. For example,
if you create a future (see control.future
- Futures), the expression in it
is evaluated in another thread, so it may not see the same
value in thread parameters as the caller. Thread parameters
also incur overhead for every parameterize
, to allocate
a new thread local.
The new model also restricts parameterize
to bind
only parameters. “Parameter-like” objects, a procedure that
merely follows the parameter protocol, can’t be handled
in parameterize
. You should separate them and use
temporarily
for the latter (see Dynamic states).
To ease transition, 0.9.13’s parameterize
checks if the given
“parameters” are all actual parameters at run-time, and if not,
it evalues as if the form is parameterize/dynwind
instead.
This kind of loose behavior tends to be the source of bugs,
so we’ll eventually make it stricter. You can check if your code
is conforming the new model by using (use srfi.226.parameter)
.