Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Coercible and HasPrefix for Zero Cost Coercions #91

Closed
wants to merge 3 commits into from

Conversation

Projects
None yet
9 participants
@gereeter
Copy link

gereeter commented May 25, 2014

This is largely based on (with lots of copied text) @glaebhoerl's proposal in rust-lang/rust#9912, but with a few changes:

  • I formalized the idea of "contexts" by using GHC's roles.
  • I used this role system to include user-defined pointers and data structures.
  • I dropped contravariance for simplicity. This may have been a bad idea, but it's unclear how useful contravariance is.

rendered draft

vtable, and is considered a built-in "kind" alongside `Copy`, `Send`, etc.

Where single inheritance and subtyping conflate many different ideas, among them transparenta
ccess to superstruct fields, zero-cost conversion from sub- to supertypes, and these conversions

This comment has been minimized.

@lilyball

lilyball May 25, 2014

Contributor

Typo ("transparenta ccess").


The trait is wired-in to the compiler, and user-defined impls of it are highly restricted as
described in the implementation section talking about roles. `coerce()` would coerce between
any two types where the target type "is a proper subtype of" the input type. Note that `coerce`

This comment has been minimized.

@lilyball

lilyball May 25, 2014

Contributor

Why does it need to be a proper subtype? This would prevent coercing between two types that are considered equivalent. For example, between a newtype and the underlying uint (which is to say, you could define the coercion in one direction, but not the other).

This comment has been minimized.

@gereeter

gereeter May 25, 2014

Author

Good point - I'm not sure why I kept the word proper in there. I'll remove it.

This comment has been minimized.

@glaebhoerl

glaebhoerl May 26, 2014

Contributor

I think my original intent here with "proper subtype" was just to emphasize the distinction from merely "has prefix". For "strictly fewer inhabitants" I would have said "strict subtype".

```rust
impl Coercible<A> for B { }
impl Coercible<B> for A { }
```

This comment has been minimized.

@lilyball

lilyball May 25, 2014

Contributor

This right here indicates that you are not actually enforcing the "is a proper subtype" rule, because B and A can't both be proper subtypes of each other.

This comment has been minimized.

@gereeter

gereeter May 25, 2014

Author

Fixed.

```rust
impl<T, static N: uint> Coercible<[T, ..N]> for (T, T, .. times N) { } // fake syntax
impl<T, static N: uint> Coercible<(T, T, .. times N)> for [T, ..N] { }
```

This comment has been minimized.

@lilyball

lilyball May 25, 2014

Contributor

Do we actually guarantee the layout of tuples? I didn't think we did. Or at the very least, I would expect RFC 18 to make the layout undefined.

This comment has been minimized.

@gereeter

gereeter May 25, 2014

Author

I'm not exactly certain how this interacts with RFC 18. It probably is reasonable to assume that there isn't any difference between fixed size arrays and tuples (reordering does nothing when the types are the same). However, the tuple prefix rule might have to be dropped, and the first field rule might need an extra keyword. The only one of these I'm truly worried about is the first field rule - the tuple coercions don't seem that useful.

This comment has been minimized.

@lilyball

lilyball May 25, 2014

Contributor

Reorder definitely does affect it. It may be the same memory layout, but if I coerce (a, b, c) to an array and get [c, b, a] then that's not very helpful.

This comment has been minimized.

@huonw

huonw May 26, 2014

Member

RFC 18 does not mention tuples, so they currently have defined layout.

This comment has been minimized.

@bstrie

bstrie May 26, 2014

Contributor

@huonw Which, frankly, seems like a backwards-compatibility hazard at this point. If we decide to make tuple layout unspecified for 1.0, we can always choose to re-specify it later. The opposite is impossible.

This comment has been minimized.

@bstrie

bstrie May 26, 2014

Contributor

The question is, where would you stick the attribute if you wanted it to be specified? In that case, I'd be inclined to force users to just use structs instead of tuples. But this is stuff for a different RFC.

This comment has been minimized.

@glaebhoerl

glaebhoerl May 26, 2014

Contributor

I think the particular set of built-in coercion rules is not so important as just having the mechanism. The rules I originally included were a grab bag of whatever I could immediately think of, mainly to illustrate the possibilities. We could have only the most obviously useful and correct rules to begin with, and then add others later in a backwards compatible way, if we want to.

This comment has been minimized.

@huonw

huonw May 26, 2014

Member

@bstrie I don't disagree, I was just pointing out that the layout isn't (yet) undefined.

Maybe someone should submit a modification to that RFC making tuples also explicitly undefined.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented May 26, 2014

Huge thanks for taking this up. I'll try to leave a few comments.

## Existing `transmute`s

There are various calls to `transmute` scattered throughout the Rust codebase, and a
number of them are perfectly safe. It would be beneficial to write these in a way that

This comment has been minimized.

@glaebhoerl

glaebhoerl May 26, 2014

Contributor

I would hope that all of them are perfectly safe, because otherwise it's a bug! I think you just meant to say that the could compiler could verify their safety under this proposal. :)

This comment has been minimized.

@gereeter

gereeter May 27, 2014

Author

They aren't all safe, and it is a bug: rust-lang/rust#13933. I think there are others, but those are the most blatant.

```

Note that nominal parameters are a strict subset of representational parameters, which are a strict
subset of covariant parameter, which, in turn, are a strict subset of phantom parameters.

This comment has been minimized.

@glaebhoerl

glaebhoerl May 26, 2014

Contributor

Interesting, I've been trying to piece together in my head how these covariant, contravariant, (bivariant, invariant) roles relate to the nominal, representational, and phantom roles of GHC for some time now. (What they all have in common is that they are only consumed by the respective Coercibles.) I'll have to think about this.

And also as in GHC, to preserve abstraction boundaries, as a general principle, for those impls which involve conversions
between user-defined types, they would only "be in scope" when the means to do the conversion manually are in scope. This
means that you could only cast `&Struct` to `&FirstFieldOfStruct` if the first field of the struct is visible to you, you could
only cast `Foo` to `NewTypeOfFoo` if its constructor is visible, and other similar rules along these lines (described above).

This comment has been minimized.

@glaebhoerl

glaebhoerl May 26, 2014

Contributor

FWIW, I was relating to GHC's implementation here, and I originally thought "here be dragons", that this whole "would only be in scope when ..." business was the least well thought out and well-specified part of the proposal. But since then, I've realized that Rust could do it much more simply and straightforwardly with something like the late RFC PR #10. And unlike GHC, we could actually implement the thing as just a bunch of (incoherent) wired-in impls. The mechanism would simply be that, like all other built-in traits, HasPrefix and Coercible would be automatically derived by the compiler for plain (public-definition) structs and enums (according to the rules above), and would need to be declared/derived by the programmer explicitly for abstract structs and abstract enums. All of the complexity in GHC's implementation is just to work around them having the wrong defaults.

- Somehow generalize the system so that users can put arbitrary bounds on `Coercible` instances. This sounds good, but could be
added on later (it is completely backwards compatible) and is hard to implement or prove correct.
- Use `as` instead of a standalone function. While this might work, it prevents inference of the final type and conflicts with
the fact that today's conversions using `as` are not necessarily free.

This comment has been minimized.

@glaebhoerl

glaebhoerl May 26, 2014

Contributor

The inference part could be solved by allowing foo as _ - I think we already have type placeholders, so this would just fall out? The part about expected meaning is the real problem. If someone writes 6.0 as int, they probably don't intend a bitwise reinterpretation.

the fact that today's conversions using `as` are not necessarily free.
- Make everything use the loosest role possible by default. This would be fine from a memory safety standpoint, and is in fact
what GHC does. However, it can often break assumptions made by libraries, and Rust's philosophy of safety by default seems to
encourage using nominal by default. Additionally, this is more consistent with using opt-in kinds.

This comment has been minimized.

@glaebhoerl

glaebhoerl May 26, 2014

Contributor

(See again RFC PR #10. I'm strongly in favor of properly enforcing abstraction boundaries by default, unlike GHC.)

@cmr

This comment has been minimized.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jun 23, 2014

FWIW if I were writing this proposal today, I would go with this naming scheme:

  • unsafe fn transmute<T, U>(T) -> U -> unsafe fn unsafe_transmute<T, U>(T) -> U
  • trait Coercible<T> -> trait Transmute<T>
  • fn coerce<U, T: Coercible<U>>(T) -> U -> fn transmute<U, T: Transmute<U>>(T) -> U

In other words, rename the existing transmute to unsafe_transmute and use Transmute and transmute instead of Coercible and coerce.

This is more consistent with Rust's existing terminology which uses "transmute" to mean the thing we are doing here, and "coerce" to mean something different (not-necessarily-free compiler-inserted casts). It also happens to directly mirror GHC's (quite logical) scheme of unsafeCoerce, Coercible, and coerce, except using the word "transmute" instead of "coerce".

I still don't have a better idea for what to call HasPrefix.

@brson

This comment has been minimized.

Copy link
Contributor

brson commented Jun 25, 2014

Discussed at https://github.com/rust-lang/meeting-minutes/blob/master/weekly-meetings/2014-06-24.md#rfc-pr-91-coerciblehasprefix-httpsgithubcomrust-langrfcspull91-

Ergonomics in general is an area we want to focus on leading up to 1.0, and coercions probably have a role. We're not ready to make any decisions on the subject yet. Closing, but tagging with 'postponed' so it can be referenced later.

@brson brson closed this Jun 25, 2014

@brson brson added the postponed label Jun 25, 2014

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jun 25, 2014

Ergonomics in general is an area we want to focus on leading up to 1.0, and coercions probably have a role.

This is much more about expressiveness, safety and performance than about ergonomics. The name "coercible" was misleading in this context, because Rust uses it to mean something different (see previous comment).

I agree that implementing this is not urgent, just wanted to clarify.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Jul 26, 2014

So... I thought I made a discovery, inspired by an email from Edward Kmett, and then I re-read the RFC, and realized that it already does things exactly the way I was going to suggest, following my "discovery". The discovery was that we can completely avoid the need to have explicit role/variance annotations on type parameters (e.g. struct Foo<covariant T>) by just writing out the equivalent generic Transmute impls instead:

  • Instead of struct Foo<bivariant T>: ("bivariant" is another name for "phantom")

    // `Foo` doesn't actually contain a `T`, so we can `transmute` the `T` to any other type
    impl<T, U> Transmute<Foo<U>> for Foo<T> { }
    
  • Instead of struct Foo<covariant T>:

    // (Here `Foo` contains `T` only in "positive" positions.)
    // We can `transmute` `Foo<T>` to `Foo<U>` iff we can transmute `T` to `U`, 
    // i.e. if `U` is a "subtype" of `T`.
    impl<U, T: Transmute<U>> Transmute<Foo<U>> for Foo<T> { }
    
  • Instead of struct Foo<contravariant T>:

    // `Foo` uses `T` only in "negative" positions, e.g. function parameters,
    //  so this is the reverse of the above.
    impl<U, T: Transmute<U>> Transmute<Foo<T>> for Foo<U> { }
    
  • Instead of struct Foo<invariant T>: ("invariant" is another name for "representational")

    // `Foo` uses `T` in both positive and negative positions, 
    // so we can only `transmute` between `Foo<T>` and `Foo<U>` if they're "subtypes"
    // of each other in *both* directions, e.g. if one is a newtype of the other.
    impl<T, U> where T: Transmute<U>, U: Transmute<T> Transmute<Foo<U>> for Foo<T> { }
    
  • Instead of struct Foo<T>: (a completely fixed type parameter is also referred to as "nominal")

    // (nothing)
    // This is the case for types which not only use `T`, but also depend
    // in some way on its precise *identity*, as opposed to merely its representation. 
    // A good example is the `K` in `HashMap<K, V>`: you can't allow transmuting
    // it to any other type, because that other type might have a different impl of `Hash`,
    // which would break the internal invariants of the `HashMap`.
    

But, as I mentioned above, the RFC already specifies exactly this:

To declare the roles of each parameter, users must write impls of Coercible that follow the above patterns - each variable must be independent and only bounded by either HasPrefix or Coercible. By default, every parameter is nominal.

So I commend @gereeter's brilliance for realizing this before anyone else.

I think I was mislead by the earlier statement that:

To deal with data structures and pointers, we need to introduce the concept of roles,

which I unthinkingly assumed implied explicit nominal, phantom, and/or representational annotations along the same lines as GHC. Whereas by just writing the equivalent generic Transmute impls instead, I would say that we are avoiding the need to "introduce the concept of roles".

(Which is a very good thing!)

@jsgf

This comment has been minimized.

Copy link

jsgf commented Jun 8, 2017

How does this RFC look through the eyes of 2017 Rust?

@Centril Centril added the A-coercions label Nov 26, 2018

wycats pushed a commit to wycats/rust-rfcs that referenced this pull request Mar 5, 2019

wycats pushed a commit to wycats/rust-rfcs that referenced this pull request Mar 5, 2019

Merge pull request rust-lang#91 from ember-cli/addon-instrumentation-…
…experimental-hooks

Promote buildInstrumentation to public API
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.