fluid-let icon indicating copy to clipboard operation
fluid-let copied to clipboard

No easier way to access value other than by creating a closure?

Open grothesque opened this issue 2 years ago • 4 comments

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.

grothesque avatar Feb 03 '23 17:02 grothesque

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::LocalKey does. 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 unsafe to not violate lifetime rules and return a reference

    I 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 T out 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 implement
    fn better_get(&self) -> Pin<Ref<T>> {
        todo!()
    }
    

    where fluid_let::Ref<T> is not Unpin but is Deref<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.

ilammy avatar Feb 05 '23 00:02 ilammy

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.

grothesque avatar Feb 07 '23 22:02 grothesque

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

ilammy avatar Feb 12 '23 12:02 ilammy

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 ?)

ilammy avatar Feb 12 '23 12:02 ilammy