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

Calling methods on generic parameters of const fns #2632

Open
wants to merge 32 commits into
base: master
from

Conversation

@oli-obk
Copy link
Contributor

oli-obk commented Feb 5, 2019

TLDR: Allow

const fn add<T: Add>(a: T, b: T) -> T::Output {
    a + b
}

and

pub struct Foo<T: Trait>(T);
impl<T: ?const Trait> Foo<T> {
    fn new(t: T) -> Self {
        // not calling methods on `t`, so we opt out of requiring
        // `<T as Trait>` to have const methods via `?const`
        Self(t)
    }
}

cc @Centril @varkor @RalfJung @eddyb

This RFC has gone through an extensive pre-RFC phase in rust-rfcs/const-eval#8

Rendered

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Feb 5, 2019

One alternative syntax design that is not mentioned here is the question of const impl versus impl const.

There are strong arguments that const impl is the more consistent syntax, as it is consistent both with the existing practice of prefixing modifiers to impl (default impl, unsafe impl, etc.) and prefixing const to keywords to indicate const variants (e.g. const fn [and the hypothetical const trait]).

Conversely, as far as I'm aware, impl const is not consistent with any existing syntax in the language or this RFC.


(I don't like to start the discussion with syntax bikeshedding, but as mentioned in the original post, this RFC has already ungone significant design discussion and I'm satisfied it's close to the optimal conservative design.)

@rpjohnst

This comment has been minimized.

Copy link

rpjohnst commented Feb 5, 2019

How do we want -> impl Trait and arg: dyn Trait to interact with const fn? If you must specify -> impl const Trait, does it also make sense to require arg: dyn const Trait? Both cases feel more existential-y and thus less tied to the function's type parameters, so I kind of lean that way for consistency.

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Feb 5, 2019

I imagine it makes most sense to use the same (syntactic) rules for impl Trait and dyn Trait as for parameter bounds: that is, in const fn, an impl Trait actually means impl const Trait/const impl Trait (and similarly for dyn) and impl ?const Trait/?const impl Trait allows opting-out of constness.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

mark-i-m commented Feb 6, 2019

I don’t think this has been mentioned before, but putting const before impl would be more consistent with other modifiers like unsafe and pub if we decided to do that.

Drop the correct type
Co-Authored-By: oli-obk <github35764891676564198441@oli-obk.de>
@ExpHP

This comment has been minimized.

Copy link

ExpHP commented Feb 6, 2019

I'm not sure why const Drop must be recursive on fields. In my mental model, the body of drop doesn't have calls to the fields' drop impls inserted, but rather, it's the role of drop glue to piece them all together:

{
    let local = get_thing();

    // Implicitly inserted
    // (each one is individually omitted if that type
    //  does not explicitly implement Drop)
    Drop::drop(&mut local);
    Drop::drop(&mut local.field_1);
    Drop::drop(&mut local.field_2);
}

Requiring const Drop on fields increases churn when library A cannot write const Drop yet due to a non-const Drop in library B. (so to use A in a const context, changes now need to be made to both library B and A in that order, instead of just B).


(one could argue that allowing impl const Drop in library A when a field is non-const Drop could lead to misleading documentation; but another thread of discussion on this PR seems to be arriving at the conclusion that we additionally require an auto-trait like ConstDrop, which partially mitigates this concern)

@oli-obk

This comment has been minimized.

Copy link
Contributor Author

oli-obk commented Feb 10, 2019

I don’t think this has been mentioned before, but putting const before impl would be more consistent with other modifiers like unsafe and pub if we decided to do that.

It has been mentioned before. This was even the original syntax. Reasoning for the change can be found at rust-rfcs/const-eval#8 (comment)

cc @scottmcm for consistency (not in semantics, but user expectations) I have slowly come back around to const impl Trait for Type being the best syntax.

@ExpHP

This comment has been minimized.

Copy link

ExpHP commented Feb 10, 2019

Ultimately the location of const is really nothing but the color of the bikeshed. impl const may be different from the other things, but it is not that hard to remember this "exception" when you have things like T: ?const Trait.

Conceptually, the keyword is attached to the trait. I.e. it's not an impl const of a Trait, but rather an impl of a const Trait.

@Lokathor Lokathor referenced this pull request Feb 11, 2019

Open

Meta tracking issue for `const fn` #57563

0 of 15 tasks complete

@Centril Centril self-assigned this Feb 14, 2019

@scottmcm

This comment has been minimized.

Copy link
Member

scottmcm commented Feb 14, 2019

Conversely, as far as I'm aware, impl const is not consistent with any existing syntax in the language or this RFC.

I think that's looking at it from the wrong perspective.

It's not that it's (impl const) Trait, but that it's impl (const Trait), the same as you can impl (dyn Trait). And that's consistent with something like struct Foo<T: const Default>(T); or similar.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

mark-i-m commented Feb 14, 2019

But as far as I can tell, the Constness is not a property of the Trait, but of the impl, which is not the same for dyn, right?

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Feb 14, 2019

It's not that it's (impl const) Trait, but that it's impl (const Trait), the same as you can impl (dyn Trait). And that's consistent with something like struct Foo<T: const Default>(T); or similar.

I agree with this in particular when viewed in light of effect systems. Moreover, impl const Trait also has composability benefits, e.g. arg: impl Add + ?const Clone + const Debug vs. arg: const impl Add + Cone + Debug are probably not the same semantically.

But as far as I can tell, the Constness is not a property of the Trait, but of the impl, which is not the same for dyn, right?

Traits can be seen as logical requirements and implementations are proofs that those requirements are satisfied. In that light, e.g. T: const Foo can be seen as a constraint that T satisfies const Foo and impl const Foo for MyT is a witness for that.

@Centril Centril added the A-effects label Feb 16, 2019

Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Show resolved Hide resolved text/0000-const-generic-const-fn-bounds.md Outdated
Apply suggestions from code review
Co-Authored-By: oli-obk <github35764891676564198441@oli-obk.de>
@rodrimati1992

This comment has been minimized.

Copy link

rodrimati1992 commented Apr 14, 2019

Associated constants are not redundant,since const fn used outside of a const context (like the value assigned to a const item*) are not guaranteed to be run at compile-time,while const items* have to run at compile-time.

*By const item I mean const IDENT:Type=expr,not including const fn.

In Rust today this fails at runtime:

const fn hello()->usize{
    let l=1;
    let r=0;
    l/r
}


fn main(){
    println!("{}",hello());
}

This fails at compile-time:

const HELLO:usize={
    let l=1;
    let r=0;
    l/r
};

fn main(){
    println!("{}",HELLO);
}

Here are the things that I would consider necessary for associated constants to be deprecated in favor of associated const fn:

  • Having static lifetime promotion for const fn for the same code as const items.
  • Being able to use traits in const fns.
  • Changing the behavior of parameterless const fn so that they are always evaluated at compile-time,this is a pretty important property of const items.
  • Being declarable as trait items.
@Ixrec

This comment has been minimized.

Copy link
Contributor

Ixrec commented Apr 14, 2019

Plus, even if we did choose to do all of the language changes needed to make associated consts technically redundant, and even if backwards compatibility weren't a concern, I think we'd still want associated consts as a syntactic sugar rather than requiring everyone to add parenthesis to the many, many associated consts that wouldn't benefit from being functions.


Changing the behavior of parameterless const fn so that they are always evaluated at compile-time,this is a pretty important property of const items.

While we obviously can't change const to mean this now, and the current meaning is a far more practical default, I think I saw someone somewhere propose introducing a separate feature for "always compile-time" stuff with a strawman keyword like constonly. Unfortunately I can't seem to find any trace of it now. Does anyone else remember seeing this?

const FOO: i32 = T::bar();
FOO
}
```

This comment has been minimized.

Copy link
@RalfJung

RalfJung Apr 15, 2019

Member

Clarification question: with this RFC, T: const Trait is never allowed as a bound, right? The constness of the bound is inferred from the constness of the function, and one can opt-out of that, but that's it.

This comment has been minimized.

Copy link
@oli-obk

oli-obk Apr 15, 2019

Author Contributor

Yes, the const Trait bound syntax is kept free with this RFC, so future changes can potentially use it. This RFC solely offers the ?const Trait syntax

This comment has been minimized.

Copy link
@RalfJung

RalfJung Apr 15, 2019

Member

Ah, I just noticed this is another alternative listed below ("explicit const bounds"). Could you add a link or at least state that explicitly?

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Apr 15, 2019

The main part of this RFC concerns the addition of const modifiers on impls as a prerequisite for allowing you to instantiate generic const fns at const time: I think this is a good idea. One thing we've regretted often is that trait definitions don't declare explicitly that they are object safe, introducing a backwards compatibility hazard. One thing I wish is that we could have something like dyn trait declarations (which also might be a hook for doing some more interesting trait object things too). I think it's the right choice to have explicitly const impls, instead of "inferring" constness and having another backwards compatibility hazard.

I have a few concerns, however.

@rfcbot concern const-position

I think this should be written const impl<T> Trait rather than impl<T> const Trait. I don't agree that the semantics suggest it should be written the way it is in the RFC - an argument could equally well be made that this is a modifier of the entire implementation, and therefore belongs before the impl (conceptually modifying the entire implementation declaration) rather than on the trait, which is only one part of the declaration. I also think users just strongly expect modifiers on items to come at the beginning of the declaration, the way that other modifiers like pub, unsafe, async, and const come there right now. The duality issue is an interesting point, but we've already established a strong precedent to treat both sides of the duality the same by having both unsafe impl and unsafe fn.

@rfcbot concern question-const

This is a deeper concern. I'm not convinced that T: ?const Trait is a well-enough motivated change to the language to include, and I'd prefer to add just const impl without this other addition. (While the justification for this change is included in the RFC, I don't know why it has been put forward now instead of being a future extension like the many extension proposals the RFC mentions).

I want to be clear about my larger position: I am dubious of the benefit of adding a lot of deep annotation heavy integrations between const fn and the generics system. I think this is something that will make people feel an obligation to properly annotate all of their generic parameters so that they propagate constness as permissively as possible, but that this will create too much burden on the community in a variety of ways:

  • Library authors will have to figure out if they can actually guarantee constness backwards compatibly, which will take their time and attention.
  • The ecosystem will suffer with version churn when libraries mistakenly mark things const and then need to make a breaking change to unconst them later on.
  • We will continue the trend of dumping huge amounts of type information on new users trying to understand the API of their libraries, much of which isn't relevant to them.

In other words, I think its important to remember that const annotations are not a socially zero cost abstraction - there is a global social cost to these extensions - and they should only be added with strong motivation. I think that strong motivation exists for allowing const fns to be generic and use trait methods, but I don't see the strong motivation for adding the syntactically complex ?const bound-modifier for what seems to me like a fairly niche use case (functions that bound generics but never actually use the bounds' items).

In a more concrete fashion, I feel a sense that the design work on constness - like many parts of our type system - is getting pretty far ahead of the real use experience. I'd like to get more a lot more motivating experience from people using const fn that will identify the most significant pain points, then start considering which of these extensions we will add. That is to say, at a high level: I don't expect (or prefer) that we will ever add a full-fledged effect system to Rust, a very pertinent question is how far toward that we will go before we stop, and I think we need more real world experience to feedback into that question.

@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Apr 15, 2019

I'm not convinced that T: ?const Trait is a well-enough motivated change to the language to include, and I'd prefer to add just const impl without this other addition.

On this note: one related issue is what to do (in the long run) about const fn foo(x: fn(T) -> U). This has actually recently come up in the Future discussion (in the RawWaker constructor). As we can clearly see there, the function passed as an argument does not have to be const, and can hence not be called inside the const fn.

This is a very similar situation to calling a trait method, so it might be a good idea to have some symmetry here. However, with this RFC, we do not! In the syntax this RFC proposed for trait bounds, foo as above would implicitly require a const fn in const context, and if it is a constructor then foo should be written const fn foo(x: ?const fn(T) -> U). (Maybe) unlike the case for traits, I think this is well-motivated -- but we cannot actually do this due to backwards compatibility.

As the RFC mentions, a function pointer that can actually be called would have to be written const fn foo(x: const fn(T) -> U) (this is listed as a future possibility). I suppose when called in run-time context, the const requirement is just ignored. (Or maybe not? It is at least conceivable that unsafe code would rely on some function being const. This relates to effect systems.)

The same applies to dyn Trait; const fn foo(x: &dyn Trait) can already be written today and can be called with non-const implementations of Trait; hence foo cannot call x's trait methods.

At the very least, I think this inconsistency between our two mechanisms of "abstracting over code" (statically with traits and dynamically with function pointers/trait objects) should be called out as a drawback. Ideally, they would be made consistent -- but given the backwards compatibility constraints, the only way to do that seems to be to require const fn foo<T: const Trait>(x: T). This removes the need for ?const, as that would be the default -- but it adds a lot of syntactic overhead, as most trait bounds of const fn will have to explicitly be made const. (And if we want to maintain the option of relying on const as absence of some effects, even this might not be a good choice.)

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Apr 15, 2019

In the syntax this RFC proposed for trait bounds, foo as above would implicitly require a const fn in const context

I don't think this is the case. fn foo is a function pointer type, not a trait, so it's unaffected by the context in which it appears. If you want to call foo, we need a const fn pointer, as you say.

I don't think there's an issue here with symmetry: there's just a difference between the expressiveness of types and traits with respect to const (the changes here are forwards compatible with expanding the expressiveness of the types later).

the only way to do that seems to be to require const fn foo<T: const Trait>(x: T)

This syntax makes it impossible to distinguish functions foo that require T to const-implement Trait at run-time and those that require it to implement it normally at run-time, so it's not just syntactically awkward — it's overly restrictive.


(Or maybe not? It is at least conceivable that unsafe code would rely on some function being const. This relates to effect systems.)

I don't think there are justifiable use cases to force functions being const. Any state at run-time should be no more restrictive than at compile-time.

@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Apr 15, 2019

I don't think this is the case. fn foo is a function pointer type, not a trait, so it's unaffected by the context in which it appears. If you want to call foo, we need a const fn pointer, as you say.

I was just saying, if the syntax was consistently extended to function pointer types, then this is what would happen.

I don't think there's an issue here with symmetry: there's just a difference between the expressiveness of types and traits with respect to const (the changes here are forwards compatible with expanding the expressiveness of the types later).

There is a symmetry break here: expressing the same thing ("may be called in const context with a non-const-safe instance") is written fn foo<T: ?const Trait>(x: T) once, and fn foo(x: &dyn Trait) the other time. In one case one has to explicitly request "can be called in const context without restrictions (because item does not get used)" by writing ?const, in the other case that is the default.

How is that not an issue with symmetry?

Maybe we are fine with this, I don't think this is a blocker. It is a downside though and should be clearly called out as such in the RFC.

I don't think there are justifiable use cases to force functions being const. Any state at run-time should be no more restrictive than at compile-time.

Even as someone mostly opposed to adding the complexity of an effect system to Rust, I have to strongly disagree with that statement. This is a way to restrict what the function can do, and that is always extremely useful when reasoning about higher-order code.

For example, one could write a function fn sort(data: &mut [i32], compare: const fn(i32, i32) -> Ordering) that would be safe to call while relying on the fact that compare will always return the same result when given the same integers. sort could be UB if compare is inconsistent, but that would be fine because there would be no way to call sort that way (in safe code).

We could decide that making Rust support that kind of reasoning has too high costs elsewhere, but IMO there is no denying the usefulness of this.

@oli-obk

This comment has been minimized.

Copy link
Contributor Author

oli-obk commented Apr 15, 2019

I think this should be written const impl Trait rather than impl const Trait.

As I have stated frequently, I don't have a strong opinion there and will essentially choose whatever gets this RFC merged ;)

This is a deeper concern. I'm not convinced that T: ?const Trait is a well-enough motivated change to the language to include

This is actually motivated by two things that I should have mentioned in the RFC, but simplified it to constructors.

  1. Drop impls need to have the same generic bounds that the type declaration has. If you want to have a const Drop implementation, all bounds must be const Trait on the type and the Drop impl, even if the Drop impl does not use said trait bounds.

  2. The standard library is full of cases where you have bounds on a generic type's declaration (e.g. because the Drop impl needs them). Any method (or its impl block) on that generic type will need to repeat those bounds. Repeating those bounds will restrict the impls further than actually required. We don't need const Trait bounds if all we do is store values of the generic argument type in fields of the result type.

I believe that removing ?const will make this RFC significantly less useful to the community than having the full RFC. The point wrt const fn pointers has already been made above. There is no way to stabilize a const fn wrapper around RawWaker::new on stable code without having a way to opt out of the constness of a function pointer in a const fn's argument. The same reasoning applies to trait bounds. E.g. Iterator methods could never become const fn, because F: FnOnce(A) -> B is not a legal bound without restricting F to having to be const fn. Without an opt-out like ?const you are in fact creating both bad churn and fear of constification in the ecosystem, because users have no way to create a const fn with generic arguments without restricting users of that const fn to passing const Trait bounds. This will likely lead to both too-eager application of const fn where the function becomes unusable in many cases and too little application of const fn in more conservative crates where const fn would be ok right now, but "unsure of future changes".

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Apr 15, 2019

There is a symmetry break here: expressing the same thing ("may be called in const context with a non-const-safe instance") is written fn foo<T: ?const Trait>(x: T) once, and fn foo(x: &dyn Trait) the other time.

Ah, I had skimmed over the part about dyn Trait. I think the point about symmetry here is a good one: one would expect dyn Trait to require the type to const-implement Trait in a const fn (following "any bound in a const fn is treated as const").

I think without changing this, the behaviour is extremely counter-intuitive. Can we make dyn Trait act like a generic trait bound instead (i.e. be const in a const fn)? dyn Trait is unstable in const fn at the moment, isn't it, so we can still make this change?

that would be safe to call while relying on the fact that compare will always return the same result when given the same integers

I don't believe we make this guarantee (i.e. determinism of const fn). I really think we should avoid conflating constness and purity. I agree that hypothetically it may be desirable to enforce purity (in the context of an effect system), but I think this should be viewed as a separate concern.

@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Apr 15, 2019

@oli-obk

If you want to have a const Drop implementation, all bounds must be const Trait on the type and the Drop impl, even if the Drop impl does not use said trait bounds.

There are no const Trait bounds... do you mean Trait (implicitly const in const context)?
This is actually why I asked about const Trait bounds, they came up so often in discussion I thought they were a thing, so I was a bit surprised now to see that they are not.

There is no way to stabilize a const fn wrapper around RawWaker::new on stable code without having a way to opt out of the constness of a function pointer in a const fn's argument. The same reasoning applies to trait bounds.

Absolutely. That's why I am arguing that the way to do this should be consistent. For function pointers, the "opt out" is actually the default.

@varkor

I think this is extremely counter-intuitive. Can we make dyn Trait act like a generic trait bound instead (i.e. be const in a const fn)? dyn Trait is unstable in const fn at the moment, isn't it, so we can still make this change?

That would make it inconsistent with function pointers though, which is just as counter-intuitive.

Also, they are not unstable; this compiles:

const FOO: &dyn std::fmt::Debug = &13;

(EDIT: But as @oli-obk points out, const fn foo(x: &dyn std::fmt::Debug) {} is unstable.)

I don't believe we make this guarantee (i.e. determinism of const fn). I really think we should avoid conflating constness and purity. I agree that hypothetically it may be desirable to enforce purity (in the context of an effect system), but I think this should be viewed as a separate concern.

We do not currently make it, but I can also not conceive of anything we would (want to) let const fn do that would break this guarantee -- hence it seems premature to close this door forever. I think breaking this guarantee would only be possible if we let const fn mutate global state, and that is inherently incompatible with the idea that a const FOO: Type = foo(); gets evaluated at compile-time but should behave exactly as if the definition was inlined at every use site.

Again, I am not necessarily arguing for conflating constness and determinism, but I am arguing for keeping that option. It seems like a natural connection, and like a way to get more out of this const fn business that it seems we want/need anyway. Wouldn't it be cool if we could get some of the benefits of an effect system for just a small cost on top of the constness system?

@oli-obk

This comment has been minimized.

Copy link
Contributor Author

oli-obk commented Apr 15, 2019

For function pointers, the "opt out" is actually the default.

Right now they are unstable. With this RFC they stay unstable. The future extension suggested in this RFC is ?const fn() as not callable const fn pointer and just fn() as callable const fn pointer

Can we make dyn Trait act like a generic trait bound instead (i.e. be const in a const fn)? dyn Trait is unstable in const fn at the moment, isn't it, so we can still make this change?

That was always my intention, I should add that to the future changes. This RFC does not make any changes to dyn Trait arguments to const fn.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Apr 15, 2019

I need to review the changes to Drop more in-depth; until I do so, I'll leave

@rfcbot concern centril-needs-to-review-more-in-depth-again

@withoutboats

I think this should be written const impl<T> Trait rather than impl<T> const Trait. I don't agree that the semantics suggest it should be written the way it is in the RFC - an argument could equally well be made that this is a modifier of the entire implementation, and therefore belongs before the impl (conceptually modifying the entire implementation declaration) rather than on the trait, which is only one part of the declaration.

This does not work well with:

fn foo<T: const Bar>() {}
//        ---------- The bound is a logical unit.

fn foo(arg: impl Bar + const Baz) {}
//               ---   ---------
// Each of these are different parts of the bound and `const` is a property of `const Baz`.

fn foo() -> impl Send + const Bar {}
//               ----   ---------
// Same here.

Conceptually, impl const Trait communicates that you are implementing a const version of Trait. This fits well with the snippets above, but const impl Trait does not fit well with those snippets.

I also think users just strongly expect modifiers on items to come at the beginning of the declaration, the way that other modifiers like pub, unsafe, async, and const come there right now. The duality issue is an interesting point, but we've already established a strong precedent to treat both sides of the duality the same by having both unsafe impl and unsafe fn.

I expect so too, but the expectation is in my view misleading especially when unsafe impl is considered.

The semantics of unsafe impl are that the documented invariants of the unsafe trait are upheld. This is unlike impl const Trait which enforces that each function within the implementation adhere to the rules of const fn. There is no similar polymorphism or "versions" of the trait; there's just unsafe impl which all implementations of an unsafe trait must use. In other words, you cannot write impl Send .... Writing impl unsafe Trait { ... } and fn foo<T: unsafe Trait>(...) would be more akin to what impl const Trait { ... } and fn foo<T: const Trait>() would be doing. Here, unsafe Trait would denote a version of the trait with unsafe functions. However, it is not clear that unsafe Trait is useful because of the half-effect nature of unsafe.

This is a deeper concern. I'm not convinced that T: ?const Trait is a well-enough motivated change to the language to include, and I'd prefer to add just const impl without this other addition.

As @RalfJung and @oli-obk noted, the notion of T: ?const Trait is already used by RawWakerVTable::new in the form of #[rustc_allow_const_fn_ptr]. When I agreed to the introduction of this hack attribute, it was because I was under the impression that everyone agreed it was a temporary hack and that we would find a syntax for supporting the semantics of T: ?const Trait in some fashion. I will not agree to having this hack in a permanent fashion without any hope of eventual consistency.

(While the justification for this change is included in the RFC, I don't know why it has been put forward now instead of being a future extension like the many extension proposals the RFC mentions).

Because it is already desired by users, e.g.:

Moreover, some of the future work noted in this RFC should be implemented in different feature gates as part of the experimentation to make sure that the future work actually works with the base of this RFC. I discussed this with @oli-obk in Berlin but it never made it into the RFC (I don't recall what parts it was exactly...). Aside: @oli-obk, can you please make a note of this in the RFC?

I want to be clear about my larger position: I am dubious of the benefit of adding a lot of deep annotation heavy integrations between const fn and the generics system. I think this is something that will make people feel an obligation to properly annotate all of their generic parameters so that they propagate constness as permissively as possible, but that this will create too much burden on the community in a variety of ways:

The ecosystem will suffer with version churn when libraries mistakenly mark things const and then need to make a breaking change to unconst them later on.

As far as I know, it is not a breaking change to go from T: Trait to T: ?const Trait. This is a loosening of a restriction. The breaking change would be to go from T: ?const Trait to T: Trait. Having the latter be the shorter one nudges users towards choosing T: Trait which is almost always right. ?const Trait is mostly useful when dealing with marker traits and constructors.

and I think we need more real world experience to feedback into that question.

The best way to retrieve real world experience is to actually test it out on nightly. I intend to treat this RFC quite experimentally.

@oli-obk

The same reasoning applies to trait bounds. E.g. Iterator methods could never become const fn, because F: FnOnce(A) -> B is not a legal bound without restricting F to having to be const fn.

Huh? Surely Iterator's methods, e.g. .map(mapper) needs to be const-polymorphic. Otherwise we'll never be able to pervasively constify libcore and large parts of the ecosystem.

@RalfJung

(Maybe) unlike the case for traits, I think this is well-motivated -- but we cannot actually do this due to backwards compatibility.

The same applies to dyn Trait; const fn foo(x: &dyn Trait) can already be written today and can be called with non-const implementations of Trait; hence foo cannot call x's trait methods.

This is not true. We added restrictions on function pointers, trait objects, and impl Trait so that we could backwards compatibly use const fn foo(x: fn()).

const fn foo(x: fn()) {}
const fn bar(x: &dyn Iterator<Item = u8>) {}

results in:

error[E0723]: function pointers in const fn are unstable (see issue #57563)
error[E0723]: trait bounds other than `Sized` on const fn parameters are unstable (see issue #57563)

Also, they are not unstable; this compiles:

const FOO: &dyn std::fmt::Debug = &13;

You are conflating const items and const fns.

@varkor

I don't think this is the case. fn foo is a function pointer type, not a trait, so it's unaffected by the context in which it appears. If you want to call foo, we need a const fn pointer, as you say.

What is the rationale for having it be unaffected? const is a modifier on the context, and so it stands to reason that it affects traits, trait objects, impl Trait and function pointers in a similar fashion. In my view, it would be wholly inconsistent to have const fn foo<T: Clone>(x: T) and const fn foo(x: const fn() -> u8) together. To be consistent, you'd have const fn foo<T: const Clone>(x: T) and const fn foo(x: const fn() -> u8) which would require a const implementaton rather than be polymorphic.

This syntax makes it impossible to distinguish functions foo that require T to const-implement Trait at run-time and those that require it to implement it normally at run-time, so it's not just syntactically awkward — it's overly restrictive.

I don't think there are justifiable use cases to force functions being const. Any state at run-time should be no more restrictive than at compile-time.

For the same reason you want const fn foo<T: const Add>(...) to force implementations const Add you also want const fn foo(x: const fn() -> u8) to force function pointers to const fns. This is both useful to enforce determinism statically and for being allowed to call x and T::add inside a const context within foo. The latter is important for interacting with const generics.

@RalfJung

We could decide that making Rust support that kind of reasoning has too high costs elsewhere, but IMO there is no denying the usefulness of this.

💯

I don't believe we make this guarantee (i.e. determinism of const fn). I really think we should avoid conflating constness and purity. I agree that hypothetically it may be desirable to enforce purity (in the context of an effect system), but I think this should be viewed as a separate concern.

I don't think we'll ever guarantee referential transparency for all (only some) const fns due to &mut T but that is something other than determinism which I think we should enforce for all const fns because it is hugely surprising not to.

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Apr 15, 2019

I was just saying, if the syntax was consistently extended to function pointer types, then this is what would happen.

That would make it inconsistent with function pointers though, which is just as counter-intuitive.

On second thoughts, I agree. Function pointers should be treated in the same way. They're unstable in const fn at the moment too, so hopefully we can change this too?

Also, they are not unstable; this compiles:

This requires the constant to const-implement the trait, though, because it's a const, right? So we should still be able to change this without fallout.

@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Apr 15, 2019

@oli-obk

Right now they are unstable. With this RFC they stay unstable. The future extension suggested in this RFC is ?const fn() as not callable const fn pointer and just fn() as callable const fn pointer

They are unstable as function arguments (I didn't know that, good call), but stable as types of const. You (@Centril said the same thing above so this is a plural "you" ;) want to interpret these differently?

@varkor

This requires the constant to const-implement the trait, though, because it's a const, right?

No, there is no such requirement:

fn foo() {
    println!("Hello!");
}
const FOO: fn() = foo;
@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Apr 15, 2019

So I retract what I said about inconsistency with function signatures, I was not aware that this is still unstable (nice foresight!). But then pretty much the same concern arises with const items. It seems really strange that this should not be legal:

const fn foo(x: fn()) {
  // for consistency with trait bounds, this will likely mean x is const-callable.
  x();
}

const X: fn() = /* ... */;
const Y: () = foo(X); // error

X is a constant of type fn() and yet we cannot call a const fn with argument type fn().

This strikes me as somewhat less surprising than if the inconsistency was within the function signature, but still as surprising and something worth calling out in the "Drawbacks" of this RFC -- because this RFC is where we commit to that choice (assuming symmetry for when this gets extended to trait objects and function pointers).

@Centril Centril added the I-nominated label Apr 15, 2019

@varkor

This comment has been minimized.

Copy link
Member

varkor commented Apr 15, 2019

@Centril:

Conceptually, impl const Trait communicates that you are implementing a const version of Trait. This fits well with the snippets above, but const impl Trait does not fit well with those snippets.

I think const impl Trait fits with these snippets, when you read it as "implement Trait in a const fashion". I think either one is consistent with const trait bounds.

The advantage with impl const is symmetry in const trait bounds. The advantages with impl const are consistency with other keywords, and with potential future extensions to this feature in the future. I make this point in slightly more detail here. I think the benefits of const impl outweigh the advantages of impl const.

@burdges

This comment has been minimized.

Copy link

burdges commented Apr 15, 2019

You could seemingly disambiguate some concerns @RalfJung noted with some notion of "const lifetime" that clarified the relationships of input constness to output constness, which sounds unacceptable. I have not understood from the above discussion if this is now known to be overkill, but maybe yes because the const fn should be pure too?

In practice, I'd kinda expect a const fn not to use any pointers passed in, but to pass them into whatever it constructs. I think means const fn foo(x: ?const fn()) sounds like the intuitive default, and const fn foo(x: const fn()) the exception.

@RalfJung

This comment has been minimized.

Copy link
Member

RalfJung commented Apr 15, 2019

@burdges I am confused. What I said has nothing to do with lifetimes?

@withoutboats

This comment has been minimized.

Copy link
Contributor

withoutboats commented Apr 15, 2019

Right now they are unstable. With this RFC they stay unstable. The future extension suggested in this RFC is ?const fn() as not callable const fn pointer and just fn() as callable const fn pointer

There are alternative designs that don't involve this syntax:

  • Allow fn pointers to be passed to const fns but not called. Add const fn pointers which can be called in const contexts.
  • Allow fn pointers to be passed to const fns but not called and never introduce any syntax for calling fn pointers in a const context.

At a first glance, either of these solutions would seem more preferable to me than ?const fn if the ?const syntax were to have no other uses. That is, you can not draw a clear line from RawWaker to ?const fn to ?const Trait; the connection is not a direct implication.

@Centril

This comment has been minimized.

Copy link
Contributor

Centril commented Apr 15, 2019

  • Allow fn pointers to be passed to const fns but not called. Add const fn pointers which can be called in const contexts.

If you use const fn for this purpose then you have to give up on either of these 2 semantics:

  1. const fn() is an effect polymorphic function pointer wrt. the const restriction (this is what T: Trait means with this RFC, so you give up symmetry, and what fn() is in the future work section).
  2. const fn() must be passed a function pointer to a const fn (this is what T: const Trait and const fn() means in the RFC, https://github.com/oli-obk/rfcs/blob/const_generic_const_fn_bounds/text/0000-const-generic-const-fn-bounds.md#explicit-const-bounds).

The way this RFC is formulated you give up on nothing.

  • Allow fn pointers to be cast to const fns but not called and never introduce any syntax for calling fn pointers in a const context.

Casting fn pointers to const fns would need to be an unsafe { ... } operation because you can sneak impurity into const fns this way. It seems like a rather strange way to support the use case.

@eddyb

This comment has been minimized.

Copy link
Member

eddyb commented Apr 16, 2019

FWIW, I think RawWakerVTable needs zero interaction with this RFC, and that some of this discussion around fn pointers for the sake of RawWakerVTable is an unnecessary waste of everyone's time.

As I wrote in rust-lang/rust#59739 (comment), we can have RawWakerVTable::new work at compile-time and allow promotion for it, even if we don't allow fn pointers in const fn nor promote calls to const fns, because we can rely on "callable" pattern aliases instead (rust-rfcs/const-eval#19 (comment)).

@oli-obk

This comment has been minimized.

Copy link
Contributor Author

oli-obk commented Apr 16, 2019

const fn() must be passed a function pointer to a const fn (this is what T: const Trait and const fn() means in the RFC

Note that this RFC does not suggest supporting const Trait or const fn(). It merely leaves the door open for that being a future extension.

Allow fn pointers to be passed to const fns but not called and never introduce any syntax for calling fn pointers in a const context.

This is actually a workable alternative. Though it would mean that any const fn that is supposed to take a function pointer becomes generic over the function type:

// what would have been
const fn foo(f: fn()) { f() }
// in this RFC, becomes
const fn foo<T: Fn()>(f: T) { f() }

we can rely on "callable" pattern aliases instead (rust-rfcs/const-eval#19 (comment)).

The issue with relying on pattern aliases is that I'd find it very surprising if we had pattern aliases that are not valid const fn.

@oli-obk

This comment has been minimized.

Copy link
Contributor Author

oli-obk commented Apr 16, 2019

you can not draw a clear line from RawWaker to ?const fn to ?const Trait; the connection is not a direct implication.

The argument for this connection is symmetry of the features. The alternatives discuss various reasons why explicit T: const Trait are undesirable for this RFC. Ending up with const fn pointers in the future would be very surprising if we go with T: Trait as proposed by this RFC.

I'd like to get more a lot more motivating experience from people using const fn that will identify the most significant pain points, then start considering which of these extensions we will add.

I made it explicit in the RFC that ?const is a separate feature that will be implemented in parallel with trait bounds on const fn, but stabilized separately.

That is to say, at a high level: I don't expect (or prefer) that we will ever add a full-fledged effect system to Rust, a very pertinent question is how far toward that we will go before we stop, and I think we need more real world experience to feedback into that question.

const fns with bounds are in use on nightly since 1.0. The current const_fn feature gate does not allow calling methods on these trait bounds. Stabilizing just the feature that noone uses so far (because it's not implemented) and removing the feature that is in use on nightly (and thus noone complains about loudly) seems slightly odd.

@Centril Centril removed the I-nominated label Apr 18, 2019

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.