This is mostly a vibe-check on the idea.
Motivation
Basically, this idea is meant to allow following usage
impl Foo {
pub fn wham(&mut self) {
for baz in &mut self.baz {
if self.has_bar() {
// ^^^^ error with current rust
*baz += "1";
}
if let Some(bar) = self.first_bar_mut() {
// ^^^^ error with current rust
*bar += baz;
}
}
}
fn has_bar(&self) -> bool {
!self.bar.is_empty()
}
fn first_bar_mut(&mut self) -> Option<&mut String> {
self.bar.first_mut()
}
}
This comes up a lot when writing helper functions. And currently you need to either:
- Somehow split you struct into smaller structs. But
- This is tedious since it will impact basically all your code.
- It's not always possible to do it (reasonably).
- Just inline the function manually.
- This can get quite verbose and unmaintable.
- As soon as helper function body is longer than 5 tokens, the readablity suffers.
Idea
Transparent function calls (TFC) let borrow-checker see through private function calls, using it's body to borrow check instead of it's signature.
..and private function calls could means one of below:
- All calls to all private functions.
- All calls to private functions with a certain attribute (
#[bikesheld_transparent]) annotated. (This is my perference.) - All calls to any self crate defined functions with a certain attribute annotated.
- Add a new function calls syntax to private functions.
Maybe there's some more variants, but the core idea is downstream crate can't see through, so there's no accidental public api breakage.
The called function itself is still a regular function.
Difference to Previous Similiar Ideas
Following ideas also solves the problem showed in the motivation part. The main difference is that, transparent function calls are much more limited, and introduce minimal new syntax.
View Types
View types is a better solution for public facing apis or traits, but it introduce much more complexity to the type system, so it's inheritly harder to move forward. And we really don't want to annotate view type for helper functions, for obvious reasons (versatility, verbosity, etc.).
Macro Functions
Macro functions's scope is much larger, and is somewhat controversial due to it's template-esque nature. TFCs are not intended to support any duck typing use cases, or any type inference.
Lifetime inference for non-public items
Actually the motivation of this transparent function calls post. And the most similiar one. Core difference is tfc doesn't try to infer anything, and only affect function items.
Misc details
- To handle recursive TFC, I imagine some kind of inlining happening at MIR level. Haven't think through all these but I think it's doable.
- Whatever the restriction is, TFC cannot happen across crate.
- Whether trait methods of local type could be called transparently is undecided yet. I lean on yes if the implementation is local as well, but it's complicated.
Further Extensions
These extensions are what I think is reasonable to add as well. But the TFC can live without them. So I did include them in the main idea.
#[bikesheld_force_transparent]
You can annotate a private (or non-pub?) function with #[bikesheld_force_transparent]. This makes the function not implementing Fn family traits, and is always called transparently. Example:
#[bikesheld_force_transparent]
fn longest(left: &str, right: &str) -> &str {
if left.len() > right.len() {
left
} else {
right
}
}
Note that since it's always called transparently, you could omit lifetime constraints on signature.
Variadic Lifetime Parameter Elision Syntax
Introduce '.. (bikesheld syntax) as a shorthand for any replication of '_ parameter in function signature. Examples:
fn foo(r: Ref<'.., T>) {} // Ref<'a, T>
fn bar(c: Ctx<'..>) {} // Ctx<'env, 'scope>
fn bad1(a: Ctx<'env, '..>) // Error: `'..` cannot be used along side other litime
fn bad2(a: Ctx<'.., 'scope>) // Error: `'..` cannot be used along side other litime
fn degenerate(s: String<'..>, v: Vec<'.., i32>) // Ok. There's zero `'_`.
This is meant to solve "add a reference field in one struct, every function got infested" problem. Maybe it deserve a seperate RFC.
End
This is a rough idea that I've had for quite a bit of time. Is this a total no-go? Or it's a too limited to be useful? Or it should add even more limitations? I'd love to hear some feedback!