-
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
Const functions and inherent methods. #911
Conversation
😍 |
|
||
# Unresolved questions | ||
|
||
Should we allow `unsafe const fn`? The implementation cost is neglible, but I |
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.
s/neglible/negligible
This RFC does not go into nearly enough detail as to how CTFE is supposed to work to be actionable. What are the limitations? What subset of the language does it allow? How is it implemented? How do we extend it? What about allocation and I/O? |
@pcwalton I thought much of your questions (e.g. about its limitations and what subset of the language it allows) were covered by this text:
E.g. do we allow allocation and IO in const expressions today? I mean, yes, we haven't formally defined what the subset of the language is that const expressions allows (to my knowledge), but it seems reasonable for this RFC to be written in this delegating manner. Update: I admit, the (sketch of a) definition quoted above leaves completely unspecified how const function calls themselves work; e.g. whether one is statically restricted to an acyclic call graph, or if recursion is allowed. (whether we can afford to adopt this feature is an entirely separate question, of course.) |
@eddyb I assume you allow recursion in your function calls (rather than restricting the call graph to be acyclic), though I have not verified that assumption via the implementation. Did you add a recursion limit for the function calls in the const evaluator? I recommend the RFC allow the use of a recursion limit (or, if you prefer, allow the DAG restriction, though I suspect that is less palatable from both the implementation side and from the user's perspective). |
(I hope @eddyb comments more on this detail later himself, here or in the RFC text, but over IRC, he just pointed out to me that attempting to write a useful recursive function in the scheme as proposed would probably be impossible, because we do not support |
} | ||
``` | ||
Having `const` trait methods (where all implementations are `const`) seems | ||
useful, but is not enough of its own. |
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 do not understand what you are trying to say here. Is it useful? Is it not sufficiently useful? (And what is the extension being proposed here, anyway? That implementors are restricted to only making const forms of these methods? Does that somehow sidestep the requirement to use the const
qualification on the trait bounds?)
Despite having myself submitted an RFC that punts on this question, I would like to see a description of what is/isn't allowed in a const function, even if it just documents what's allowed today. For one, it should be specified whether or not const functions will follow the rules about references from RFC 246. Other things that I could probably guess or easily test about the current implementation, but are not set down in any obvious location:
A fairly important question, though one that can be deferred to a later RFC, is which library functions should be made Then there are traits:
|
I agree with @pcwalton that the details are too vague. In general I feel like all the proposals in this vein are motivated by the desire to get "something" into the language as fast as possible without considering future design implications of coming up with an ad-hoc sub-language and baking it in before 1.0. Personally I think we should show restraint on picking a design for this too early. There are more interesting and powerful directions we could push the language in if we wait for some of the post-1.0 design to shape up. As a language that borrows from many different sources I believe we should also consider more than a single design before adoption (and this feels way too much like C++), and there are many other ways to do this. We could for example move towards singleton types, a very interesting idea that is implemented as a library in Haskell, and has both a library implementation and a prototype in the compiler in Scala. This combined with a couple other features is much more powerful than the proposed extensions and doesn't require inventing a new sub-language with restricted semantics. As a final point, regardless of what happens, the definition of the allowed sub-language is incredibly important. Unless you only allow a restricted subset like @pnkfelix describes above, we most likely need to require the sub-language to total and pure. If not the termination of type checking will cease to be a guarantee and personally I find this feature to be way less useful without recursion (arbitrary loops are probably out of the question). If we allow recursion I don't want to introduce non-termination into the type checking of my code by calling some one else's malicious "const" function. Although probably just an academic exercise to many the dependent typing community has thought through these issues in depth, and many of the restrictions imposed by dependently typed languages are to enable the full relaxation of the phase distinction between terms, and types. Any design that gives us the ability to express constructs like CTFE moves us close in that direction and we shouldn't ignore years of thought, even if our final design is quite different. |
You can get if-else even if you don't allow const fn if_else<I, E, T>(cond: bool, iff: I, elsef: E) -> T
where I: FnOnce() -> T, E: FnOnce() -> T {
// Haven't actually tried to compile this, but you get the idea.
let index = cond as u8;
[iff as &FnOnce() -> T, elsef as &FnOnce() -> T][index]()
} so I think allowing recursion basically means we are promising we are going to add the rest of control flow, which leaves us with many of the questions brought up in this thread already. |
@reem 👍 to the general point (even if we can't do virtual methods as @eddyb notes). I think that is a good point out that once you add general recursion all control flow is possible which is the problem. Since we can't solve the halting problem, if we want termination we must restrict ourselves to using only primitive recursion (i.e we can only recurse on syntactic sub terms). An example is: #![feature(box_syntax)]
enum List<A> {
Cons(A, Box<List<A>>),
Nil
}
fn map<F, A, B>(xs: List<A>, f: F) -> List<B> where F: Fn(A) -> B {
match xs {
List::Nil => List::Nil,
List::Cons(x, tail) => {
// This works because I can show that I'm moving towards termination
List::Cons(f(x), box map(*tail, f))
}
}
}
fn main() {} |
@reem You are calling a virtual method there, which is far from being allowed. |
@eddyb true, but you get the idea. |
# Alternatives | ||
|
||
* Not do anything for 1.0. This would result in some APIs being crippled and | ||
serious backwards compatibility issues - `UnsafeCell`'s `value` field cannot |
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.
There are ways around this, for example: stability checking will not trigger on code that's directly generated by a std
macro, so we could have the field be marked unstable
and a macro like
macro_rules! unsafe_cell {
($e: expr) => { UnsafeCell { value: $e } }
}
This would ensure that UnsafeCell
values can be constructed via unsafe_cell!(foo)
, but not allow direct access to the value
field in the stable channel.
Downside: it would introduce an unsafe_cell
macro to global namespace. If/when we get a more structured way to construct such values, the body of the macro can be replaced with that, and the macro itself can become deprecated.
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.
That is, sadly, incorrect - that field (and a bunch of others in std::thread_local
, initialized by a macro) had to marked stable.
I would very much like to see stability work correctly with macros (because of things like format_args!
), but I'm not sure how much work it is (cc @cmr).
EDIT: I was wrong, things have changed since I last looked
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.
They don't have to be marked stable. rust-lang/rust#22803 now marks them unstable and compiles fine (initially it marked them stable but that was unnecessary).
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.
(There are some bugs with the stability checking of macros in general, but I'm most of the way through a patch fixing it, as we speak.)
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 am sorry, I was basing the previous statement on the assumption that the std::thread_local::imp::Key
fields were stable for a reason. Apparently, they were not.
I was also confused by the presence of stability attributes, even though they were actually unstable, not stable.
If my understanding of this and a skim of the reference impl is correct, the RFC is to allow something similar to the below:
Which would, as @eddyb points out, allow us to hide implementation details while still allowing users to use |
@Ryman that is entirely correct. I plan to properly update the RFC to reflect the main goal better, ASAP. |
@jroesch However, I don't think that it's as obvious that it should be total. Rust does not guarantee that type inference will terminate today, nor do I expect that it ever will in the future (except in the most practical sense, via recursion limits). In fact it is pretty trivial to use associated types to generate an infinite type stack, which in practice leads to the compiler blowing past the stack size limit (or it did a few months ago when associated types were young; I should check on what happens now, and maybe file a bug report...). If type inference is not guaranteed to terminate, is it still beneficial to make a guarantee about const functions terminating? |
@reem I did not think when I posted my message that a lambda-calculus style encoding of if/then/else was expressible in the |
@quantheory It isn't very difficult in principle to fix the non-termination problem with type-level computations using associated types. Haskell's type classes with type families are similar but certain restrictions are enabled by default which limit instances to a decidable fragment unless you specifically opt out. See here. IIRC, the intention was to do something similar with Rust at some point.
|
I was not aware that anyone intended to do this. If this is not already planned for 1.0 (by which I mean, someone has a design right now), I doubt that re-instituting a guarantee of termination will happen in the foreseeable future.
I agree. |
As far as recursion is concerned, I don't see I see no reason why In both cases, if the arbitrary limit is too low for a specific use case, there is or could be an attribute for increasing it locally. |
One (perhaps minor) drawback of this approach is similar as with (Also, there is no scenario where it would be legal to run |
@glaebhoerl Sadly, we need to mark this property in the API, to be able to abstract away the contents of the function. As for |
Based on the discussion here, we are merging this RFC for now. Note that the feature is not intended to be stable for 1.0 or any particular release. This gives us room to experiment with |
According to the RFC, we will mark each implementation of a trait as |
@nodakai Quite the contrary, trait methods cannot be |
@eddyb Hmm, I must have misread something... It was clearly stated trait methods can't be
|
Amend #911 const-fn to allow unsafe const functions
Edit #911 - The order `const unsafe fn` was chosen (rust-lang/rust#29107)
|
@alexchandel what happens if a function in some API I'm working on happens to be const for a while and then I change it in a way that makes it non-const? The |
@alexchandel It's not that simple
|
@sfackler I'd rather see it treated like @golddranks 1) This is still possible, though it might be easier with a separate phase. 2) Not substantially. It might be done very easily by assuming functions are const until proven wrong. For example, Idris assumes totality until proven wrong, but doesn't require it. 3) Yes it does. It's addressable in the short term by marking constructs like |
@maninalift Hmm; Was this a comment on RFC #2237 ? |
Good point - I wasn't aware of that discussion - it looks like most of what I said has been said there. I will take the time to read it. |
Current RFC text
(above added by @pnkfelix)
Rendered
Reference implementation is at rust-lang/rust#22816.