Short answer: Valen would have something similar to Fn and FnMut (but phrased in terms of effects rather than Fn vs FnMut). In other words, we would be able to express "a closure that does not modify anything it captures", or rather, "a closure that has no mut effects".
That closure, because it doesn't modify anything it captures, would be safe to share among multiple threads in a structured-concurrency-like / std::thread::scope-ish way.
The key here is that one _can_ express immutable references in Valen; an immutable reference is a reference that the containing function doesn't express a `mut` effect for. And once we have immutable references, we get all of the nice concurrency benefits that Rust trailblazed.
I'd also like to make a way to do the above without a function call, perhaps using something like the `parallel` keyword I described in [0].
A no-access reference is an interesting idea. That could be a more powerful way to express may_dangle. In Valen, I hope to have an "opaque" group to express something like that.
I don't know whether it's a good idea, but in Valen I'm trying to decouple access capabilities away from the reference types as much as possible. We'll see if that bet pays off.
[0] https://verdagon.dev/blog/seamless-fearless-structured-concu...
I'm contemplating this. Is it enough?
Suppose I have an object (I'm not even trying to get the syntax right, especially since Valen's syntax appears a bit different from Rust's):
let obj: T = ...;
And I also create a structured concurrency thingy in the same scope: let workgroup: StructuredConcurrencyThingy = ...;
Now I pass references to both of these down the callchain, through a few functions, maybe via some structs with lifetime parameters, and in the inner function I do this: workgroup.submit(move || print_in_rust(obj));
where print_in_rust is a Rust function taking &T. (I haven't the faintest clue how to spell that in Valen.) So I'm making a closure, and the closure captures obj, and the closure needs obj to exist and be immutable for the lifetime of the closure, which exceeds the creating function's lifetime. It's bounded by the workgroup's lifetime, and Rust is fine with this.But, if I'm understanding you right, the immutability of the referent of obj depends on the signatures of everything in the callchain that might execute during the lifetime of the closure. How does that work?
edit: On further contemplation, I don't think that actual concurrency is needed to illustrate it. I think the same issue exists if I have a T<'a> that has a method that takes an &'a reference (probably like store_a_reference(&mut self, ref: &'a u32)) and dereferences ref both immediately and later and asserts that it sees the same value both times.
In Rust, a reference is forever shared/immutable or forever unique/mutable.
In Valen, a reference is... "it depends". Specifically, it depends on the context.
It's similar to a &GhostCell<T>, where its mutability isn't determined yet because the GhostToken isn't present yet.
So, what determines the mutability at any given point in time? The function's `mut` effects (or lack of them) for the group/path that the reference is pointing to.
So we can imagine a `execute_on_4_threads` function like this:
func execute_on_4_threads<C', F: Func<void, (), C>>(
workgroup: &W,
closure: F
) { ... }
(`Func<void, (), C>` is a trait for a function that returns void, takes no extra parameters, and names its captures as group C).The most relevant fact here is that this function doesn't declare any `mut` effects at all (not on C, not on W's group, nothing), so nothing is being mutated. (And because of that, this function's callees also can't have any `mut` effects; they also can't mutate the data)
Now let's say we changed `workgroup`'s type to `&W in w`, and added a `mut(w)` to the function, to describe that we might modify the workgroup.
At that point, we would still know that we can invoke the closure from 4 threads, because we declared no relationship between `w` and `C`, so the compiler assumes (and enforces) that they're disjoint, have no overlap, nothing in one aliases anything in the other.
Hopefully that helps. Maybe I should write a blog post on this, my posts are usually clearer than my HN comments.
(Also, I'm not sure I understand your edit, if you could clarify that would be much appreciated)
#[derive(Default)]
struct Receiver<'a> {
ref_and_val: Option<(&'a u32, u32)>
}
impl<'a> Receiver<'a> {
fn set(&mut self, reference: &'a u32) -> ()
{
self.ref_and_val = Some((reference, *reference))
}
fn check(&self) -> ()
{
if let Some((reference, val)) = self.ref_and_val {
if *reference != val {
panic!("The impossible happened!")
}
}
}
}
fn valen_inner_fun<'a>(r: &mut Receiver<'a>, val: &'a u32)
{
r.set(val)
}
fn valen_intermediate_fun<'a>(r: &mut Receiver<'a>, val: &'a mut u32)
{
// In a real mixed-language example, this would be in Valen, not Rust,
// and it would declare a mutable effect on val.
valen_inner_fun(r, val);
// And it would do this, which Rust disallows.
*val = 17
}
fn main() {
let mut r: Receiver = Receiver::default();
let mut fortytwo = 42;
valen_intermediate_fun(&mut r, &mut fortytwo);
r.check()
}
This compiles except for the *val = 17 line. (rustc's error message is a bit confused, and maybe I'll file a bug about that. If one follow's rustc's advice, the weird error turns into a less weird error.)My point is that Receiver::set() requires a promise that its parameter's referent is immutable for the entire lifetime 'a, which exceeds the lexical duration of set() itself. And valen_outer_fun in Rust knows this to be true as a result of Rust's shared-xor-mutable system, and Rust's borrow checker verifies this:
a) the code would compile if I removed the offending *val = 17
b) the code, correctly, does not compile as written
c) If I force the issue by replacing *val = 17 with:
unsafe { *(val as *const u32 as *mut u32) = 17 }
then it panics and we can all imagine we're using C++ instead of Rust :)But valen_inner_fun does not mutate val, and if I understand right then this means that Valen would accept its Valen equivalent without mut(val), and this seems unsound to me.
When I first thought of this, I imagined set() instead being a work-dispatching function that would run its passed-in closure and hit UB due to a data race with the *val = 17 line, and my edit was my realizing that there's a simpler non-concurrent example.
I've occasionally contemplated whether Rust would benefit from another flavor of reference: no-access.
Most of the uses for this I can think of are best solved by opaque pointers for FFI. Having a rust-native reference just seems incongruous. Like, does it have size and alignment info? How would it interact with NLL? The only way I can imagine it working is if it extended referent lifetime throughout the lexical lifetime of the reference, but that defeats the purpose of NLL.Rust solves a similar problem in closures with unique immutable references, but they're not quite the same.
Unsafe Rust could promote a no-access reference to a shared or a mutable reference, and the unsafe code would be responsible for not violating exclusivity rules but would have a guarantee that the referent actually exists. Using unsafe code to create a no-access reference to a nonexistent object or to a misaligned object would be UB.
Safe code could convert the other way:
let a: &mut u32 = ...;
let b: &noaccess u32 = a;
let c: &u32 = ...;
let d: &noaccess u32 = c;
This is not an entirely serious proposal.So for example:
let mut data = vec!['a', 'b', 'c'];
let b: &noaccess [char] = &data[..];
data.push('d'); // Does this error?
Hence the questionYour example is sneaky, though. Lifetime issues aside (suppose the next line of code uses b), that’s a noaccess reference to memory (an object? a place? I’m not sure what the current term is) that is only guaranteed to exist so long as data is not mutated. So the code with a subsequent use of data would error.
But if it were instead:
let b: &noaccess = data;
Then it would not error.