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

Generalize object and type parameter bounds #192

Closed
wants to merge 2 commits into from

Conversation

nikomatsakis
Copy link
Contributor

An RFC summarizing type system changes that close up rust-lang/rust#5723 and related bugs.

region parameters as follows. Within a function or method, we apply
the wellformedness function `WF` to each function or parameter type.
This yields up a set of relations that must hold. The idea here is
that the caller could have type checked unless the types of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/could have/could not have/

@lilyball
Copy link
Contributor

lilyball commented Aug 7, 2014

👍


Currently, the type system is not supposed to allow references to
escape into object types. However, there are various bugs where it
fails to prevent this from hapenning. Moreover, it is very useful (and
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/hapenning/happening/

@Valloric
Copy link

Valloric commented Aug 7, 2014

Related bug: rust-lang/rust#13703. I hit that one all the freaking time. I'm ecstatic to see people are thinking about solutions to this issue.

A *lifetime bound* is written `'a:'b` and it means that "`'a` outlives
`'b`". For example, if `foo` were declared like so:

fn foo<'a, 'b:'a>(...) { ... }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presentation suggestion: If you write the definition of -:- as 'a:'b (i.e. with formals 'a and 'b), do not immediately follow it with an example that uses 'b:'a (i.e. swapping the arguments), since it is rather easy for someone to overlook that swap and then get very confused by the sentence "the lifetime 'a was shorter than (or equal to) 'b" that immediately follows "'a outlives 'b" above.

Instead, either use 'b:'a in both places, or choose fresh variable names for the concrete example, like

foo<'x, 'y:'x>(...) { ... }

(the latter is my preference).

However, there are complications:

- What about a type like `struct Ref<'a, T> { x: &'a T }`? `Ref<'a, Trait>`
should really work the same way as `&'a Trait`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I think that we can special case & at least because it is a special case in general (it even has special syntax), I wouldn't necessarily expect to apply the same rules to Ref. I agree Box is tricky since it should behave like a regular struct. (Although having to rarely have a redundant lifetime seems a price worth paying for simplifying the common case, unless I misunderstand the complications here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Thu, Aug 07, 2014 at 04:08:45AM -0700, Nick Cameron wrote:

I guess I think that we can special case & at least because it is a special case in general (it even has special syntax), I wouldn't necessarily expect to apply the same rules to Ref. I agree Box is tricky since it should behave like a regular struct. (Although having to rarely have a redundant lifetime seems a price worth paying for simplifying the common case, unless I misunderstand the complications here).

I have been using Box as a standin for "a regular struct that does
not implicitly contain borrowed data", basically. So I would expect
e.g. Box and Rc (and Vec) to behave the same with respect to
their type arguments. My bigger point here was that if you said, for
example, that we would apply a 'static default as the lifetime bound
for object types that appear in a type argument, it would do an
annoying (and almost certainly wrong) thing for Ref.

It is also mildly unclear to me whether it makes sense for there to be
one default for the lifetime bound on an object type and a distinct
set of defaults for lifetime parameters. In other words, should people
have to write &'a SomeStruct<'a> or should the latter 'a also be a
default (presuming we did do defaulting for traits)?

@brson
Copy link
Contributor

brson commented Aug 12, 2014

@brson brson closed this Aug 12, 2014
@glaebhoerl
Copy link
Contributor

Man, this was fast. I hadn't even read it yet. But now that I have I don't have anything to add, other than +1. (I haven't fully understood the tension between the various forms of inference the RFC doesn't propose doing, but not inferring is safely forwards-compatible and can be liberalized later, so I agree with this as well, even without understanding it yet.)

There's one thing I've been thinking about however, and this seems like an appropriate place to ask about it:

Is there ever a use case for lifetime parameters on types to be anything other than contravariant? As a subcase: is it ever preferable to have the lifetimes on (higher-order) function parameters be fixed from outside, instead of higher-rank?

E.g., do you ever want something like:

struct Foo<'a> {
    f: fn(&'a Bar) -> ...;
}

instead of

struct Foo {
    f: <'a> fn(&'a Bar) -> ...;
}

?

The reason I'm wondering is that if we can declare that lifetime parameters on types are always contravariant, then we can avoid the need for any kind of variance inference or annotations, which would be very nice. (This would also nicely plug the hole in lifetime elision w.r.t. covariant lifetimes: if covariant lifetimes on types are impossible, then avoiding their elision is trivial.)

My "grand plan" would also involve making type parameters be always invariant , and using Transmute/Coercible any time you want to safely change the types or lifetimes of inner types (type arguments). With the new 'a: 'b bounds the relevant impls could even be written in the language itself, instead of being magically wired-in: impl<'a, 'b: 'a, T> Transmute<&'a T> for &'b T { } (and likewise for &mut).

@nikomatsakis
Copy link
Contributor Author

On Wed, Aug 13, 2014 at 09:11:05AM -0700, Gábor Lehel wrote:

E.g., do you ever want something like:

struct Foo<'a> {
    f: fn(&'a Bar) -> ...;
}

instead of

struct Foo {
    f: <'a> fn(&'a Bar) -> ...;
}

This is supported and will be inferred to be covariant. However, this has seen very little use.

The reason I'm wondering is that if we can declare that lifetime parameters on types are always contravariant, then we can avoid the need for any kind of variance inference or annotations, which would be very nice. (This would also nicely plug the hole in lifetime elision w.r.t. covariant lifetimes: if covariant lifetimes on types are impossible then avoiding their elision is trivial.)

No, we cannot do this, there are definitely times when lifetime
parameters must be invariant. For example, 'b in a struct definition
like:

struct Foo<'a,'b:'a> {
    foo: &'a mut Foo<'b>
}

However, I have been contemplating a plan to make:

  • lifetime parameters contravariant by default
  • type parameters covariant by default in types
  • a mut prefix to declare invariance

I wanted to gather some data and was hoping to do it in the next few days.

Obviously this plan loses some expressiveness, though nothing that
couldn't be regained with some more potential annotations. One problem
with this plan is what to do about associated types (today: trait type
parameters -- which reminds me, I need to make a note on the RFC).
With the move to unboxed closures, fn types generally become object
types, and hence we may be unsatisfied without contravariance and
covariance. And it's not clear to me that the transmute plan below can
address this.

My "grand plan" would also involve making type parameters be always invariant , and using Transmute/Coercible any time you want to safely change the types or lifetimes of inner types (type arguments). With the new 'a: 'b bounds the relevant impls could even be written in the language itself, instead of being magically wired-in: impl<'a, 'b: 'a, T> Transmute<&'a T> for &'b T { } (and likewise for &mut).

I guess it'd be worth experimenting a bit. The last few times I
investigated "removing subtyping" it wound up either breaking a lot of
code or essentially being reintroduced through increasingly elaborate
coercion rules. I will note that I frequently encounter problems
because Option<&'a T> is (currently) invariant with respect to &'a T. Aggressive use of the Transmute relation would likely help here
in many cases, though it adds its own complications around inference.

bors added a commit to rust-lang/rust that referenced this pull request Aug 28, 2014
Implements rust-lang/rfcs#192.

In particular:

1. type parameters can have lifetime bounds and objects can close over borrowed values, presuming that they have suitable bounds.
2. objects must have a bound, though it may be derived from the trait itself or from a `Send` bound.
3. all types must be well-formed.
4. type parameters and lifetime parameters may themselves have lifetimes as bounds. Something like `T:'a` means "the type T outlives 'a`" and something like `'a:'b`" means "'a outlives 'b". Outlives here means "all borrowed data has a lifetime at least as long".

This is a [breaking-change]. The most common things you have to fix after this change are:

1. Introduce lifetime bounds onto type parameters if your type (directly or indirectly) contains a reference. Thus a struct like `struct Ref<'a, T> { x: &'a T }` would be changed to `struct Ref<'a, T:'a> { x: &'a T }`.
2. Introduce lifetime bounds onto lifetime parameters if your type contains a double reference. Thus a type like `struct RefWrapper<'a, 'b> { r: &'a Ref<'b, int> }` (where `Ref` is defined as before) would need to be changed to `struct RefWrapper<'a, 'b:'a> { ... }`.
2. Explicitly give object lifetimes in structure definitions. Most commonly, this means changing something like `Box<Reader>` to `Box<Reader+'static>`, so as to indicate that this is a reader without any borrowed data. (Note: you may wish to just change to `Box<Reader+Send>` while you're at it; it's a more restrictive type, technically, but means you can send the reader between threads.)

The intuition for points 1 and 2 is that a reference must never outlive its referent (the thing it points at). Therefore, if you have a type `&'a T`, we must know that `T` (whatever it is) outlives `'a`. And so on.

Closes #5723.
@pnkfelix
Copy link
Member

pnkfelix commented Sep 2, 2014

@gereeter gereeter mentioned this pull request Sep 6, 2014
spikespaz pushed a commit to spikespaz/dotwalk-rs that referenced this pull request Aug 29, 2024
Implements rust-lang/rfcs#192.

In particular:

1. type parameters can have lifetime bounds and objects can close over borrowed values, presuming that they have suitable bounds.
2. objects must have a bound, though it may be derived from the trait itself or from a `Send` bound.
3. all types must be well-formed.
4. type parameters and lifetime parameters may themselves have lifetimes as bounds. Something like `T:'a` means "the type T outlives 'a`" and something like `'a:'b`" means "'a outlives 'b". Outlives here means "all borrowed data has a lifetime at least as long".

This is a [breaking-change]. The most common things you have to fix after this change are:

1. Introduce lifetime bounds onto type parameters if your type (directly or indirectly) contains a reference. Thus a struct like `struct Ref<'a, T> { x: &'a T }` would be changed to `struct Ref<'a, T:'a> { x: &'a T }`.
2. Introduce lifetime bounds onto lifetime parameters if your type contains a double reference. Thus a type like `struct RefWrapper<'a, 'b> { r: &'a Ref<'b, int> }` (where `Ref` is defined as before) would need to be changed to `struct RefWrapper<'a, 'b:'a> { ... }`.
2. Explicitly give object lifetimes in structure definitions. Most commonly, this means changing something like `Box<Reader>` to `Box<Reader+'static>`, so as to indicate that this is a reader without any borrowed data. (Note: you may wish to just change to `Box<Reader+Send>` while you're at it; it's a more restrictive type, technically, but means you can send the reader between threads.)

The intuition for points 1 and 2 is that a reference must never outlive its referent (the thing it points at). Therefore, if you have a type `&'a T`, we must know that `T` (whatever it is) outlives `'a`. And so on.

Closes #5723.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants