In our codebase, we have the following type:
pub enum ScalarValue {
Bytes(Vec<u8>),
Str(String),
Int(i64),
Uint(u64),
F64(f64),
Counter(i64),
Timestamp(i64),
Cursor(OpId),
Boolean(bool),
Null,
}
We want to create another type that is similar to a Vec<ScalarValue>
, but that enforces the constraint that all of its members are of the same type.
To do that, we've done the following:
pub enum ScalarValues {
Bytes(Vec<Vec<u8>>),
Str(Vec<String>),
Int(Vec<i64>),
Uint(Vec<u64>),
F64(Vec<f64>),
Counter(Vec<i64>),
Timestamp(Vec<i64>),
Cursor(Vec<OpId>),
Boolean(Vec<bool>),
// length only (nulls cannot differ)
Null(usize),
}
One issue we've run into is, how to implement methods on this wrapper type in a way that isn't super repetitive.
For example, right now, len
is implemented like this:
pub fn len(&self) -> usize {
match self {
ScalarValues::Null(len) => *len,
ScalarValues::Bytes(xs) => xs.len(),
ScalarValues::Str(xs) => xs.len(),
ScalarValues::Int(xs) => xs.len(),
ScalarValues::Uint(xs) => xs.len(),
ScalarValues::F64(xs) => xs.len(),
ScalarValues::Counter(xs) => xs.len(),
ScalarValues::Timestamp(xs) => xs.len(),
ScalarValues::Cursor(xs) => xs.len(),
ScalarValues::Boolean(xs) => xs.len(),
}
}
get
is similarly repetitive. Note that get
returns an Option<ScalarValue>
since this type, from the outside, behaves just like/very close to a Vec<ScalarValue>
.
Our append method is also similarly repetitive, although here the repetitiveness is more justified since we need to check whether the incoming ScalarValue
matches the current variant of ScalarValues
:
/// Add a ScalarValue to a ScalarValues
pub fn append(&mut self, v: ScalarValue) -> Result<(), InvalidMultiSetValues> {
Ok(match (self, v) {
(ScalarValues::Bytes(xs), ScalarValue::Bytes(x)) => xs.push(x),
(ScalarValues::Str(xs), ScalarValue::Str(x)) => xs.push(x),
(ScalarValues::Int(xs), ScalarValue::Int(x)) => xs.push(x),
(ScalarValues::Uint(xs), ScalarValue::Uint(x)) => xs.push(x),
(ScalarValues::F64(xs), ScalarValue::F64(x)) => xs.push(x),
(ScalarValues::Counter(xs), ScalarValue::Counter(x)) => xs.push(x),
(ScalarValues::Boolean(xs), ScalarValue::Boolean(x)) => xs.push(x),
(ScalarValues::Null(xs), ScalarValue::Null) => *xs += 1,
(values, v) => {
return Err(InvalidMultiSetValues::MixedTypes(
format!("{:?}", values),
v.to_string(),
))
}
})
}
I was wondering if there are more idiomatic ways to handle this issue. I was thinking maybe something could be done where I use a library like strum to generate a ScalarValueKind
enum and then have a struct of the form:
struct ScalarValues {
vec: Vec<???>, // not sure what goes here
kind: ScalarValueKind
}
impl ScalarValues {
pub fn append(&mut self, v: ScalarValue) {
if std::mem::discriminant(self.kind) == std::mem::discriminant(v.as_kind()) {
// somehow append the value
}
}
but this is pretty hazy.
Does anyone have thoughts on this?
Update: I realized one obvious solution that I've been missing here is:
struct ScalarValues {
vec: Vec<ScalarValue>,
kind: ScalarValueKind
}
impl ScalarValues {
fn append(&mut self, v: ScalarValue) {
// do the check here
}
}
This solution feels a bit slow though since we have to store a useless Vec<ScalarValue::Null>
(when in the above example we optimize it to a single integer).