-
Notifications
You must be signed in to change notification settings - Fork 34
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
Makes List
and SExp
thin wrappers around Sequence
#502
Conversation
See issue #500 for discussion. This PR removes the `IonSequence` trait and reintroduces a concrete `Sequence` type. It also converts `List` and `SExp` into tuple structs that wrap `Sequence`. `ListBuilder` and `SExpBuilder` have also been removed in favor of a single `SequenceBuilder` implementation with `build_list()` and `build_sexp()` methods. `List` and `SExp` each implement `Deref<Target=Sequence>`, allowing them both to expose the functionality of their underlying `Sequence` of elements transparently. This eliminates a fair amount of boilerplate code that was needed to provide functionality for both types. It also enables unified handling for `List` and `SExp` in match expressions in situations where the author does not care about the Ion type distinction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗺️ PR tour
/// assert_eq!(actual, expected); | ||
/// ``` | ||
pub struct ListBuilder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗺️ The distinct ListBuilder
and SExpBuilder
types (which were mostly duplicate code) have been deleted in favor of a unified SequenceBuilder
type.
/// let actual: SExp = Sequence::builder() | ||
/// .push(1) | ||
/// .push(true) | ||
/// .push("foo") | ||
/// .build_sexp(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗺️ While Sequence::builder()...
followed by build_list()
or build_sexp()
works just fine, I expect most users to continue reaching for the ion_list!
and ion_sexp!
macros, which now use SequenceBuilder
under the hood.
/// Builds a [`Sequence`] with the previously specified elements. | ||
pub fn build(self) -> Sequence { | ||
self.values.into() | ||
} | ||
|
||
/// Builds a [`List`] with the previously specified elements. | ||
pub fn build_list(self) -> List { | ||
List(self.build()) | ||
} | ||
|
||
/// Builds a [`SExp`] with the previously specified elements. | ||
pub fn build(self) -> SExp { | ||
SExp::new(self.values) | ||
pub fn build_sexp(self) -> SExp { | ||
SExp(self.build()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗺️ The unified SequenceBuilder
type now offers build()
, build_list()
, and build_sexp()
methods that construct the expected types.
// These `From` implementations allow a builder to be passed into any method that takes an | ||
// `Into<Element>`, allowing you to avoid having to explicitly call `build()` on them when | ||
// the intent is clear. | ||
|
||
impl From<ListBuilder> for Element { | ||
fn from(list_builder: ListBuilder) -> Self { | ||
list_builder.build().into() | ||
} | ||
} | ||
|
||
impl From<SExpBuilder> for Element { | ||
fn from(s_expr_builder: SExpBuilder) -> Self { | ||
s_expr_builder.build().into() | ||
} | ||
} | ||
|
||
impl From<StructBuilder> for Element { | ||
fn from(struct_builder: StructBuilder) -> Self { | ||
struct_builder.build().into() | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗺️ These From
implementations were introduced before the collections macros were available. They made things slightly less verbose when passing off a builder to something that expected an Into<Element>
.
For example:
// Old, pre-macro code
fn do_something(element: impl Into<Element>) {
// ...
}
// Pass the builder off without calling `build()`
do_something(List::builder.push(1).push(2).push(3));
Now that we're using a single type for constructing List
and SExp
(that is: SequenceBuilder
), we can't correctly infer which sequence type the user wants to construct without additional information. However, we also have the macros now which are even more concise, so I'm removing this shortcut.
// New code
fn do_something(element: impl Into<Element>) {
// ...
}
do_something(ion_list![1, 2, 3]);
src/element/list.rs
Outdated
impl Deref for List { | ||
type Target = Sequence; | ||
|
||
fn deref(&self) -> &Self::Target { | ||
&self.0 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗺️ This impl allows users to transparently access the underlying Sequence
's methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deref
polymorphism is considered an anti-pattern. Can you explain why you went with this approach instead of any of the alternatives?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I discussed this in our design thread on issue 500.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I missed that. I'll go take a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just pushed a commit to replace Deref
with delegate!
. The result is slightly more verbose, but it's probably worth it to sidestep potential controversy.
pub fn as_sequence(&self) -> Option<&Sequence> { | ||
match &self.value { | ||
Value::SExp(sexp) => Some(sexp), | ||
Value::List(list) => Some(list), | ||
Value::SExp(SExp(s)) | Value::List(List(s)) => Some(s), | ||
_ => None, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗺️ Notice:
- Accessing the contents of the
List
orSExp
no longer requires dynamic dispatch. 🎉 - Handling for
SExp
andList
can occur in the same arm of thematch
.
@@ -110,7 +110,7 @@ | |||
//! ``` | |||
//! use ion_rs::IonResult; | |||
//! # fn main() -> IonResult<()> { | |||
//! use ion_rs::element::{Element, IonSequence, IntoAnnotatedElement, Value}; | |||
//! use ion_rs::element::{Element, IntoAnnotatedElement, Value}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗺️ Users no longer have to import the IonSequence
trait in order to access the basic functionality of List
and SExp
.
delegate! { | ||
to self.0 { | ||
pub fn clone_builder(&self) -> SequenceBuilder; | ||
pub fn elements(&self) -> ElementsIterator<'_>; | ||
pub fn get(&self, index: usize) -> Option<&Element>; | ||
pub fn len(&self) -> usize; | ||
pub fn is_empty(&self) -> bool; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I actually quite like this.
values: Vec<Element>, | ||
} | ||
|
||
impl SExpBuilder { | ||
/// Crate visible; users should call [`SExp::builder()`] or [`Element::sexp_builder()`] instead. | ||
impl SequenceBuilder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we parameterize SequenceBuilder
, the return type of build()
can be set when the SequenceBuilder is created so that you can't write List::builder().build_sexp()
.
struct SequenceBuilder<T> {
_target_type: Phantom<T>,
values: Vec<Element>,
}
impl <T> SequenceBuilder<T> {
/// Crate visible; users should call [`Sequence::builder()`] instead.
pub(crate) fn new() -> Self {
Self {
values: Vec::new(),
_target_type: Phantom::default()
}
}
/// Helper method for [`Sequence::clone_builder()`].
pub(crate) fn with_initial_elements(elements: &[Element]) -> Self {
let mut new_elements = Vec::with_capacity(elements.len());
new_elements.extend_from_slice(elements);
Self {
values: new_elements,
_target_type: Phantom::default()
}
}
// etc.
}
// Repeate for Sequence and SExp
impl SequenceBuilder<List> {
pub fn build(self) -> List {
List(self.values.into())
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would work, though I think my current leaning would be to simply eliminate the
List::builder() -> SequenceBuilder
andSExp::builder() -> SequenceBuilder
synonyms in favor of the (already available) Sequence::builder() -> SequenceBuilder
method.
The doc comment examples for List
and SExp
can demonstrate using Sequence::builder()
to construct them, and most users will reach for the macros anyway.
An interesting (minor) benefit of using the same builder type for all sequences is that I can do:
let sexp = ion_sexp!(1 2 3);
// I can make a List using the SExp's elements as a starting point
let list = sexp.clone_builder().build_list();
It's possible that we'll want to introduce a Stream
sequence type--values with no container, which are used at the top level and (in Ion 1.1) template expansion. We would also be able to use SequenceBuilder
for that.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An interesting (minor) benefit of using the same builder type for all sequences is that I can do:
let list = sexp.clone_builder().build_list();
We could use the generics so that List::builder()
only lets you build a list, but list.clone_builder()
will let you build a List
, SExp
, or Sequence
. E.g.:
impl SequenceBuilder<Cloned> {
pub fn build_list(self) -> List {
List(self.values.into())
}
pub fn build_sexp(self) -> SExp {
List(self.values.into())
}
// Etc.
}
That being said, I think the solution you've proposed is fine (and certainly simpler).
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## main #502 +/- ##
==========================================
+ Coverage 89.58% 89.60% +0.02%
==========================================
Files 75 80 +5
Lines 13894 13965 +71
==========================================
+ Hits 12447 12514 +67
- Misses 1447 1451 +4
... and 1 file with indirect coverage changes Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report in Codecov by Sentry. |
See issue #500 for discussion.
This PR removes the
IonSequence
trait and reintroduces a concreteSequence
type. It also convertsList
andSExp
into tuple structs that wrapSequence
.ListBuilder
andSExpBuilder
have also been removed in favor of a singleSequenceBuilder
implementation withbuild_list()
andbuild_sexp()
methods.List
andSExp
each implementDeref<Target=Sequence>
, allowing them both to expose the functionality of their underlyingSequence
of elements transparently.This eliminates a fair amount of boilerplate code that was needed to provide functionality for both types. It also enables unified handling for
List
andSExp
in match expressions in situations where the author does not care about the Ion type distinction. Finally, users no longer have to importIonSequence
in order to access the basic functionality ofList
andSExp
.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.