Const Trait Counterexamples
14 points by beef
14 points by beef
proposal 5: academic zealotry
This section really rubs me the wrong way. For example…
If we wanted to unify them to make our version of effects closer to formal modeling, we must do so for everything that we currently have, not just const traits.
…this is already a big project goal! People are working on formal models for references, for the borrow checker, for the trait system as a whole, for everything!
Perhaps this is just a difference in how we’re using terminology, but if we wind up with a design for const
traits that can’t be modeled formally, or would be extremely onerous to model formally, something has gone very, very wrong.
This is not to say it should match any particular existing formalism exactly or even closely, but I also don’t think that’s really the idea you should take away when people bring up those formalisms. Rather, the idea is that these formalisms have already done a lot of the work of mapping out weird corner cases and interactions that can arise, depending on the set of features they start with. Often the most useful outcome here is that we learn about a new choice that can be made, which otherwise we would blunder into with our eyes closed. It would be a waste to ignore all of that prior art just because Rust can’t pull one off the shelf and deploy it unmodified.
Designing a language with your head down and without regard for the larger problem space is how you wind up with an incoherent mess that doesn’t learn from others’ mistakes.
Yeah, it rubs me the wrong way too. I think this section is in response to the keyword generics proposal… but I find it rather strange to talk about it as “academic zealotry” when I’ve not really sure there’s been any discussions with PL researchers with regards to this proposal (from what I’ve seen as an outsider at least). I think these are exactly the kinds of people who would be useful to have in the room when designing things around const
. For example, I would love to understand why viewing const
as an effect makes sense - it doesn’t seem like an effect to me.
Whatever’s going on with keyword generics, I agree that the author’s disdain for PL research does seem quite concerning. A big part of the success of Rust has been a result of taking inspiration from that work, and I hope that continues into the future. Maybe this is just a communication issue as you say.
Is there some way to phrase that section better? Just to re-iterate, in that section, I was arguing that PL research should not hold as much weight as we currently give them in language discussions. That’s because PL research isn’t always applicable to the specific issues we’re trying to solve. I would love PL research much much more if every result is a silver bullet for the corresponding problem in all production-grade programming languages.
The idea is not that const
is an effect, but rather runtime
, or “the ability to call non-const functions” is an effect. I have worked on multiple MVPs in trying to get the effect model into Rust, e.g. making every function generic over the effect, making traits generic over that effect. In one MVP, we made it so that Trait<const>
and Trait<non-const>
were different traits to be implemented, but then that led to many issues. In the next MVP, we made it so that the effect is represented as an associated type on every (const) trait, with requirements for effects validated based on that associated type. That was directly after our compiler team on const traits talked to researchers working on boolean effects (and they mentioned the idea of using associated types to represent them). I’m not on lang team so I don’t know whether they have talked to PL research people.
We had to scrap the effect desugaring model in the end because it was too complex to maintain and, iirc, had performance issues.
Just to re-iterate, in that section, I was arguing that PL research should not hold as much weight as we currently give them in language discussions. That’s because PL research isn’t always applicable to the specific issues we’re trying to solve.
Ahh yeah, I don’t think industrial PL work should be beholden to academic research, nor vice versa. I do think it’s healthy to remain in contact with the academic community, and have ongoing discussions, and to bear in mind a pathway to formalisation even if you can’t do everything at once. The fact that you are at the point of framing this as “academic zealotry” in a blog post suggests that something has gone wrong in that relationship (which might not be your fault), and I hope you can repair that trust over time.
I would love PL research much much more if every result is a silver bullet for the corresponding problem in all production-grade programming languages.
I think it’s fine to not wait for academic research to exactly fit the specific problem that you are working on, but I do think it’s a good idea to at least bear it in mind, and there to be ongoing conversation between with theory and practice. Keeping in contact with researchers can be useful as they can help to guide you to work that you might not be aware of, or point out issues before you realise it. Your questions might also prompt them to be curious about investigating something that you’re working on as well, if they find some gap that you’re trying to fill. You do have to be wary that researchers are human, and sometimes have pet interests that they struggle to look beyond, so it’s important to not rely on single perspectives.
our compiler team on const traits talked to researchers working on boolean effects
I’m not familiar with boolean effects tbh! I’ll have to think a bit about how I feel about the approach you mention in your comment, but I think it’s understandable to drop those if they’re not working for you. Traits are already incredibly complicated as is (their complexity is one of the things I least like about Rust, tbh), and I can appreciate the difficulty in integrating something new into them, especially without some formal model of them.
My misgivings with regards to framing const
as an effect (e.g. in stuff like the keyword generics stuff) was that I usually think of effects as monadic, but const
does not behave in that way. E.g. you can’t write a function like fn pure<T>(x: T) -> const T
, which takes a runtime value and converts it into a compile time value (similar things are expressed in this thread on reddit, for example). Re-reading this section of your blog post, it seems like you already address this difference towards the end which is nice to see, and exactly what a good PL person should do!
The fact that you are at the point of framing this as “academic zealotry” in a blog post suggests that something has gone wrong in that relationship (which might not be your fault), and I hope you can repair that trust over time.
Well, if you allow me to elaborate on the motivation for writing something like that in the first place, as I mentioned in my comment below, I’m on the compiler team. It turns out that many people on the lang team have backgrounds in (or are at least more familiar with) academic PL design while I don’t. (I could get more familiar with it later but that’ll probably have to wait since I’m in ugrad rn :P)
In language design discussions, there are multiple times where academic work on effects has been cited as justification for syntax. Like, instead of T: ~const Trait
in the proposal, use T: do(_) Trait
, or instead of T: ?const Trait
, use T: do() Trait
. But I can’t realistically see that happening if we don’t propose anything other than const using this scheme. (How on earth do we explain this to people learning Rust, if this is only used for const traits?)
On the other hand, there are multiple times (where I’d only speculate as caused by at least one of: staring too much into formal models/academic papers, or spending way to much time trying to force const
and actual effects e.g. async
and avx512
to unify) where the academic work has been used to justify concrete MVP proposals that can’t work, such as the different proposals listed on my post. The “academic zealotry” section is aimed at people who want to simply Ctrl+C and Ctrl+V semantics from an academic paper without looking at specific drawbacks those semantics imply.
At the same time we get people who just want to keep things simple and think these features aren’t worth the hassle or complexity or effort.
At the same time people (including me) just want to ship something usable and get the feature stabilized as fast as we can (which does not mean ignoring future extensions).
At this point I really appreciate if there are researchers willing to talk about how they solved how compile-time-callable functions interact with traits in their work. (especially since Rust requires explicit trait impls and not implicit ones like just requiring having methods with the same name and signature) But I don’t want it to serve as a distraction from discussing the actual, concrete proposals at hand.
Is there some way to phrase that section better? Just to re-iterate, in that section, I was arguing that PL research should not hold as much weight as we currently give them in language discussions. That’s because PL research isn’t always applicable to the specific issues we’re trying to solve.
Then my disagreement is not one of phrasing in the first place. I agree with brendan above:
A big part of the success of Rust has been a result of taking inspiration from that work, and I hope that continues into the future.
I also disagree more specifically that research on effect systems isn’t applicable to the problem of const
in traits. My impression of initiatives like keyword generics, and the more recent things you describe (“every function generic over the effect,” Trait<const>
, associated const
-ness) is that these are indeed not a very good fit for Rust, but neither are they very representative of the most successful styles of effect systems I see in research.
We are obviously not going to hash out a solution in a lobste.rs comment thread. But I have yet to see any convincing attempt to even consider the sorts of solutions that already exist to the kinds of problems described in the post.
I suppose on that note you’ll have to forgive my ignorance. Although the post does not primarily focus on the issues the const traits feature aims to solve, but rather the different semantic choices and their negative implications, I’m still always open to being convinced of different semantics if they can work better.
One of the main reasons we’re proposing this without specifying an effect system is that an effect system would be very big, and I don’t think the lang team has even 10% of the capacity needed to flesh that out and make that happen.
I did fail to specify in my blog post that the const traits project group is actually a compiler subteam and we get no authority nor responsibility over the language design, even though we’re actually the people who arguably made the most progress on this feature so far. I’d be really happy if someone could just come along and figure everything out, but that’s not what’s been happening.
One of the main reasons we’re proposing this without specifying an effect system is that an effect system would be very big, and I don’t think the lang team has even 10% of the capacity needed to flesh that out and make that happen.
This I agree with. What I would hope to see is not a fully formed effect system, or even a desugaring to an internal effect system, but rather an approach to const
in traits that plausibly meshes with all the other effect-like things Rust is already doing with async
in traits (and potentially gen
), as well as hypothetical extensions to const
itself (e.g. stuff that only works at compile time and not runtime).
The typical approach here starts by giving function types and signatures an additional component- a collection of effects that exists alongside the parameter and return types. I’m not referring to anything syntactic like T: do() Trait
here, but purely the notion of using function types to track the operations they may perform when invoked. (Though inventing a fake syntax just to be able to talk about this could still be useful!)
At this point there is immediately a lot of variety in the research: is that collection a set or a list; how does it interact with subtyping and generics; what impact does it have on where and how a function can be used? These are the kinds of questions that the research explores, and they are also the kinds of semantic choices you’re talking about in the post.
For example, the issues around the const
-ness of impl
s and the const
-ness of individual trait methods strongly remind me of this paper on checked exceptions: https://www.cs.cornell.edu/~yizhou/papers/exceptions-pldi2016-tr.pdf. The throws
part of a function signature in Java is just like a collection of effects, and the main limitation of Java’s checked exceptions is that there is no way to be polymorphic over them- indeed this is one of the biggest benefits people cite about the Result
approach to error handling. The paper describes an extension to checked exceptions on interface methods that is reminiscent of the polymorphism we want from both async
in traits and const
in traits.
As another example, since the annotation and type system complexity burdens are a concern, I am also reminded of this paper on effect polymorphism without effect generics by way of second-class functions: https://se.cs.uni-tuebingen.de/publications/brachthaeuser20effekt.pdf, as well as this followup that brings back the expressiveness of first-class functions: https://se.cs.uni-tuebingen.de/publications/brachthaeuser22effects.pdf.
I don’t think these examples or similar ones will just immediately slot into Rust. But I do think that surveying the ways people have solved the same and related problems is useful- it gives you more angles to look at the problems from, it can point out potential pitfalls you may have missed, and it gives you more starting points to come up with your own ideas. I think this is often where suggestions like T: do() Trait
are coming from, or should be- not prescriptions of syntax, but ways of framing the problem that might lead to new insights about how to solve it.
I don’t think I’m arguing for a version of const traits that can’t be modelled formally. People have proposed many different ways to model our existing semantics.
The problem was that many people were using their formal models, proposing that we switch to semantics/syntax closer to their formal models. That’s not an invalid proposal in and of itself, but as I argued in that section, it should be treated the same as evaluating a proposal to “Make Rust’s compile time more like Zig’s”.
I must be wrong, but it still seems to me that the best path forward is to allow calling whatever in const contexts, and error out if specific thing tires to FFI or something. This does mean that you can’t semver check constness, but it feels like this issue is better solved through ecosystem culture (the same way we solve unsafe) than through sublanguage.
Since constness is part of the type, this would mean that now you can’t typecheck a function without looking at the body, including transitively for all of its callees. Unsafe doesn’t work the way you’re proposing either, or else this would compile:
unsafe fn do_nothing() {}
fn foo() { do_nothing() }
Could you give a litmus test example for illustration?
It seems like we already have something like this:
fn foo<const N: u32>() -> u32 {
const { N - 1 }
}
fn main() {
foo::<1>();
foo::<0>(); // Doesn't compile.
}
Furthermore, I don’t think [~const]
has any impact here? If you leak results of comptime computations into types, there’s no way to not run the computation to get the type. And, being computation, it can fail, so you need to account for that. ~const
assures that computation can’t fail due to ffi, and that’s valuable, but doesn’t really allow us to not run the computation?
Ah, I forgot about const generics, you’re right.
I think one nice property of explicitly-marked constness is that it provides a better error experience in the same way that requiring explicit trait implementations provides better errors than C++/Zig-style “post-monomorphization” checks; if I have a trait where some types have a const impl and some don’t, it’s easier for me to parse “MyType doesn’t implement ~const Default
” rather than getting an error because a five-deep nested struct calls some non-const method in its Default
impl.
That’s not to say that it doesn’t have downsides, of course.
I think allowing const for everything is quite different from this example, though. For starters, you can’t know, when one of your recursive dependencies starts making FFI calls or does something we can’t support in const eval yet (e.g. allocating on the heap might also be an issue). But making “whether the body performs non-const operations” a semver property is not an option. So people who rely on this will just have to deal with breakages, which also isn’t ideal.
Yeah, my hypothesis here is that dealing with breakages after the fact using ecosystem culture would be less pain overall than dealing with breakages proactively by encoding no ffi/no syscalls property in the type system.
I’d expect the rules-of-thumbs like:
In libraries, use consteval sparingly, think about whether it would always make sense for the function you call to be const-evalable. In applications, feel free to go to town, provided that you are willing to deal with potential breakage proportional to how much const-eval abuse you do.
I realized that I am confused! What does C++ do?
There’s some discussion here: https://old.reddit.com/r/cpp/comments/ceyxta/why_the_need_for_constexpr/
My understanding is that:
constexpr
doesn’t mean that you can call code at comptime, only that you might be. More precisely, absence of constexpr
means you can’t call at comptime#include <iostream>
constexpr int f(bool b) {
if (b) std::cout << "See you at runtime!";
return 92;
}
int g(bool b) {
if (b) std::cout << "See you at runtime!";
return 92;
}
constexpr int a = f(true); // error
constexpr int b = f(false); // works (!!!)
constexpr int c = g(false); // error
So it seems this is how they avoid the need for ~constexpr
! This actually makes sense, letting the library authors say “I don’t forbid calling this function at comptime”.
EDIT: another interesting link to peruse: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2448r2.html
Two things:
I think our language design has tried its best to avoid post-mono (or during-mono) errors. We have type system checks to ensure that each generic function calls only provably callable/instantiate-able/well-formed functions. For the const-eval system to now abandon that would not be a good idea.
I also think our language design aims to treat SemVer seriously. So we have (mostly) clear rules for which changes are semver compatible and which changes are not. And now this would introduce semver breaking changes in transitive dependencies. A lot of the design concerns for some language proposals that we had pop up because we treat SemVer seriously. I don’t want us to abandon that either.
It seems to reduce the complexity of language design but it just offloads that complexity to downstream users. (Who, in economists’ terms, will always read every piece of documentation we ever publish about these features before using them)
I also think this is a relevant question about designing systems and the social contracts within them. We already seem to have a culture of expressing lots of disdain for people who don’t bump major versions when making semver incompatible changes. Not treating breaking changes seriously in language design means people will stop treating breaking changes seriously (because who gets to define what counts as a breaking change? Why are these rules so silly?). And you’d just dismiss people who just want their things to work. (probably fine for C/C++ people because if my hearsay impressions are correct even the compiler gets to break user code somewhat frequently)
This is one of those features that I think is better to just not do at all. It feels similar to the ever growing template madness in C++. It will make Rust more complicated and it is already a slightly over featured language. Moreover, “const” is being retrofitted onto Rust by means of an entire sublanguage. Once going down this road every language feature will need to be made “const” compatible. I predict soon we’ll have const async. Is there a way built into Rust governance to have a counteracting force to adding new features?
Just now getting around to reading this article, I’m wondering why the “maybe Const
” syntax is different from the “maybe Sized
” syntax (i.e: why is it written ~const
rather than ?const
).
It is brought up in the first section, as ?const
and ~const
are different, but I am failing to really understand the point.