I have "builder" structs, whose job it is to prepare an actual "thing". For performance reasons, I often provide two implementations: a to_thing(&self)
[less efficient] and a trait From<Builder> for Thing
implementation, where the from(self)
consumes the builder [more efficient]. A recent example, abridged:
#[derive(Debug)]
pub struct DatasetSchema {
variables: Vec<Variable>,
ordinals: HashMap<String, usize>,
}
impl DatasetSchema {
#[inline]
pub fn builder() -> DatasetSchemaBuilder {
DatasetSchemaBuilder::new()
}
#[inline]
pub fn ordinal(&self, name: &str) -> Option<usize> {
self.ordinals.get(name).copied()
}
#[inline]
pub fn get_variable_at(&self, ordinal: usize) -> Option<&Variable> {
self.variables.get(ordinal)
}
#[inline]
pub fn variables(&self) -> &[Variable] {
&self.variables
}
}
#[derive(Debug, Clone)]
pub struct DatasetSchemaBuilder {
variables: Vec<Variable>,
}
impl DatasetSchemaBuilder {
#[inline]
fn new() -> Self {
Self {
variables: Vec::new(),
}
}
#[inline]
pub fn add_variable(&mut self, variable: Variable) {
self.variables.push(variable);
}
#[inline]
pub fn try_to_dataset_schema(&self) -> Result<DatasetSchema, Cow<str>> {
let mut ordinals = HashMap::with_capacity(self.variables.len());
for (ordinal, variable) in self.variables.iter().enumerate() {
let name = variable
.name()
.ok_or("A variable did not specify a name.")?;
if ordinals.insert(name.to_string(), ordinal).is_some() {
return Err(Cow::Borrowed("Encountered a duplicate variable name."));
}
}
Ok(DatasetSchema {
variables: self.variables.clone(),
ordinals,
})
}
}
impl TryFrom<DatasetSchemaBuilder> for DatasetSchema {
type Error = Cow<'static, str>;
fn try_from(value: DatasetSchemaBuilder) -> Result<Self, Self::Error> {
let mut ordinals = HashMap::with_capacity(value.variables.len());
for (ordinal, variable) in value.variables.iter().enumerate() {
let name = variable
.name()
.ok_or("A variable did not specify a name.")?;
if ordinals.insert(name.to_string(), ordinal).is_some() {
return Err(Cow::Borrowed("Encountered a duplicate variable name."));
}
}
Ok(DatasetSchema {
variables: value.variables,
ordinals,
})
}
}
In summary, there's some logic checking to making sure every variable has a unique name before returning the DatasetSchema
.
What bugs me is that the logic for constructing the ordinals
map (and guaranteeing unique names) is duplicated, more or less. I tried putting a method taking &self
on the builder to build the map, but the TryFrom
implementation complains I am calling a method when I'm "moving the value out".
One idea, since I am cloning variables
anyway, would be to implement try_to_dataset_schema
by doing something like this:
#[inline]
pub fn try_to_dataset_schema(&self) -> Result<DatasetSchema, Cow<str>> {
self.clone().try_into()
}
But before I go and make this change in 200 places, I thought I would ask if this is a good approach or if there's a more common pattern involving builders.
Also curious about my naming of things. Is try_to_blah
a good name? Suggestions welcome.