Comments (1)
This issue persists out-of-the-box with a fairly small class of types:
&str
asString
&CStr
asCString
&OsStr
asOsString
&Path
asPathBuf
&[T]
asVec<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)
- [transmit-case] Ergonomics for `Choice<N>` bounds
- Self-documenting `offer!()` and `choose!()` HOT 3
- Make serialization non-dependent on serde HOT 5
- Try lifting `Session<Action = _>` bounds into `impl` for better errors HOT 2
- Backend wrapper restricted by an explicit allow-list
- `offer!` macro does not catch too-small arity
- Improve type error presentation
- Fix broken Clippy after update
- Ensure channel_id is actually unique HOT 1
- Add `confirmation_depth` to Merchant.toml example as well as explanation in README.md
- Provide a way to dialectic to have an extra encryption tunnel using a session key
- Write key-server function to accept a request to store a secret
- Add CHANGELOG since 0.3
- Support batching backends
- Transition to poll_fn style backend traits HOT 2
- Multiplexing backend adapter
- Explicit "unsafe" in session type language HOT 2
- Parameterize serde backend by serialization strategy HOT 1
- [transmit-case] Issues with `TransmitCase` by ref
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from dialectic.