-
Notifications
You must be signed in to change notification settings - Fork 20
ACP: Option::update_or_default
#575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
let mut opt: Option<String> = None;
opt.get_or_insert_default().push_str("Hello");
nit:
pub fn or_default_update<F>(&mut self, f: F)
where
F: FnOnce(&mut T),
T: Default, let mut opt: Option<String> = None;
opt.or_default_update(|s| s.push_str("Hello"));
You should provide some better examples that actually make use of your version passing it by value.
And if you're going to get a |
Before I want to clarify intention of this method, because it could be not clearly describe in the ACP: You're right that I very agree that Your suggestion for #[derive(Default, Debug, Clone)]
struct Config {
log_level: u8,
output_path: String,
}
impl Config {
fn merge(self, other: PartialConfig) -> Config {
Config {
log_level: other.log_level.unwrap_or(self.log_level),
output_path: other.output_path.unwrap_or(self.output_path),
}
}
}
struct PartialConfig {
log_level: Option<u8>,
output_path: Option<String>,
}
fn main() {
let mut config: Option<Config> = None;
let partial = PartialConfig {
log_level: Some(2),
output_path: None,
};
// With proposed API:
config.update_or_default(|c| c.merge(partial));
dbg!(&config); // Result: Some(Config { log_level: 2, output_path: "" })
// Current approach:
let c = config.get_or_insert_default();
*c = c.clone().merge(partial); // Clone is awkward and potentially expensive
// OR:
config = Some(config.take().unwrap_or_default().merge(partial)); // Verbose
} In a CLI application, a The transition method consumes the #[derive(Default)]
struct GameState {
score: u32,
level: u8,
}
impl GameState {
fn transition(self, event: Event) -> GameState {
match event {
Event::Score(points) => GameState {
score: self.score + points,
level: self.level,
},
Event::LevelUp => GameState {
score: self.score,
level: self.level + 1,
},
}
}
}
fn main() {
enum Event {
Score(u32),
LevelUp,
}
let mut state: Option<GameState> = None;
let event = Event::Score(100);
// With proposed API:
state.or_default_update(|s| s.transition(event));
// Result: Some(GameState { score: 100, level: 0 })
// Current approach:
state = Some(state.take().unwrap_or_default().transition(event)); // Verbose
// OR:
let s = state.get_or_insert_default();
*s = s.clone().transition(event); // Clone is inefficient
} You raised a great point about whether users want a Next StepsBased on your feedback, I propose:
|
This is related to rust-lang/rfcs#1736, you might want to see its discussion for why it is not easy to replace |
Since it returns For by-value updates, another alternative is opt.get_or_insert_default();
opt = opt.take().map(some_update_func); |
Thank you for your feedback! You're right that However, the main motivation behind |
Proposal
Problem Statement
Rust programmers often need to update an
Option<T>
by applying a transformation to its value, initializing it withT::default()
ifNone
, and storing the result back in theOption
. This pattern is common in configuration parsing, state machines, and incremental updates. Currently, this requires verbose boilerplate, such as:or:
These approaches are cumbersome, requiring multiple steps or temporary variables, which increases cognitive load and error risk (e.g., forgetting to update the reference). There’s no standard method to combine default insertion and in-place transformation in a single, ergonomic operation.
Motivating Examples or Use Cases
This pattern appears in real-world scenarios, such as:
Configuration Parsing:
Incremental String Building:
These examples, inspired by configuration handling in libraries like
serde
and incremental updates in CLI tools, show verbose multi-step operations that could be simplified.Solution Sketch
Add a new method,
Option::update_or_default
, tostd::option::Option<T>
:Usage:
The method combines default insertion and transformation, reducing boilerplate and aligning with ergonomic APIs like
HashMap::entry().or_default()
.Performance Notes
T::default()
only ifNone
. For types likeString
, this may allocate (e.g.,String::new()
). Users with expensive defaults can useget_or_insert_with
.f
may allocate (e.g.,push_str
onString
).take
,unwrap_or_default
,Some
) is allocation-free, adding no overhead beyondT::default()
orf
.Alternatives
Use Existing APIs:
get_or_insert_with
and manual mutation:take
andunwrap_or_default
:update_or_default
is more ergonomic, combining both steps into a single call.External Crate:
option-ext
could add this method, butOption
is a core type, and inclusion instd
ensures discoverability and consistency with APIs likeunwrap_or_default
.Generalized Method:
update_or_insert_with(default, f)
with separate default and transform closures is more flexible but less ergonomic for the commonT::default()
case. This could be a future extension.Links and Related Work
Option::unwrap_or_default
: ExtractsT
with a default, no in-place transformation.Option::get_or_insert_with
: Initializes but requires separate mutation.HashMap::entry().or_default().and_modify(f)
: Similar in-place update pattern.Map.prototype.set(key, map.get(key) ?? defaultValue)
for updating/initializing.std
.What Happens Now?
This issue contains an API change proposal (or ACP) and is part of the libs-api team [feature lifecycle]. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.
Possible Responses
The libs team may respond in various different ways. First, the team will consider the problem (this doesn’t require any concrete solution or alternatives to have been proposed):
Second, if there’s a concrete solution:
The text was updated successfully, but these errors were encountered: