For Development HEAD DRAFTSearch (procedure/syntax/module):

6.16 Parameters and dynamic states

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).


6.16.1 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:

ShareThreadLegacy
Toplevel mutation visible?yesnono
Dynamic mutation visible?yesnoyes

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.

Function: make-parameter value :optional converter
Function: make-shared-parameter value :optional converter
Function: make-thread-parameter value :optional converter
Function: make-legacy-parameter value :optional converter

[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.

  • The 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.
  • If you use gauche.parameter, make-parameter works as make-legacy-parameter. This won’t change, since gauche.parameter is kept for the backward compatibility.
  • If you use 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.

Macro: parameterize ((param value) …) body …

[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
Function: parameter? obj

[SRFI-226] Returns #t iff obj is an object created by make-parameter.

Function: current-parameterization

[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.

Function: parameterization? obj

[SRFI-226] Returns #t if obj is a parameterization, #f otherwise.

Function: call-with-parameterization parameterization thunk

[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.

Macro: parameterize/dynwind ((param value) …) body …

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:

  • You can use non-parameter procedure (a procedure which isn’t created by 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).
  • The last expression of body is not evaluated in the tail context, even this form itself is in the tail context.
  • This can handle the ’observer’ feature of the legacy parameters introduced by 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).


6.16.2 Dynamic states

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.

Macro: temporarily ((proc value) …) body …

[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.


6.16.3 Difference of parameters and dynamic states

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.


6.16.4 Transition to SRFI-226 parameters

First, here’s rule of thumb:

Under the hood:

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).



For Development HEAD DRAFTSearch (procedure/syntax/module):
DRAFT