Now that you point it out, it seems sensible to allow user of the type to decide how the items will be polled (control concurrency), but I don't recall seeing Streams of Futures in public APIs.
i think most of the time public api have a type that encapsulates the stream and then offer an async function to get the next element or allow to setup a closure that will be called every time one of the underlying futures reaches completion, probably helps to hide implementation details and give a more readable hint to what the stream is.
Maybe an unpopular opinion, but I think a module or crate should never have foreign types besides the standard library in its public interface, even popular ones.
This would mean that you need very “thin” interfaces and gigantic crates. Maybe not QT-size “everything including the kitchen sink” gigantic, but certainly the exact opposite to the Rust crates ecosystem as we have it, today.
We already have this world in C++… and it's not all that rosy, with the exact same things duplicated dozen of times (each library would essentially bring copy of everything that's currently shared).
And I'm not even sure that having 10 crates each 100-200MB in size would be better from security POV than having 200-300-500 tiny crates.
(It does not matter if MyType resides in a sub module of the same crate.)
By foreign types I mean types that are defined outside the crate and are used in public interfaces.
Example:
use other_crate::ForeignType;
pub fn my_public_interface(...) -> ForeignType { ... }
Of course it is perfectly fine to use a foreign type internally, for example in private fields of your own public types:
use other_crate::ForeignType;
pub struct MyType {
data: ForeignType;
}
So:
Because of one of the oldest problems in computer programming: coupling.
When a crate exposes foreign types in its public interface, it couples its users to those types. If those types change in a breaking way, both the crate and its users must adapt.
When foreign types are used only internally, the coupling stops at the crate boundary. Changes still affect the crate itself, but not its users. This interrupts the cascading dependency chain.
Except, of course, there are no perfect solution to coupling. You may either have tight coupling and create tiny binary for Windows (or Android or MacOS) or you can apply loose coupling and pack the whole tricking another OS and produce “hello, world”, measured in megabytes.
TANSTAAFL: with loose coupling you may freely change you components, but these components grown in size, become heavy and sooner or later, turn into mostrocities (QT download, these days is over 100MB in size, and that's in packed form!).
True, but price is heavy: without ability to reuse large, complicated APIs every library develops its own vocabulary types. And then you either have to translate from one DSL to another DSL, or absorb more and more functionality to avoid that translation.
It's probably not a good idea to splinter your code into thousands of crates NPM-style, but attempt to avoid foreign types in crates interfaces would lead to the C++ end of the spectrum… and that one is not a very joyful one, too.
I think, bringing it back to the original topic, that it's simply a bit odd to half-expose details of the implementation like this.
Normally you're either exposing the source stream and the functionality you can map over it because you're a library of helpful Lego bricks of functionality, or you're making a bunch of calls on implementation practicality so it "just works".
I think if you're confident that the caller will want to be able to pick different buffering sizes, you should make it an option or required parameter in some sense, to keep usage simpler.
Because now you have to provide all the APIs that ForeignType had. And you have three options: one bad, one that's even worse, one that's awful (but that's what happens in the end).
The bad one: provide wrappers/forwarders that provide all these APIs that can be used with a ForeignType for your struct A. Result: you have achieved nothing, your API still depends on ForeignType, only now with time lag where you manually add them to your forwarders. Since that loses the benefits of loose coupling that's rarely the stage where people stay.
The next step is worse one: develop another API that's completely indenpent from ForeignType. Now developer of your library would need to learn two kinds of APIs: the one of ForeignType and the one you are providing for the end user.
This leads to attempts to “simplify” things and awful solution — because the easy way to do that is to pull ForeignType into your library and “cut it to the size”… but then other users of the ForeignType would do the same, too, they would just mangle it in a different way!
The end result: all the functionality is implemented in all the libraries, you have to learn many different forks of APIs related to the ForeignType… and, naturally, would try to reduce number of libraries that you use to reduce cognitive load.
This leads to even more code duplication as people are duplicating functionality in every library.
The end result: you binary comes with with dozens of implementations of every structure and also carries lots of code that's not even needed but it's there because someone, somewhere, may need it (and that's why it was included in the library).
Sure, you have achieved your beloved “loose coupling”… but was the price that you paid justified?
It is not about not using ForeignType at all. It is about users of a module, library, or API not transitively depending on ForeignType.
Additionally, by wrapping a ForeignType, its interface can be tailored to the specific usage in an API. This enhances the consistency of the API.
It is quite common that different solutions with different APIs exist and compete for the same problem.
This is especially true in the Rust ecosystem, where it is somewhat encouraged that multiple packages are available on crates.io. Even the philosophy of the standard library is to provide only some fundamentals. For example, there is no random number generation in std. This is in contrast to languages like Go, where the standard library is very broad.
I think this conclusion is wrong because if a crate with ForeignType is used internally by an API, and the user of this API also imports ForeignType directly, this does not result in code duplication. The crate that provides ForeignType is only linked once in the final binary.
Additionally, in Rust, wrappers are often very thin and effectively free because they exist only at compile time. It is quite normal in Rust to have such wrappers. They are advocated in many educational texts and widely used across many crates.
You made way too many typos in the world “reduces”. If there was one and now there are two then consistency is reduced, not “enhaned”. Now you have two different APIs and have to remember what's where.
That's before it's cloned and changed.
That's news to me. What's normal and encouraged it to reexport foreign types, then users of your library are isolated from changes in version of a ForeignType.
Of oourse if ForeignType is used to implement something radially different, when you type not just wraps ForeignType, but adds many different types together and provides some functionality on top of that, then exporting ForeignType would be silly, it's just an implementation detail, in that case – but I've never seen a recommendation to wrap some type solely to ensure that it can not be used as ForeignType in the caller. That's simply wrong.