What is direct parameter assignment useful for?

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.

Contrived example:

#lang racket/base

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

2 Likes

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?

1 Like

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?

1 Like

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
(my-argument new-value)
than
(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 command-line example. :slight_smile:

1 Like

I think a more illuminating question is, what advantage would any other form have over direct parameter assignment for that example?

1 Like

Direct variable assignment with set! would be simpler, and uses of the variables wouldn't require parens.

For one, just using straightforward define and set! would avoid wondering about why the author of the code used parameters. :slight_smile: For that reason, I think using define and 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 define and 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.

Alex.

2 Likes

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 define and 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. :slight_smile: )

1 Like

FWIW current-command-line-arguments is a parameter. :wink:

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.

I think this is an important point: Defining things in a way that supports a program growing.

Sometimes in a new program you know that you have some bit of "configuration". The parameter signature is a super flexible way to handle getting and setting it.

You could start with some-config being from make-parameter. That will tend to do the right thing for most sorts of configuration, even if your program later starts to use multiple threads.

However maybe you have some configuration that should not be thread-specific. Well you can redefine some-config to be a case-lambda, where you use or set! a variable, maybe guarded by a semaphore. All of the call sites (where you set or get) can remain the same.

You could even redefine some-config to be a wrapper around the "raw" thread-cell get/set functions. Or read/write an external data store. Or whatever.

So you could say that parameters are not a bad default to start with, plus that "interface" is easy to update to be backed by other representations.


Having said all that, some people prefer not to have wrapper abstractions like this. As a program grows, sometimes these abstractions help minimize gratuitous changes... but sometimes they hide necessary changes. As with many things, "it depends"?

4 Likes