-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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: const bounds and methods #2237
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've wanted const fn
s in traits for the longest time and I think that this is honestly the best option I've seen, at least for the first three parts. The fourth one makes me feel a bit uneasy, but I left a large comment on the RFC source about my opinions on it.
Currently, this RFC also proposes that you be allowed to write `impl const Trait` | ||
and `impl const TraitA + const TraitB`, both for static existential and universal | ||
quantification (return and argument positiion). However, the RFC does not, in | ||
its current form, mandate the addition of syntax like `Box<const Trait>`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that const Trait
objects should be elaborated in a bit more detail. What does it mean to have &const Trait
or Box<const Trait>
? Right now, AFAIK, const eval doesn't cover references, so something like:
const NUMBER: &usize = 5;
const NUMBER_2: usize = *NUMBER;
would fail. If this behaviour were to be changed (and IMO, it makes sense to do so), then something along the lines of &const Trait
would be reasonably well-formed. That said, Box<const Trait>
still makes no sense.
Longer term, it might make sense to simply use inference to determine if a function can be executed in constant time or not. For example, take the following (not very efficient) function:
const fn pow<T: const Mul + const One>(val: T, exp: usize) {
if exp == 0 {
T::one()
} else {
val * pow(val, exp - 1)
}
}
To me, it makes more sense to completely forgo the const Mul + const One
in favour of Mul + One
. This would allow the function to be called for inputs which are not const which do not support const fn
, but also let the function be called in a const context if, for example, T
was u64
.
Perhaps a clippy lint could warn for calling const fn
s like this one with a constant value where the requisite traits were not const
as well. However, in general, it would be allowed by the language.
I feel like this sort of thing would make a lot more sense than simply providing a const Trait
definition which opens up a whole interesting world of things.
That said, adding const impl Trait
syntax would be totally fine IMHO. The only issue would be that things like impl const Default + Iterator
would no longer be expressable, but I'm not sure what the benefit of that would be. At that rate, why not simply define a type directly which has all of your requirements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also adding further to this: &const Trait
only makes sense if it's &'static const Trait
, because const
s all have 'static
lifetimes now. Which kind of further makes things a bit confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another addendum: const impl Trait
in function signatures makes sense considering how const impl Trait { ... }
is allowed by the third option. Which, IMHO, is another good justification for option number three.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for a great and elaborate review!
Good point on const impl Trait
and the const impl Trait { .. }
syntax - I hadn't thought of it. I will surely add this to the motivation. Thanks <3.
Writing Box<const Trait>
, entails that it can only be constructed by some T
for which T: const Trait
holds. Other than that, it is just Box<Trait>
. The same applies to &(const Trait)
. The only expressive power you have gained with Box<const Trait>
is the ability to restrict to const fn
s in the trait impl, nothing else. Since this is marginally useful, I have opted to not propose it yet. Perhaps if not added in this RFC, it can be added in a subsequent RFC. I have written a bit on &'static const Trait
now.
Type inference is nice, but also more complex (algorithmically) and is considerably more costly wrt. compile times. The fact that Rust already has long compile times is something we should try hard not to increase too much. In addition, there is certain expressive power to the ability of restriction - do note that T: const Trait
is also allowed for normal fn
s and impl<T: const Trait>
. I think the explicitness and uniformity is a good thing.
The impl const Default + Iterator
thing is fully expressible with this proposal - as an example of what you can use this for is to plug into the universal quantification here:
fn foo<T: const Default + Iterator>(bar: T) {
// Use the const Default bound:
const BAZ: T = T::default();
// Use the iterator bound:
BAZ.foreach(|elt| { dbg!(elt); });
}
EDIT: read a bit too fast - -> const impl TraitA + TraitB
syntax does seem interesting - tho I'd like not to lose the ability to write -> impl TraitA + const TraitB
as used in foo
above. Of course you could allow both -> const impl Trait
and -> impl const Trait
. I don't think similarity of -> const impl Trait
and const impl
is enough justification to reduce expressivity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I mentioned, I'm having trouble understanding in what context you'd want a impl T1 + const T2
; the case you mentioned is worthwhile but I'm not sure if it bears the weight of the syntax implications it holds. As stated, you can always use newtypes to get this kind of expressiveness, but in most cases when you're returning a value you either want it to work in all const contexts or none, IMHO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm for sure open to const impl TraitA + Trait B
as a syntax if impl const TraitA + const TraitB
is too weird to swallow - but my base position is always that expressivity is more important than syntax, even if syntax may become clunky as a result of it. If there is popular demand for this change I will certainly do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be a supremely bad idea, but you could also do: -> const impl TraitA + impl TraitB
.
…rait syntax, type inference alt.
Kneejerk:
seems unusual to me. Doesn't it hurt searchability by creating more locations that the reader needs to search? (quick! What are the trait bounds on |
@ExpHP By this I mean that you don't have to look at every EDIT: Not mentioned in the motivation directly but well in the guide-level-explanation and drawbacks is the usefulness of |
Sorry, you are right, I misread it as referring to searchability of a The |
I should mention that IMO a solution that can't be integrated with auto-deriving is a non-starter. EDIT: to be more clear, I mean existing (builtin) |
It might be better just to derive the most- |
@eddyb, @ExpHP: Another possibility that just crossed my mind (and thanks for bringing the omission in the RFC to my attention, btw) is: #[derive_const(Copy, Clone)] // No bc-break because we have reserved all derive_ prefixed attributes.
struct Foo; Which do you prefer? @rpjohnst Oh yeah - this is the question "Does T: const Trait specialize T: Trait ?" which I ask in the unanswered questions. cc @aturon on this. |
I doubt this is possible, considering that
This brings to light a drawback that this RFC has compared to e.g. What I mean is: It should be possible to have a type |
@ExpHP Yep, I think it's the same question. Tho I have to ask why you'd want two impls of Btw - which syntax do you prefer of the two alternatives? |
I think I'd rather think more about the implications before shedding on the syntax, but between the two, I'm not sure if |
With respect to overlap, the major problem arises for things like:
With respect to derive the value of being explicit about constness in derive is that custom derive can get the information. |
Hmm, good point. A more general solution might be to allow functions to be |
Even though this RFC is a sensible extension of the existing Today, rustc tells me that "blocks in constant functions are limited to items and tail expressions", which is basically the same limitation as existed in C++ when constexpr functions were first specified in C++11. Pretty soon this limitation was decided to be unnecessary, given that it required algorithms to be rewritten using recursion instead of iteration - which is both unnatural in C++ and inefficient - when it would add little compiler complexity to just support the full language. So C++14 removed the restrictions. If I'm not mistaken, Rust is on track to do the same thing once miri-based constant evaluation is finished. (See also: "Relaxing constraints on But that already gets you to a point where a large fraction of functions and methods can be marked The biggest limitation in C++ is that for now there's no heap allocation in constexprs. So no But… if heap allocation becomes supported, const-ineligible fns will be the exception rather than the rule. One disqualifier would be FFI. Another would be pointer-to-int conversions, perhaps used for hashing, but I don't think Hash impls usually rely on that(?). Another might be randomization, like for HashMap seeds, but that's a solvable problem (you can start with a fixed seed and randomize on growth). Overall, I feel like const eligibility could be treated like an auto trait, like Send or Sync: enabled by default, and ineligibility implicitly propagates to dependencies (in this case, caller fns, as opposed to structs containing ineligible fields), because it's common enough that the usual policy of requiring explicit opt-ins/bounds wouldn't be worth the cost in verbosity. |
@comex I'm not sure if you're talking about a literal trait, or just something trait-like. But given that satisfying a trait (let's call it Also - is your only point of contention 4. (bounds) or everything? |
Is It'd be lovely if We could almost imagine a curried form of
It's not quite right though because if There is no obvious similar way to adapt
We cannot afaik add this associated type constructor and method to tl;dr We might be fairly close to correcting the type of expressions like |
@ExpHP Thinking about it some more I can't see a reason why this would not be possible: struct Foo<A>(A)
impl<A: Default> Default for Foo<A> {
default fn default() -> Self { Foo(A::default()) }
}
const impl<A: const Default> Default for Foo<A> {
fn default() -> Self { Foo(A::default()) }
} They do not overlap and preserve coherence because if the substituted-for type However, you may of course not have: struct Bar;
impl Default for Foo {
default fn default() -> Self { Bar }
}
const impl Default for Foo {
default fn default() -> Self { Bar }
} as there is nothing to specialize. However, notably having only the latter |
Come to think of it now and speaking of specialization there's the |
@burdges Doesn't this work? const impl<T, const N: usize> Index<usize> for [T; N] {
type Output = T;
fn index(&self, index: usize) -> &Self::Output {
// ..
}
} This I'm not sure about the other traits as |
PS: I think I need to chill a bit and read some papers... =) OK; I think I can communicate this better-ish via code-ish... effect<C> C // I understand this as: forall effects C. C-implement
impl<T: C Default> Default for MyStruct<T> {
fn default() -> Self {
MyStruct(T::default()) // <-- What effects does the body assume?
}
} effect<C> C
impl<T: C Default> Default for MyStruct<T> {
fn default() -> Self {
do_some_IO(); // <-- What effects does the body assume now?
MyStruct(T::default())
}
} I'm going to go nuts with the strawmanning and pulling ideas out of thin air now... These ideas may all be stupid, but here goes... // A hierarchy of effects as provided by the compiler.
// Probably a lot more to it but for now let's keep this as a
// "simple sub-effecting scheme" (think subtyping).
// Question: How does this compose with custom effects?
effect total { ... }
effect no_panic : total { ... }
effect const : no_panic { ... }
effect safe : const { ... } // Safe is default
effect unsafe : safe { ... } effect<C> C // The safe effect is allowed, but not unsafe.
impl<T: C Default> Default for MyStruct<T> {
fn default() -> Self {
do_some_IO(); // Legal.
MyStruct(T::default())
}
} effect<C: safe> C // Same as above.
impl<T: C Default> Default for MyStruct<T> {
fn default() -> Self {
do_some_IO(); // Legal.
MyStruct(T::default())
}
} effect<C: const> C // safe effect is illegal, only const and more restrictive is allowed
impl<T: C Default> Default for MyStruct<T> {
fn default() -> Self {
do_some_IO(); // TYPE ERROR.
MyStruct(T::default())
}
} effect<C: no_panic> C // Only no_panic and more restrictive allowed
impl<T: C Default> Default for MyStruct<T> {
fn default() -> Self {
panic!() // TYPE ERROR.
MyStruct(T::default())
}
} effect<C: total> C // Only total is allowed, there's nothing more restrictive.
impl<T: C Default> Default for MyStruct<T> {
fn default() -> Self {
loop {} // TYPE ERROR.
MyStruct(T::default())
}
} |
@Centril: Your use of |
Right right - the intended effect was "if you don't specify anything, |
There are a few other issues I have with how you've framed it:
Currently, Rust has three effects:
Today, This RFC proposes:
The discussion has also covered the following effects:
It may be worth extending Here's a baseline example of the structure, without any extraneous detail.
Here, we give up a default effect, namely
Here, we request an additional effect, one not present in what we depend on:
|
@glaebhoerl Such a "transparent"/"effect-inline" function would still be unrestricted at the definition side, but in generic contexts the viral nature would only propagate based on use, effectively gaining the C++ deferred checking of templates, but for effects instead of types. E.g. if you have an auto-derived So you would never have any preconditions for marking functions/impls/modules as such, it's purely a choice of "freezing" some/all |
Here's an exerpt, slightly edited to remove irrelevant stuff, from #rust-lang @ irc.mozilla.org. The full conversation is found here. centril || eternaleye: I used the constraint formulation cause that's the one
|| I'm most comfortable with expressing ;)
eternaleye || centril: Yeah, but it doesn't compose :P
centril || eternaleye: Yeah, I realize it was not the best formulation, it's
|| only for me to get my ideas across
eternaleye || centril: Anyway, I had to edit my post a good bit to fix drafting
|| mistakes, so maybe refresh
eternaleye || But does my overall framing of it make sense to you?
centril || eternaleye: safe => impure - isn't that just a renaming?
eternaleye || Impure doesn't correspond to safe
eternaleye || It corresponds to const
eternaleye || Anything that is not const is impure
centril || eternaleye: right, but essentially that was what safe in my "effects" meant
eternaleye || centril: Anyway, what you tried to do is a hierarchy, but currently Rust has three
|| _orthogonal_ effects
eternaleye || You only get a hierarchy at all if you introduce partial/panic
eternaleye || Because partial and panic have a subtyping relation (panic is a restricted form of
|| partial, which can only diverge by a single mechanism)
centril || eternaleye: that feels like inverting the names and order?
centril || what is the base effect?
eternaleye || Currently, Rust has `untrusted` and `impure` defaulted. If we added `partial`,
|| it would also be defaulted (pulling in `panic` as a result)
eternaleye || centril: There is no "base effect" - the base is the absence of effects
centril || eternaleye: ok, but what does the base entail?
centril || eternaleye: "bounding on an effect is covariant to bounding on its corresponding
|| constraint" <-- unpack this?
eternaleye || If you had C: ?impure + ?partial + ?panic + ?untrusted, then C would only permit
|| referentially-transparent (?impure) bounded (?partial) non-panicking (?panic)
|| functions that uphold their documentation invariants as a matter of ABI (?untrusted)
centril || eternaleye: does not ?partial => (?impure + ?panic) ?
centril || (?partial sounds to me like total)
eternaleye || Not necessarily
eternaleye || A function can be both total and impure
eternaleye || Consider accessing a static, but in a strictly decreasing way.
centril || but at the very least it can't panic...
eternaleye || You're getting it backwards. Partial implies panic, but the question marks don't
|| mean what you think they do.
centril || Yes, I'm having a hard time unpacking what you're saying
centril || ill read your comment in full and get back, sec
eternaleye || centril: C: partial means that C allows _all_ forms of partiality - C is "partial or
|| worse" C: ?partial isn't a bound, it's a syntax for taking off an invisible default
|| bound
centril || ah
eternaleye || C: !partial is what you were interpreting C: ?partial as
centril || yes
eternaleye || T: ?Sized doesn't accept only unsized types, after all :P
centril || eternaleye: right, that interpretation makes sense
eternaleye || The `forall` annotations on my examples are meant to illustrate that
centril || eternaleye: can you talk a bit more about untrusted ?
eternaleye || Sure!
eternaleye || That's for things like `unsafe trait Send`
centril || eternaleye: right, and that is different from unsafe fn and unsafe {..}
eternaleye || `unsafe` used that way is completely unrelated to `unsafe fn` in terms of the effect
|| it denotes
eternaleye || To the point that it's a restriction, rather than an effect
centril || eternaleye: right, it's unfortunate that it is annotated that way?
eternaleye || Its absence means "implementations may break promises the trait docs make"
centril || eternaleye: yeah, unsafe trait doesn't make sense as an effect - it doesn't do
|| anything at all really other than requiring a syntactic "yes, I know about the API
|| contract"?
centril || eternaleye: right, but it has no executable content?
eternaleye || It has executable content, it's just that the compiler can't verify that the
|| executable content restrictions were obeyed
centril || hmm, ok.. =)
eternaleye || It's still in effect though - a sufficiently careful language would make ptr::read
|| require that its argument was derived without an effect.
centril || eternaleye: so !partial => !panic ?
eternaleye || Yes
centril || eternaleye: but assuming it was in the surface lang you'd write C: ?partial instead?
eternaleye || Panic permits a subset of the behaviors of partial. Permitting all of partial's
|| behaviors permits panic's behaviors, and banning all of partial's behaviors bans
|| panic's behaviors
centril || eternaleye: right that makes sense
eternaleye || ? is no guarantee - neither a guarantee of presence nor a guarantee of absence
eternaleye || Sort of like T: Default, then invoking it with something that also supports Clone
eternaleye || Your code can't use clone, because your bound didn't include it
centril || eternaleye: so in the hypothetical surface lang you'd write C: !partial then?
eternaleye || But the code you call, behind the abstraction boundary, might
centril || eternaleye: given that we had panic and partial , what would be default-permitted?
|| panic + partial + impure + untrusted ?
eternaleye || Yes
centril || and so ?partial gets rid of the default bounds panic + partial ?
eternaleye || Anything else would be a compatibility break
eternaleye || No, it gets rid of the default bound on partial
centril || eternaleye: ah
eternaleye || centril: `effect<C: !partial> C fn foo(f: &C Fn())` means that foo's body and f _must
|| not_ be partial; the same with `?partial` means that foo's body must not be partial,
|| but f may be.
eternaleye || Because `foo` is _generic over_ partiality now (?partial)
eternaleye || But f knows whether it's partial
centril || eternaleye: aaah :P
eternaleye || Whereas C: partial is fixing partiality to be supported, and !partial is fixing it to
|| be _unsupported - neither is generic over partiality
eternaleye || Just like ?Sized makes you generic over sized/unsized
centril || eternaleye: can you also talk a bit about custom effects? "handlers" and whatnot
eternaleye || centril: Effects with handlers are really just a sugar on monads - you denote the
|| monad's constructors, so that the language can make them _look_ like they just return
|| the wrapped value
centril || eternaleye: the monads type constructor or the functions pure +
|| [bind/join/kleisli-composition] ?
eternaleye || centril: The type-specific constructor functiond
centril || eternaleye: interesting! =)
eternaleye || Some/None/Ok/Err/etc
eternaleye || Bind, join, etc. vanish
centril || eternaleye: Could you please edit/add a post with the things you've explained to me
|| thus far? It has been super helpful!
centril || Thanks <3
eternaleye || Because the language knows exactly where to insert them for you
centril || eternaleye: Also; any comments you have on what the surface language should look like
|| (including using the effects syntax if you like that) would be neat
eternaleye || The panic effect, in fact, is basically Result - `panic!()` is sugared `Err`, while
|| `return` is sugared `Ok`
eternaleye || Whereas with the partial effect, the Err side of Result carries a closurized recursive
|| call.
centril || eternaleye: I think the effects polymorphism in surface lang is very interesting to
|| have and certainly open to it
eternaleye || Agreed
centril || eternaleye: if you don't have the time to update the comment / add another one, can I
|| copy/paste the conversation into the RFC?
eternaleye || Though I don't think it's a tack that Rust will take
eternaleye || (much like mutability polymorphism)
eternaleye || Feel free
centril || eternaleye: hmm well, perhaps we can take a path that leaves us open to effect
|| polymorphism at a later stage syntactically ?
eternaleye || centril: I think that effect polymorphism is a bit too much for the strangeness
|| budget, considering the target audience of rust
eternaleye || I am 100% willing to acknowledge that I'm on the academic end of the target audience.
centril || eternaleye: is it tho..? Rust seems to have success bringing in pythonistas to
|| Haskllers and some C++ers - it seems to be a mixed bag
centril || eternaleye: yeah, so am I
eternaleye || While effect polymorphism would definitely be a stumbling block for people who are
|| immigrating from C++
centril || eternaleye: So perhaps we don't need to add polymorphism now, but make us compatible
|| with such syntax in the future?
centril || eternaleye: Btw... considering how easy it was to transition to effects-polymorphism
|| syntactically in the RFC-thread, I think we're not that far off
centril || eternaleye: we special case the keyword `const` as sugar for `!impure`, remove the
|| forall quantifier `effect <C..>` and we're home
eternaleye || centril: Sure, but then we can't fix #[derive(Clone)]
eternaleye || It needs ?impure
centril || eternaleye: (the universal quantifier makes me naturally ask: are there existentially
|| quantified effects?)
eternaleye || ST, maybe?
centril || it may be a question devoid of meaning, but..
centril || eternaleye: ST = ?
eternaleye || The ST monad
centril || oh that one
centril || eternaleye: so if we introduce `const == !impure` but also allow ?impure ?
centril || is that blowing the strangeness budget?
centril || but no universal quantification of effects
eternaleye || centril: My point is that #[derive(Clone)] basically needs const polymorphism
eternaleye || But it may not need the full monty
centril || eternaleye: by const polymorphism you mean?
centril || (in our effect<C: ..> syntax.. if you may)
eternaleye || (Strawman) maybe `const? fn foo<T: const? Default>() -> T` would work
eternaleye || And basically steal from lifetime inference
eternaleye || Avoids supporting multiple effect bounds
eternaleye || Or exposing effects as values
eddyb || eternaleye: ooooh
eddyb || eternaleye: that could maybe work for conditional traits in impl Trait too
eternaleye || eddyb: it does read pretty naturally...
eddyb || cc aturon nmatsakis woboats cramertj
eddyb || -> impl Iterator + ?Clone + ?DoubleEndedIterator
centril || eternaleye: so questions: 1. why the moving of ? to the end (to avoid parsing
|| ambiguities?) 2. const? means the same as ?impure ?
eddyb || or something of that sort
eternaleye || centril: 1.) Yes, plus legibility 2.) Yes
centril || eternaleye: cool, dumping conversation to RFC tread then =) |
@eternaleye Thanks for the links. It seems to be a formalisation of what has already been discussed above here, with nothing but very subtle differences in semantics. (I'll need time to digest it though.) Would that be wrong to say? |
No problem, that's fair enough. Thanks for introducing the formalism and past literature on the subject. I'll try to give it a read myself soon. |
@Centril asked that I post here about my use case for So I've got all these (*(*Foo).Field).OtherField // just terrible Now, as we all well know, de-referencing a raw pointer of dubious origin is unsafe (might point out of bounds, or it might point to invalid bits for that type even if it's in bounds), but to have rustc let us use auto-dereferencing we have to have a
|
@Lokathor How does this have anything to do with const traits? |
@alexreg Not much ;) Just effects and |
As I continue to look at random issues, I noticed this one: #1926 It's phrased as macros right now, but as it's just parsing, it feels like something that could definitely be handled with a One thing, though, that I couldn't figure out how I'd do, so it might be good to address in the RFC: how can |
I'm going to go ahead and close this RFC for now. To all those interested in this design space I recommend joining me at https://github.com/Centril/rfc-effects to help flesh out the design for the v2.0 of this RFC. |
using iter in const functions would be great. Has it all run out of steam (I do understand), or is someone still carrying the torch? |
Rendered
Allows for:
const fn
intrait
s.over-constraining trait
fn
s asconst fn
inimpl
s. This means thatyou may write
const fn foo(x: usize) -> usize {..}
in the impl even tho thetrait only required
fn foo(x: usize) -> usize {..}
.syntactic sugar
const impl
forimpl
s which is desugared by prefixingall
fn
s withconst
.const
bounds as inT: const Trait
satisfied byT
s with onlyconst fn
s in theirimpl Trait for T {..}
. This means that for anyconcrete
MyType
, you may only substitute it forT: const Trait
iffimpl MyType for Trait { .. }
exists and the onlyfn
items insideare
const fn
s. Writingconst impl MyType for Trait { .. }
satisfies this.