Sketch of Vec, Slice and Array -> Iterator: Helpful? Clear?

Thank you! Yes. I misspoke - I'll make the change in the post itself to clarify.

The arrows, as presented anyway, did not get a lot of love. This next version is in a table format.
Better?

This is same in Python, too. There is built-in iter function that returns an iterator for passed object, and then you are calling the next function on the returned iterator for getting elements. The IntoIterator trait was strange for me until I noticed that.

2 Likes

Thanks to, and informed by the feedback provided to-date, I have provided an updated version that describes the "entry-points" for leveraging the Rust iterator infrastructure.

Last updated Feb 13, 2021

While this seems to provide a reasonably complete view/starting point of the "moving parts", whilst touching on the decisions and implementations need to be made for a hypothetical MyCollection, this does not capture the trade-offs of each option, and in context of the move vs borrow semantics.

It doesn't really matter which is implemented in terms of what. That could be changed tomorrow and nothing would be different from a user's perspective. I just meant they do the same thing.

There is a sense in which IntoIterator is more important, because if IntoIterator didn't exist then you wouldn't be able to use it in generics; whereas if .iter() didn't exist you would just have to use (&foo).into_iter() instead and it would be a little bit more verbose and annoying but not really less expressive in any way. But since both exist it doesn't matter which one is implemented in terms of the other.

Hmm, sorry, I know C pretty well but I'm not really sure what this question means. I think you might be confusing &str with str? They are distinct types. String is a pointer to str, &str is another kind of pointer to str. Vec<T> is similarly a pointer to [T].

The problem (?) with FullFeatured<T> and DataOnly<'_, T> is that FullFeatured<T> is not a pointer to DataOnly<'_, T>. You couldn't implement Deref for FullFeatured so that it points to DataOnly (or vice versa). This doesn't mean they're not useful types, they're just not related to each other the way Vec<T> and [T], or String and str are.

Deref is fundamentally a trait that is implemented for pointers, or things that are in some way pointer-like (there's some disagreement around what exactly should be Deref; here's an earlier thread on that subject). Deref doesn't say anything about serialization or equality or what other data the struct might have... it basically just says "this struct points to something". Vec<T> implements Deref because it's a (smart) pointer, and the thing it points to is [T]. Since it implements Deref, you can use . to call methods of [T] on Vec<T>, the same way you can call methods of [T] on &[T]; they're both pointers.

The author meant IntoIterator.

If Collection<T> can implement IntoIterator, do that. If you can implement IntoIterator for &Collection<T> and/or &mut Collection<T>, with the semantics one would expect, you should do that too. If there are other ways of iterating over your collection, write inherent methods, like Vec<T>::drain (which yields Ts by emptying, not consuming, the Vec).

The other "half" of the interface between collections and iterators, which hasn't been mentioned directly in this thread yet, is FromIterator<T>, which is the trait that enables Iterator::collect. You implement FromIterator<T> for Collection<T> to be able to write things like let c: Collection<T> = some_iterator.collect(). I suppose you could say that FromIterator<T> and IntoIterator are what Rust has instead of a high-level Collection interface like in Java.

DerefMove (if it existed) could take self in the same way DerefMut takes a &mut self; for example:

trait DerefMove: DerefMut {
    type Output;
    fn deref_move(self) -> Self::Output;
}

Box does not need this because the knowledge of how to move things out of a Box is actually coded within the compiler itself. You can call .into_iter() on a Box<Vec<T>>. But this isn't possible for other types.

2 Likes

That's a great comment that took me a while to connect with what I'm realizing is a "bigger" learning from all this.

While you make the parallel with what's going on in .Net, does the Rust "infrastructure" accomplish anything noteworthy? (i.e., the use of traits, inherent methods and the dot operator)

On a related note, I was thinking that whatever was accomplished here, could be considered as a more general pattern for unifying two inherently incompatible types. In the case of iterating over MyCollection, I need a separate struct to host the state of the iterator; in that sense MyCollection and MyIterator are "incompatible". More generally, another example might be the challenge often faced with self-referencing data structures. The solution (other than Box) can be to create two structs to accomplish the task. The usual downside is then how to "unify" them for the user.

Am I off base with this train of thought?

I'm not sure what you mean by "noteworthy". Rust's traits are quite close to Haskell's type classes, for example.

Ah, you have gotten too used to Rust! Has the magic of it left you :)) ?

I would include how Rust uses traits despite the fact that it was proved useful in Haskell.

So while the pattern seems to be used in .NET, is the user experience of it (if you will), particularly impressive in your opinion?

For instance, one hassle with a strongly typed system is the ability to reuse logic where overlap exists between types. What is helpful is coercion and other forms of casting. Facilitating where it makes sense makes the type system that much more useful (in practice).

Great point. What I’m thinking about is a way to consider data and mutable views of the data. To be clear, read-only data, only able to mutate some synthesis/transformation of the data.

OO has you think data and methods under the “same roof”. In Rust, the borrow checker makes it challenging to ref self. It’s a contrived need but somehow conceptually compelling.

FP is all about function composition where the syntax is only ergonomic in something like Haskell. JS function chaining works nicely because it abstracts over what I love about Rust.

However, is there a way to otherwise unify the mutable view and data? Perhaps more like how Iterator and IntoIterator? That’s perhaps the best of both? Best way to align the most appealing design, keeping the borrow checker happy with a robust/clean ergonomic syntax.

I think this QotW is a good explanation of it:

The magic of rust is the combination, not really any of the individual things themselves.

I conquer. However, you did not answer my question :)). Part of what I like about Rust is how it’s both not OO nor FP, but can also be OO and FP. Rust isn’t limiting how I think about my abstractions and design, I am! I’m my own worst enemy :))

I suspect/wonder if there is not a pattern, or something in the Iterator and IntoIterator combination that I should be looking at to get me to “think differently”. You said you saw this in .NET. Iteration is fundamental, so at one level “of course” you see it elsewhere. I’m wondering if from your experience if you see anything else that maybe only Rust accomplished. Do you know what I mean?

Updated versions of the charts... A theme that comes to mind is both the amount of type inference made possible and how it's utilized to provide an ergonomic MyCollection/Iterator capacity.

Ideas for the key message/observation are welcome in addition to general impression of usefulness.

last updated Feb 16, 2021

This includes the feedback thus far provided.

Any suggestions on how to make this more accessible?

Here is the final version of the documentation. Thank you to everyone for providing their constructive feedback.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.