I'm trying to make sense of the example code here (below Examples). I don't understand that parametrize construct. The docs for it are here, but they don't help. What does it do?
parameterize
sets particular parameters to specified values for the duration of the block, without affecting their values outside of it.
Parameterize is a means by which you can dynamically re-bind values within an existing function, without using lambda to do so. In practice sometimes it is a lot easier to use parameterize to re-bind values within a function rather than being required to pass arguments and bind them using lambda.
For example, say that a library that you use emits HTML to stdout but for sake of convenience you want to capture that value to a string and perform further operations on it. The library designer has at least two choices to make that easy for you: 1) accept an output port as a argument to the function or 2) parameterize the current-output-port value. 1 is ugly and a hassle. 2 is nicer since the most likely behavior is to print to stdout, but in case you want to print to a string-port you can just parameterize the call to that function.
parameterize
is used to have values that are "dynamically scoped". You get a parameter with make-parameter
. The parameter itself behaves as a function: call it with no inputs and you get its value, call it with one value and it will set the value. For example:
> (define p (make-parameter "blah"))
> (p)
"blah"
> (p "meh")
> (p)
"meh"
Many functions (including many primitive ones) use parameters as a way to customize their behavior. For example printf
will print stuff using the port that is the value of the current-output-port
parameter. Now, say that you have some function that prints something:
> (define (foo x) (printf "the value of x is ~s\n"))
You usually call this function and see something printed on the screen -- but in some cases you want to use it to print something to a file or whatever. You could do this:
(define (bar)
(let ([old-stdout (current-output-port)])
(current-output-port my-own-port)
(foo some-value)
(current-output-port old-stdout)))
One problem with this is that it is tedious to do -- but that's easily solved with a macro. (In fact, PLT still has a construct that does that in some languages: fluid-let
.) But there are more problems here: what happens if the call to foo
results in a runtime error? This might leave the system in a bad state, where all output goes to your port (and you won't even see a problem, since it won't print anything). A solution for that (which fluid-let
uses too) is to protect the saving/restoring of the parameter with dynamic-wind
, which makes sure that if there's an error (and more, if you know about continuations) then the value is still restored.
So the question is what's the point of having parameters instead of just using globals and fluid-let
? There are two more problems that you cannot solve with just globals. One is what happens when you have multiple threads -- in this case, setting the value temporarily will affect other threads, which may still want to print to the standard output. Parameters solve this by having a specific value per-thread. What happens is that each thread "inherits" the value from the thread that created it, and changes in one thread are visible only in that thread.
The other problem is more subtle. Say that you have a parameter with a numeric value, and you want to do the following:
(define (foo)
(parameterize ([p ...whatever...])
(foo)))
In Scheme, "tail calls" are important -- they are the basic tool for creating loops and much more. parameterize
does some magic that allows it to change the parameter value temporarily but still preserve these tail calls. For example, in the above case, you will get an infinite loop, rather than get a stack overflow error -- what happens is that each of these parameterize
expressions can somehow detect when there's an earlier parameterize
that no longer needs to do its cleanup.
Finally, parameterize
actually uses two important parts of PLT to do its job: it uses thread cells to implement per-thread values, and it uses continuation marks to be able to preserve tail-calls. Each of these features is useful in itself.