As Rust is becoming more widely used, many different developers need to use it. There is huge variety in their requirements. System developers want to be able to fully control where allocations occur [1]. And the casual user wants to have as little friction as possible, not having to worry about the unlikely scenario that the operating system is unable to supply them with memory.
These goals are at odds with each other. For the time being I think that language development has
danced between these two worlds and tried to satisfy both equally. I feel like that this is going to
become increasingly difficult in the future. While adding fallible functions to Vec
has worked out
better than providing FallibleVec
[2], I still think having to use try_push
instead of push
in an environment where one is only allowed to use fallible allocation (the Linux kernel) is a
detriment to ergonomics. Especially when we look at worse examples like Box::try_new_uninit
. We do
not need Box::new_uninit
, as it panics, but we would like to reuse the name...
I have not done enough thorough research to find if this has been proposed already. This is because I have no idea what to search for. I will name this feature "profile" temporarily (yes it clashes with compilation profile, so it should have a different name, but I could not come up with a better one). There are probably many things that we could do different syntactically, please leave bike-shedding for later.
The solution
What if we solve this issue of having to cater to multiple diverging worlds at the same time? I think we could introduce so called "profiles". They can be used in an impl block like this:
impl<T> Vec<T> in @default {
pub fn push(&mut self, item: T) {}
}
I have used @
as a marker for profiles. The @default
profile is the default profile! It would
need to include the whole of std
for backwards compatibility. But now we could add some more
profiles:
pub profile no_std;
impl<T> Vec<T> in @no_std {
pub fn push(&mut self, item: T) -> Result<(), T> {}
}
With the magic of profiles, we can have the same function name, but different signature and functionality. While this change is not backwards compatible, I think it could be done over an edition. Also you are not locked out from using the function from another profile:
#![profile(no_std)] // this sets the default profile
fn main() {
let mut v = Vec::new();
v.push(42).expect("ran out of memory");
<Vec<i32> in @default>::push(v, 42);// this will panic if there is not enough memory
}
This feature would need extremely good documentation. I think every function that is only available in a specific profile should be annotated similar to how std::os::unix is. There should be some easily accessible drop down menu to select the current profile regardless of the current position.
Future extensions
There are many ways to extend this feature. For example adding #![deny_profile(default)]
which errors on <Vec<i32> in @default>::push()
.
Or allowing orthogonal profiles that can be enabled independently. Another more elaborate extension could be to add a profile
@implicit_alloc
and allow implicit allocations for dyn Future
s returned by dyn TraitWithAsyncFn
.
This could be a bit extreme, but on the other and also be exactly what Rust needs to be more accessible.
Disadvantages
More complexity for API designers:
- which profile should I choose?
- when should I create my own profile?
More possibility for bad APIs:
- why does this API use
@default
even though it is compatible with#![no_core]
?! - these two methods from the same profile are not even remotely related!
"Profile soup"
Users might have to enable too many profiles at the same time, so we would end up with:
#![profile(no_std, foo, bar, foo_and_bar, )]
Users can still enable profiles that conceptually do not fit together:
#![profile(no_std, implicit_alloc)]
We could of course first only allow stdlib to create profiles, preventing the profile soup and we could lint against them not fitting together. But the first two problems still exists...
Please let me know what you think. For the moment I think we should constrain this feature to implementing inherent methods only.