Giter Club home page Giter Club logo

Comments (1)

sdleffler avatar sdleffler commented on May 25, 2024

This issue persists out-of-the-box with a fairly small class of types:

  • &str as String
  • &CStr as CString
  • &OsStr as OsString
  • &Path as PathBuf
  • &[T] as Vec<T> (for some restrictions on T, depending)

For all of these cases, the issue is that it is considered idiomatic Rust to pass around references when possible rather than dealing with the original, non-Copy type. But a Dialectic session type is highly unlikely to refer to the str or Path or [T] types rather than the String or PathBuf or Vec<T> types; which means that if a string slice is being passed around but a session type calls for a String, the &str must be copied into a newly allocated String. This is not only a performance issue but also unergonomic, although the argument can be made that if this is necessary, the user can just carry around an &String instead.

However, that makes little sense. The extra information afforded by &String over &str, &Vec<T> over &[T], etc. is strictly capacity information needed for modifying the buffer. Especially for these types, the wire format for each should be identical. Given this, there seems like a wider class of potential feature here: the ability to send some type T for a given type U which is in the session type specification, where we can be certain that T and U have the same wire format and that sending a T to be received as a U will, for some guarantee, result in an equivalent U on the other side.

Actually specifying this guarantee is nontrivial, but we can lean on a similar guarantee which is needed for Rust's HashMap. The std::borrow::Borrow trait mandates that if you have some type T which implements Borrow<U>, then Eq, Ord, and Hash must be equivalent for T and U; so, for some t1, t2: T, (t1 == t2) == (t1.borrow() == t2.borrow()), and similar for Ord and Hash. We could use Borrow, then, to express the relationship between a type which can be transmitted as equivalent to another type; for some T which may be transmitted as U, the result of sending some t: T over the wire and receiving it as a u: U must be equivalent when borrowed (t.borrow() == &u, and similar for Ord and Hash.) This is not perfect but it's about as close as we can get to saying "the values must come out identical."

It is not possible for a type T to be sent and correctly be guaranteed to come out the other end as a type U, without the backend knowing that it is a T being sent as a U. In particular, the mpsc backend will treat this as a mismatch between session types, and panic when it finds a Box<Any> containing a T rather than a U. It will have no way to do the conversion on its end; the conversion must be done during transmitting.

So given this, there are a few solutions that we've come up with so far:

1.) Mandate that for a specific set of special-cased types (str as String, [T] as Vec<T> for T: Clone) all backends implement this extra ergonomic nicety; encode it as a trait TransmittableAs; and then allow it to be known that for any backend which transmits, say, String, you must also be able to transmit a str and have it come out as a String on the other side.)
2.) Add a parameter U to the Transmit<T, Convention, U> trait, which is the type actually being transmitted. Accept the type U as the parameter to a send/send_ref call, and allow the backend to explicitly specify what types can be sent as what.
3.) Add an associated type ReceivedAs to the Transmit<T, Convention> trait. Ensure that for any given type T, there is exactly one type that it matches in the specification. For example, str would be Transmit<str, Ref, ReceivedAs = String> on a given backend. This has the upside that there is no need to specify what type is being received as what on a given backend-generic function. However, it destroys the ability to blanket-implement for a backend over all T, though there are some ways to get around this.

Each of these has its own advantages and drawbacks.

Option 1.) Mandate a specific set of special-cased types

This is the simplest option, and the easiest to deal with for Dialectic. We can do this by using a sealed trait which declares that all T can be sent as T, and then allowing the special cases to be sent where some U they are compatible with is present in the session type. This would be done by amending the types of Chan::send and Chan::send_ref. This works *only in the case where the types we're sending are !Sized and the types we're receiving are Sized; this is because in a case like the mpsc backend, a blanket impl over Transmit<T, Val> for all T: Sized types would allow implementing Transmit<str, Ref> as str being unsized is enough for Rust to declare the impls distinct.

This is also the nastiest option to deal with for backend implementors. All of a sudden, backend implementors have to be aware that if their backends are capable of sending a str (by reference of course) then that must be compatible. Though whether this is actually a problem is debatable.

Option 2.) Add a parameter to Transmit denoting receive-equivalence

This option is one of the two which allows a backend implementor to be aware of what type is being sent as what. This trivially fixes the issue with mpsc because now the mpsc backend can be aware that we're trying to send a str as a String, and perform the allocation before adding it to the queue. Unfortunately blanket impls are still out of the question.

This also allows backend implementors to specify their own receive-equivalent types. Whether this is a plus or a minus is up for debate.

Another downside of this option is that for backend-generic functions, this will have to be specified as part of the Transmit trait bounds and thus the Transmit attribute macro will need syntax for it. Regrettably.

Option 3.) Add an associated type to Transmit denoting receive-equivalence

Given this piece of toy code, Rust is happy with these two impls being distinct:

trait Transmit<T: ?Sized, Convention> {
    type ReceivedAs: Sized;
}

impl<T, Convention> Transmit<T, Convention> for () {
    type ReceivedAs = T;
}

impl<Convention> Transmit<str, Convention> for () {
    type ReceivedAs = String;
}

Modifying the send and send_ref methods becomes fairly simple with this, and it has the major upside(?) of ensuring that ANY type can only be sent in place of a single other type. As the ReceivedAs type is uniquely specified by T, Convention.

However, just like option 2, we'd need to add syntax to the Transmit macro.

Option 4.) Mandate that all Sized types are sendable as themselves, and give unsized types the ability to choose what they'll be sent as

As I was writing this, I came up with a middle ground between options 1 and 3.

The distinction between Sized and !Sized is enough for Rust to consider impls distinct for a blanket impl over T: Sized and specific impls where the same position T is unsized. For example:

trait Transmittable {
    type ReceivedAs;
}

impl<T: Sized> Transmittable for T {
    type ReceivedAs = T;
}

impl Transmittable for str {
    type ReceivedAs = String;
}

impl<T> Transmittable for [T] {
    type ReceivedAs = Vec<T>;
}

trait Transmit<T: ?Sized, Convention>
where
    T: Transmittable,
{
    // ...
}

This scheme would allow the Transmittable trait to remain open rather than being sealed, and specify behavior in the case that a backend is capable of sending these types, rather than mandate that a backend can send these types as other types. In addition, the trait remains open for any unsized types; every example given at the top of this writeup lists a reference to an unsized type which wants to be sent as a sized type. If the user has their own unsized types in the same vein (for example the bitvec crate's BitSlice type) then it becomes possible for them to define a Transmittable implementation custom-made for their type, e.g. impl Transmittable for BitSlice { type ReceivedAs = BitVec; }.

Option 5.) WONTFIX

Option number 5 is to do absolutely nothing. This isn't a horrible option to be honest; this basically only gives any sort of convenience when sending something like a slice where the receiver expects a vec, or similar.

Okay, so...?

Option number 4 seems like the best to me so far. It's simple, adds only one more trait which doesn't require adding extra syntax to the Transmit macro to generate bounds for it, and has a big default blanket implementation meaning that backend implementors don't actually need to do anything too special unless they want to support sending str as String and etc.

It would be a breaking change but if we're going to make a breaking change, now is the time as we're not coming close to a 1.0 release quite yet.

from dialectic.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.