Specialization on stable

I am wondering what options we have to do specialization-like things on stable Rust. For example, consider the tokio::io::split utility, which splits an AsyncRead/AsyncWrite into a read half and a write half. Since the underlying read/write methods are &mut self, it needs to protect access to the halves with a lock.

However, if the underlying type is TcpStream, then there are special ways to perform reads or writes without taking the lock.

Is there an easy way to check whether a generic type is some specific type?

if is_tcp_stream::<T>() {
    return self.read_tcp_stream(self.transmute_to_tcp_stream());
}
// generic impl goes here

I know there is std::any, but we don't have a T: 'static bound on the generic.

3 Likes

I've heard rumors that it might be relying on what is essentially a soundness bug, but there is this:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0994ca6a76e17a7ba7669d90fb3c9af0

2 Likes

Not a direct answer to the question, but it seems that the problem is solvable with proper abstraction instead as you control the abstraction traits. I guess the problem can be abstracted as "do this if ops are supported on shared ref, otherwise lock it and do that with mutable ref" which may be represented as following new method to trait AsyncRead like:

// TODO: use better name
fn try_read_from_shared(&self) -> Option<Result<...>> { None }

with a convention that returning None signals it did nothing significant. Since IO traits already are driven by convention a lot(like returning Ok(0) means EOF) adding another optional convention for optimization doesn't hurt much I believe.

just rant about specialization

Yes, I'm generally against with specialization. It should be considered a last resort. While it seems quick fix for pinpoint optimization it have fundamental problem which can lead quite counterintuitive situation - it doesn't compose. It's well known that &str have specialized ToString implementation. But does it also apply when you have &&str? Nope.

4 Likes

I think the IDET trick (or some variants of it) can also be used in this situation.

good point. I also feel that once I start to use specialization, I might soon develop a habit of relying on it, and reluctant to think in proper abstractions.

2 Likes

Here's another potential use-case. Consider tokio::fs::write. It has this signature:

pub async fn write(
    path: impl AsRef<Path>,
    contents: impl AsRef<[u8]>
) -> Result<()>

Internally, it clones contents into a new Vec<u8> and moves it to a background thread where it actually gets written. Now, if contents is actually an Vec<u8> or String, then we've just made an unnecessary copy of the contents.

Can we use some sort of specialization here to avoid the copy?

Obviously, the real fix is to use something different than AsRef<[u8]>, but that's a breaking change, so I can't do that.

For tokio::io::split, since you control the trait(s) AsyncRead/AsyncWrite, you could add a hidden&sealed&default-implemented additional method to AsyncRead or AsyncWrite to determine whether or not you have TcpStream.

This exact specialization desire is why I personally quite dislike functions taking impl AsRef — the receiving function can only use the value by-reference but accepts it by-value. Taking impl AsRef<Borrowed> + Into<Owned> has some legitimate use cases but it basically doesn't get used for that.

Calling std::fs::write::<&Path, String> is essentially a wrong usage of the function, because it doesn't (and can't) do anything with the ownership of the string other than drop it; you should call std::fs::write::<&Path, &String> instead for the low cost of a single additional source character (and remembering you need to do so).

The fs::write API gets off a bit easier, since it's explicitly just a convenience API around try { File::create(&path)?.write_all(&contents)? }. But the other fs functionality, much less so.

Historically, IIRC, the reason AsRef works the way it does is that someone thought it looked nicer to be able to write fn open<P: AsRef<Path>>(path: P) instead of fn open<P: AsRef<Path> + ?Sized>(path: &P). We're still eating the downsides of this choice for aesthetics over semantic ownership today.

4 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.