Search code examples
rustcommon-lisp

Is there something akin to Common Lisps "special variable" in Rust?


The code I am working on is single threaded, but for the sake of this question, this is secondary.

In Common Lisp, there is a (in my eyes) cool feature, called special variables and it goes like this:

(defvar *i-am-special*)

(defun some-low-level-function-deep-down ()
  (format t "~a~%" *i-am-special*))

(defun some-function-above ()
  (let ((*i-am-special* "Hello World"))
    (some-low-level-function-deep-down)))

As you see, the *i-am-special* variable is not bound to a value when it is declared. The some-function-above then dynamically binds a value to the special variable and down the call tree, some others might even rebind it and thus shadowing the previous binding until they go out of context.

Now, in my Rust application I consider having a command line argument switch, which changes the behavior of a leaf function, deep down. The typical situation, usually solved by a global variable (a simple bool in my case).

fn leaf(a:bool, b:bool) -> bool {
  !my_command_line_switch || a && !b
}

fn main () {
  let args = Args::parse();
  enter_call_tree_eventually_calling_leaf()
}

As I use Common Lisp most of the time, I am itching to use a special variable for this, but... does Rust have something like that or would I have to resort to a good old fashioned global variable instead?

Passing the Args down the call tree into this rather frequently called function sounds contra-indicated to me, in my case.

The otherwise very good article about rust global variables falls a bit short on the question about, how to set the value of a thread local variable from main... and I am not sure if I should do/could do Àrgs::parse() from within a thread_local!() form... and also I do not want to have all the command line args global.

Lastly, while thinking about the topic a bit more, I start to wonder, how std::env::args actually does it. I sure do not need unsafe {...} to access it...


Solution

  • No, Rust does not have Lisp-style special variables built-in; it would run contrary to the Rust theme of mutability control.

    You can build special variables in Rust, but it seems like an odd fit for what you're actually trying to do. You don't seem to need the dynamic scoping, and if your program uses threads, then I think you want the flag not to be thread-local.

    Rust programs usually parse command-line arguments once, at startup, producing a struct named something like Options. This data is immutable from then on. The problem, then, is how to make the options available in the rest of your code.

    1. The most straightforward way to do this is to pass the command-line options around everywhere. This is the most test-friendly approach.

    2. Or, you can use a global lock. In this case a OnceLock is a good fit:

      use std::sync::OnceLock;
      
      struct Options { ... }
      
      static OPTIONS: OnceLock<Options> = OnceLock::new();
      
      /// True if the `--my-switch` command-line option was set.
      fn my_switch() -> bool {
          OPTIONS.get().unwrap().my_switch
      }
      
      fn main() {
          OPTIONS.set(Options::parse()).unwrap();
          ...
      }
      
    3. If the flag is very heavily accessed, so that you need to avoid locking for performance, you can use a global atomic boolean:

      use std::sync::atomic::{AtomicBool, Ordering};
      
      static MY_SWITCH: AtomicBool = AtomicBool::new(false);
      
      pub fn my_switch() -> bool {
          MY_SWITCH.load(value, Ordering::Relaxed);
      }
      
      pub fn set_my_switch(value: bool) {
          MY_SWITCH.store(value, Ordering::Relaxed);
      }
      

      (The load and store calls here compile to ordinary mov instructions on x86, so this is really just a global boolean.)