No easier way to access value other than by creating a closure?
Interesting project! I think this idea could have great potential especially in the absence of default function parameters in Rust.
However, I find the way to retrieve (generic) dynamic values a bit too cumbersome:
LOG_FILE.get(|current| {
if let Some(mut log_file) = current {
write!(log_file, "{}\n", msg)?;
}
Ok(())
})
Having read the docs, I understand that this is dictated by the need to limit the lifetime of the retrieved value to shorter than the lifetime of the closure where it was set.
I am no guru of Rust lifetimes, but is there really no way to solve the above problem without introducing a new closure? It seems that it would be sufficient if the lifetime would be limited to that of the current code block.
I am no guru of Rust lifetimes, but is there really no way to solve the above problem without introducing a new closure? It seems that it would be sufficient if the lifetime would be limited to that of the current code block.
Neither am I an expert :(
You're right about the current block.
From what I understand, the options are:
-
Use new closure to enforce lifetime
This is what
std::thread::LocalKeydoes. New closure makes a new shorter lifetime for a reference. -
Clone and return the stored value, detaching the lifetime
You break it – you buy it :)
This is what
.copied()methods do here. -
Pinky-promise with
unsafeto not violate lifetime rules and return a referenceI guess that would be an alternative with "friendlier" syntax. But the only available—nameable—lifetime there is
'static, so it would be on the user to promise to not take the&'static Tout of the block where they got it. -
Do the above, but limit the lifetime to that of the current block
The problem here is that in Rust the lifetime of the current block (or the current function) is not nameable. You cannot refer to it from some generic or macro or whatever. So I can't easily add some
fn better_get(&self) -> &'enclosing_block T { todo!() }Closure gets around this limitation because of how lifetime elision works for closures: every reference parameter gets the lifetime of the closure call, and the compiler nests the lifetime of the closure call in the block. Basically, closures are the way to explicitly denote the blocks.
-
~~Maybe there is some hack with
Pin~~ no there isn't...allowing to implementfn better_get(&self) -> Pin<Ref<T>> { todo!() }where
fluid_let::Ref<T>is notUnpinbut isDeref<Target=T>.Then you'd get an owned value, sure, but 1) you shouldn't be able to move it anywhere, 2) a reference you get from it should be tied to the lifetime of that owned value, which is itself tied to that of the enclosing block.
Thank you very much for enumerating the various possibilities.
I also do not see a better alternative.
However, I have the feeling that in principle a facility for dynamically scoped variables would be a particularly good fit for Rust, since there is no way to have (keyword) arguments with default values for functions. Unfortunately, to me fluid-let in it's current form does not seem to be practical if only due to the cumbersome syntax. (Imagine writing a function that looks up 10 of these dynamic variables. This can arise quickly for a plotting library, say).
Perhaps this is just a consequence of Rust being simply not dynamic enough for an idiomatic implementation of dynamic variables.
Tried out the thing with Pin – no, it won't help, it solves a different problem.
The thing is, Rust always allows to move things in general. Pin prevents the thing contained within to be safely moved out, but nothing stops you from moving the pinned reference itself around – which is exactly the problem for dynamic variables.
Lifetimes don't help here since the dynamic variable itself is 'static. Thus the only lifetimes available are either 'static, or the unnamed lifetime of the function call itself (exploited by the closure hack).
The next best thing that I could come up with is some crazy macro that you use like this:
fn stuff() {
fluid_get!(log_file = &LOG_FILE);
// you can use `log_file` here
}
similar to existing fluid_set!(), which would expand into something like this:
fn stuff() {
let block = ();
let reference = unsafe { DynamicRef::new(&LOG_FILE, &block) };
let log_file = reference.as_ref(); // Option<&File>
}
This way it's possible to tie log_file's lifetime to that of the current block, by defining a secret variable hidden by macro.
Still rather weird syntax, but at least it spares all the ugliness of dealing with closures (like, what if you want to use ?)