Parameters can be either parameterized or directly assigned to. The latter mutates a thread cell, while the former attaches a continuation mark.
When is it useful to actually assign a parameter instead of using
parameterize? The two use cases I can think of are:
- When you want
(require somelib) to trigger configuration of a parameter
- When you don't feel like indenting your code to the right more
These use cases don't seem super compelling on their own. I think the former could be replaced by parameterizing the body of client modules instead. Are there other reasons to directly assign parameters?
For context, I've been thinking about making something like async-await for Racket, and I want it to cooperate with parameters and continuation marks. So I'm doing a lot of research into the evaluation model for continuations and threads.
When you want to run into very subtle bugs that take ages to figure out. :>
For more, see here: Running a procedure from 'delay' does not capture modified parameters
I have some html rendering code that uses a mix of parameters with parameterize as well as regular variables. I use parameterize when I can to hold the current style when the renderer descends into a new element. For other cases it wasn't convenient to use parameterize, so I used a standard variable with assignment instead. I can imagine someone choosing a slightly different path and using a mix of parameterize and assignment to parameters.
Assigning a parameter is useful when you want to provide some interface for updating its value.
(provide current-addresses add-address)
(define _current-addresses (make-parameter (hasheq))
(define (add-address id val)
(_current-addresses (hash-set (_current-addresses) id val))
(define (current-addresses) (_current-addresses))
There may be better ways to do this kind of thing? but I know it’s something I’ve done.
In that particular case, wouldn't a mutable hash and
hash-set! be better? Or is there some reason to prefer a private parameter?
The docs on command line parsing use parameters to hold the flag values. So maybe a good question is, why do the docs use parameters here (without parameterize)?
Incidentally, I adopted this pattern in the cli library as well, so that flags correspond to parameters, but I don't recall now whether I considered using mutable (lexically-scoped / global) hashes.
Another place I've personally used parameters is in the probe debugger for Qi. This comment explains how parameters are used. Would parameterize work for this usecase? I'll plan to take a closer look at this at some point.
I'm being sort of devil's-advocate-ish here, but:
"Avoiding indent" seems easy to dismiss, but given the amount of effort that's gone into internal
define as an alternative to
let, I wonder if a "post-mortem" discussion of that effort might be relevant for this?
Given that parameters are thread-local, the ability to set that thread-local default --- the value to be used in a thread if no one
parameterizes it --- seems useful? It seems to enable expressing, "If no one has some better idea, here's what I think the value should be". Maybe as you say all such uses could be replaced by a parameterization introduced in the expansion of the module... but I wonder if that option is always available or practical?
Wouldn't that case be dealt with by simply giving the parameter an initial value?
If no one has some better idea, here's what I think the value should be".
Wouldn't that case be dealt with by simply giving the parameter an initial value?
Yes, for a library that defines a parameter: "Here is the default value for all threads created by every other library or applications that uses me."
But for an application using that library, it might want to set its own default?
Because parameters are thread-safe?
The thread safety doesn't extend to mutable objects contained by parameters. You're still concurrently updating a hash table.
In the contrived example I gave above, the value contained by the parameter is immutable. I am definitely not a Racket expert so maybe my mental model is wrong. But if I “update” an immutable hash table contained in a parameter by doing this:
(current-param (hash-set (current-param) 'newkey "newval"))
…then I’m not mutating a value held in the parameter, I’m rather changing the current thread’s copy of the parameter to point to a completely new hash table, right? Or no?
In other words, it’s still thread safe, because if I had spawned another thread before doing that update, that thread’s
current-param parameter would still be hanging onto the original hash table. Using a parameter in this way prevents me from stomping on another thread’s data, where
hash-set! definitely would not give me that assurance. [edit to add:] For sure it makes sense to me that it doesn’t make much sense to try and clothe a mutable value in a parameter, at least in any context I can think of.
I’ve run into this issue directly with Pollen projects. Pollen holds metadata about the current document (if there is one) in an immutable hash table served by a
current-metas parameter, because it’s useful to be able to get at that data from everywhere. But it’s also useful to be able to add to or update those values from functions called within the document itself.
You don’t want the scope of those changes to be limited to that one function call, you want them to be visible to all later code evaluated in the Pollen program/document.
You also need to consider that multiple Pollen programs/documents may be running concurrently.
So you need a) an immutable hash-table contained in b) a parameter, and c) a way to update it in a way that “sticks” outside the current lexical scope (direct assignment).
I've asked myself the same question. One possibility is that you can
parameterize the setting in other places in the code, but that could be very brittle depending on how the parameter or even values derived from it are used in the code.
Another possibility could be that it's easier to write
(set! my-argument new-value).
On the other hand,
(define my-argument initial-value)
is easier to write than
(define my-argument (make-parameter initial-value))
I guess the actual answer has to come from the person who wrote the
I think a more illuminating question is, what advantage would any other form have over direct parameter assignment for that example?
Direct variable assignment with
set! would be simpler, and uses of the variables wouldn't require parens.
For one, just using straightforward
set! would avoid wondering about why the author of the code used parameters. For that reason, I think using
set! is clearer.
Initially, one can simply follow the example and use the parameters, than, once their program grows to multiple files, they can move each parameter to the most appropriate file in their project and things will continue to work.
If you use
set!, you'll be in for a surprise when you refactor your code.
There are, of course, several ways to solve the multi-file project problem, but I am not sure if any alternative would be clearer or simpler.
Usually, I try to avoid global state (including parameters) for command line arguments. I use one module that parses the command line data and returns it as a struct.
An example is here. I use
set! only locally in the function
make-command-line-data. The main program in
todoreport.rkt gets the command line arguments with
make-command-line-data and calls the actual functionality in other modules. No code accesses global state with command line data.
I try not to pass the command line struct to code that's unrelated to command line processing. Called functions are supposed to only get the data they need.
Of course, the above approach may not work or not work so easily for some other programs. That said, if in doubt I would start with a relatively simple design and refactor later if the simpler design is no longer enough (YAGNI). (You could argue that just using parameters would be simpler, but not depending on global state was so important to me that I didn't use this design. )
current-command-line-arguments is a parameter.
EDIT for p.s.: I apologize for being a smarty-pants. I realize that the fact that Racket originally provides this information as a parameter, doesn't really have any bearing on how you use it and treat it thereafter in your program.