Things I hope we will do…
const trait { fn default() }
)const trait { [const] fn default() }
)Things I don't expect to do…
The keyword generics line of inquiry began with the observation that Rust has a lot of "modal" things–like unsafe, async, const–where you can apply a keyword to a function f
and then f
can only be called within a certain context. This in turn led to investigation in to academic work on effects.
An effect can be thought of as "side-effects the function has on its way to computing its result". A side-effect is basically some sort of interaction with the environment – could be allocating memory, reading or mutating global variables, panicking, etc. Anything that falls outside the normal (param1, param2) -> output
signature.
If we stretch the definition, we can include things like "infinite loop" as a side-effect (the function never returns the value) or even unsafe
(function does something that requires caller to prove add'l bounds beyond what a safe function would provide). Opinions vary on how well unsafe
in particular fits. Not important for this doc.
const
, which indicates the set of things a function can do either during compilation or runtime, vs runtime
, which indicates the set of things a function can do only at runtime. The typical fn
can be seen as having a default set of effects, const+runtime
.comptime
, for effects that can only occur during compilation (e.g., obtaining layout information from a TypeId
).async
being the canonical example, which modifies the function to return an impl Future
.async fn
, and we use ?
to propagate an error). In contrast, marker and intrinsic effects by their nature are "transitive" (if I call a function that panics, it.. just panics).One of the Questions I'd like to settle today is what effects to focus on. My position is that we…
Focus initially on a known set intrinsic and marker effects. Leave user-defined or transformer effects to be considered in the future. They are more complex but also raise non-trivial impl concerns due to the transformations they require. Virtually everyone sees the value of marker effects – and our experience with them will inform whether we believe effects could be extneded to handle transformer effects or not.
…I don't expect a lot of controversy here, so we don't have to spend time on it, but good to know.
Semi-independently there has been a desire to extend const
functions with the ability to be generic and invoke trait methods. This is required for key operations like operators ==
.
Numerous techniques were tried to implement const traits in the compiler. Most were dead-ends but they taught us some lessons. There is a particular 'feature contour' that fits very well with the compiler:
We did some initial formalization of associted effects in a-mir-formality. The goal was to create a formal notation that aligned with the implementation limitations of the compiler (or modest extensions of it). This is not intended as a proposal to extend Rust, but rather to give us a shared vocabulary. More work is needed here but we made enough progress to document it.
First, associated effects as members of traits. The values of these effects will be specified by impls, allowing the impl to choose what effects the methods in the trait should have.
Second, the value of these associated effects is an effect parameter E
defined by
const
– things that can be done at compilation timedefault
– superset of const
that includes things that can be done at runtime<Ty as Trait>::Effect
– an associated effectE1 + E2
– union (least upper bound) of two effectsThird, all functions are annotated with a list of effects, where the default
effect is explicit:
Using this, we can define the trait Default
fully:
and we can write some impls
Finally, we add the ability to put a bound on the effect in a trait, so that we can support "must be const" requirements:
We do not require other forms of bounds for the basic const subset.
Tyler Mandry made the observation that effects could capture the work that Luca Versari was doing on supporting code that is generic over what set of target features it requires. The motivation is to support an in-progress Rust port of the libjxl library from C++ into Rust. That library makes use of Hwy
, a C++ library for writing SIMD code. Hwy lets you write templated code like this:
The choice of type D
controls what SIMD opeations are used, and the idea is that the user will first test what's available and pick something appropriate. In Rust, this might be written like so:
…and the idea is that the implementation of D::load
etc would be written with target-features enabled:
The problem is that it is unsafe to call these methods unless you've tested for being in avx512
. Of course we can make the whole trait unsafe but this feels like something that could be covered by the safe type system with only a mild extension, so that would be unfortunate.
In particular, you can extend our formalization to cover this concept fairly easily:
There is one additional piece required, though. Because the main
function does not declare the avx512
effect, we need some way to make that effect available. This didn't arise for const fn
because the default effects on main
were a superset of const
. One way to do this is to have some kind of unsafe "effect masking block" that lets you write a block that can have effects that are hidden from the outside:
I've written this using a mildly odd notation to emphasize the scoping of unsafety: masking an effect is unsafe, or at least masking this effect is, because if you are wrong, you will get illegal instruction errors. Note that checking whether avx512 is available requires OS support and isn't really suitable for building into the language as a primitive[1]; also some effects (e.g., comptime
) are not suitable for masking. But we don't want that unsafety to "infect' the contents of the block itself, it should be limited to the assertion that avx512
is safe to use.
do default + avx512
), and that seems awkward. In practice I imagine we would want to define a floor (e.g., fn
has default
by default) and then layer add'l effects on top (e.g., #[target_feature(avx512)]
adds an additional effect to whatever floor is defined). I'll return to this theme.The original const trait RFC, covered briefly below, gave rise to a series of explorations of how to expose const in the surface syntax. The original proposal gave a clear minimal semantics and a strawperson syntax, but those concepts hadn't been exposed to the lang team for very long. So this gave rise to a lot of exploration.
The original const proposal had generic functions like this
which desugar to something that is const if T
implements Default
in a const way:
Users could also write T: const Default
bounds which would require a const impl. That translates to T: Default<Effect: const>
in our formal notation.
Reviewing this proposal led to a number of questions and investigations:
const
and ~const
and how should they?T: ~const Default
is the behavior people will want most often. Should we just spell it T: const Default
?~const fn
instead?I believe some of those have "firm answers", others are worth discussing today. We'll cover those in the next section.
const
and ~const
and how should they?Before we dive into the back and forth, I want to put forward a model for two ways of thinking about const
that are both quite common. My goal here is mostly descriptive – I want to capture the way people think about it so that we can figure out what will be intuitive. But it's also mildly prescriptive in that I think we want to nudge people towards intuitions that will let us generalize from const to other marker effects.
Rust today currently permits const functions:
This means a function that can be called at compilation time (but also runtime). This distinction is occasionally important. The body of a const fn is not known to run at compilation time. In contast, the body of a const item or const expression must run at compilation time:
I've seen two major ways of thinking about constness, both of which I think are compatible.
One way to think of a const fn
is that a "const-safe function" is one that can be called from const blocks. This is a familiar "syntactic pattern" in Rust, e.g., an unsafe function can only be called from an unsafe block and an async function can only be called from an async block.
This intuition can lead you astray when you think about effects, because it can make you think of const as an extra effect, but in fact it's a narrowing (a const fn
has fewer effects than a fn
, despite being written with a keyword). Nonetheless it is probably the predominant way people think about it, so it's good if we can ensure our notation is largely compatible with it.
Another way to think about a const fn
is– it is a function that cannot have side effects, or that can't interact with the operating system. If you think of something like eprintln!
or Box::new
, these are things that "can only happen at runtime". Many users have an intuitive sense for what "couldn't possibly happen at compilation time". This sense is likely too narrow, in fact (e.g., we can create a miniature operating system that supports allocation if we want to, but we have to answer some thorny questions), but it's there. So you can think of a const fn as one that avoids those things.
This intuition leads you pretty directly into thinking of consts as effects.
In this model, const
means "always const" and ~const
"maybe const". I'll cover this a bit in the question about whether we should write const fn
or ~const fn
, but the tl;dr is that I think is not helpful, and we should nudge people towards one of the previous two models.
T: const Default
mean "maybe const"?In the original RFC, most const fns will want to use ~const
trait bounds like T: ~const Default
. const
bounds like T: const Default
are only needed if the methods are going to be used from a const item or a const expression. Given that, there was a proposal that T: const Default
should mean "maybe const" when used on a const fn
:
This approach has some appeal for consistency. const fn
is already "maybe const" in the sense that its body may run at compilation time or runtime (unlike, say, a const expr) and there's a kind of inconsistency between the const
in const fn
(may be safe to call from a const block) and the const
in T: const Default
(must be safe to call from a const block).
But this approach gives rise to other contradictions and in particular conflicts with the "const safe" mental model. Remember that, in that mental model, const means "can use in a const block". Now consider this expanded example:
It is quite surprising indeed that T: const Default
does NOT mean "can be used in a const block" but rather "can be used in the const fn".
For that reason, I believe this approach should be ruled out from further consideration.
~const fn
when there are ~const
bounds?There is a variation on the "side-effects" mental model that doesn't totally fit with this proposal. I'll call it the "const-maybe-const" mental model. In this model, things are
When you look at a function like
you might then conclude that the function is "const safe" because it says const fn
. But in fact it is "maybe const", since there are ~const
bounds.
One could remedy this by saying that instead of const fn
, you should write ~const fn
when there are ~const
bounds:
But this proposal, while not unworkable, has some notable downsides:
It feels clunky. The syntax feels clunkier – the ~const fn
at the start of the line catches my eye, at least, in a way that is noticeably heavier.
It creates syntax combinations that make no sense. The design also gives rise to combinations of syntax that don't make sense:
~const fn foo() {}
– what should this mean? Is it equivalent to const fn
?const fn foo<T>() where T: ~const Default {...}
– what should this mean? If the user calls T::default
, it has effects that are outside of const
.It's not a blocker to have the possibility of inconsistent syntax, we can just flag errors, but it is a "design smell". You can see this inconsistency in the desugaring, where there are two cases:
const fn
desugars to do const
always~const fn
desugars to do const + ...
where ...
are the associated effects from the boundsThe key observation is that if there are no ~const
bounds, then the two expansions are equivalent, which is why it feels like we are "stuttering" a bit by requiring ~const
both at the beginning and in the where-clause list.
Conclusion: My conclusion is that leading with ~const
(or some similar notation) is an additional bit of consistency that doesn't buy us anything. It is a concession to a "slightly wrong" mental model, and it'd be better to lean into the potential confusion and explain in terms of side effects (not the academic notion of effects, mind you). i.e., a const fn
can only do const things in its body, but it can call ~const
trait methods and those methods might do const things. That makes it "const-safe modulo the bounds"
~const
or something else?As I said, ~
has history in Rust – and our experience has strongly been that unfamiliar sigils make people feel like things are harder to understand, overall. We explored a number of syntactic options:
~const
– the original proposal. The ~
suggests "maybe" or "approximately".(const)
–
()
for grouping, e.g., impl (Foo)
is legal today.[const]
– meant to be similar to EBNF
[]
Discussion topic: Of these, I believe [const]
hits the sweet spot – concise, nonintrusive, uses familiar characters, and leverages existing conventions in EBNF and elsewhere. The main downside is that it's already used for arrays, which means at minimum we should avoid []
(empty effects) and, I think, suggests that we may want to be careful about something ambiguous like [T::Foo]
– is T::Foo
an associated effect or an associated constant? I'm going to use [const]
from here on out, but other options can be swapped fairly easily.
The original proposal put constness entirely on traits – but with other effects or effect-like things, we put it on individual methods. For example, just as we have traits with functions that are always async or never async, we may want traits with functions that are always const or never const:
There are of course many more varieties, which can to be known as the "quadrants" because there are two axes (is the impl a const impl, and do the generic parameters on the method have const impls):
Default::default
, whose constness depends on the implconst
or notconst
implsWe went fairly far down this road, but I've personally backed off from it. The turning point for me was fee1-dead's write-up of learning curve concerns combined with the sheer syntactic overhead that was imposed on simple cases. For example:
I think this is a case where "less is more" – repeating the [const]
everywhere is rather overwhelming. In theory it makes all the connections explicit, but I think in practice it will simply be more confusing, as it's very hard to follow at a high-level without really understanding everything.
Also, this proposal included generality (const
methods, etc) that would not be included in the initial implementation and stabilization. That's ok but not optimal. I'd rather align the RFC with what we will stabilize if that still forms a coherent set of functionality.
Conclusion: I believe that if we stabilize const traits (see the next question for the alternative), then we should start with a more minimal proposal similar to the original RFC. I'll include my proposal below.
In the previous section we discussed the various "quadrants" that might be considered and how it seemed like that design was getting too complex for its own weight. One way to handle that is expose the "building blocks", putting the flexibility into user's hands without promising a slick package.
Specifically, we could design a syntax that maps very closely to our formalization. This hasn't seen as much syntactic exploration, but I'll put out a strawperson proposal here for what it might look like:
fn
is default
const fn
is const
pure fn
#[do(E)]
where E
is
const
default
T::Effect
or <T as Foo>::Effect
avx512
or whateverE + E
do Effect = E
T: Trait<Effect: E>
on those boundsUsing these primitives we can write our usual examples:
and for SIMD:
Under this system, when a function is declared with the effect avx512
, the compiler would take as a signal to use different codegen, so the explicit #[target_feature]
annotation is not needed.
For target features, we would like some way to assert that some effects are allowed. We have precedent for this – or at least something conceptually similar – which is unsafe
blocks. These are a way to say "this block of code takes actions that are not judged safe by borrow check, the code author takes responsibility for checking they are compatible". We could have some form of block that says "this block of code has side-effects that are not tracked in the fn signature, the code author takes responsibility for ensuring they don't cause problems". And indeed this operation, at least for target feature effects like avx512
, must be unsafe, since the logic to check if a given target feature is safe to use is outside Rust's purview (and sometimes OS-dependent). Not all effects can be asserted: imagine we have an effect for something that can only happen at compilation time (comptime
). That could not be "asserted" at runtime, it doesn't even make sense, we can't generate code for it.
What syntax shall we use for this? This hasn't received much exploration, I will propose here as a strawperson an annotated block:
Note that this really wants the ability to scope unsafe to just the annotation.
I intentionally excluded generic effect parameters from this description. This ensures that all effects are "minimal", as in, they reflect exactly the things a function may do – instead of the caller saying "I'd like you to allow yourself these effects", the callee is saying "here are the effects I have". This simplifies implementation and avoids the need to integrate effects into trait dispatch and other parts of the compiler. However, in the long run, we will likely have to consider effect generics to cover some cases, notably non-hard-coded effects on dyn
and fn
:
(Credit to TC for providing this example.)
This brings us to the major decisions for today:
const
traits proposal?
[const]
or some other alternative?
[const]
and to get an idea of who I should circle up with for a deeper dive.My recommendations and reasoning:
Q1. Yes, we should focus on marker effects, for the reasons I gave. Most of the interesting questions come up, they are very strongly motivated needed, and it avoids some implementation complexities and general controversies that would slow progress.
Q2. We should do a maximally minimal "top-down" solution for const
first and then explore a "bottom-up" approach to cover target features.
In particular, I think we should land a maximally minimal const traits proposal that isn't meant to cover target features and then start to explore how to layer on associated effects to cover target features.
My reasoning is that we will want a way for people to do const traits that doesn't require them to understand effects (so we're not adding complexity). I think we've explored the space well enough that we have a sense for what that should look like; and we've also got the impl ready to go and it will let us make incremental progress.
There is another, practical reason: not moving this work forward will be majorly demoralizing, both to implementors and users[3], and it's unclear that we will have anybody funded or motivated to do the work to explore the "bottom-up" proposal (particularly if we demoralize the people who've been working hard on the const proposal). I don't think this is a strong enough reason on its own to make a wrong decision for the language, but since I think that a dedicated const syntax is the right decision for the language, it's worth considering.
Q3 and Q4. I like [const]
because it is visually familiar ([]
), builds on the EBNF tradition, and "just reads well". I'll cover my specific proposal in the next section.
I believe we should (a) introduce const generics and specifically the subset described in All That Is Old Is New Again, I concluded we should adopt a mental model of methods inheriting the effects declared on the trait. We can always regain flexibility by having the declare additional effects beyond that baseline. So then we have this:
which I feel is much more approachable. The explanation for users becomes:
[const]
bounds are also const-safe.[const]
bounds in scope.I also think after this we should look at extending to cover target features, probably through a more explicit notation of associated effects. Certainly we'll want to explore that on the impl side.
As the author, I gave my take. =)
Generally I like the proposal. Starting with marker + intrinsic effects is what I think we should do. Solving the problem of const traits should be the priority.
If we start with the top-down const Trait
, which I'm happy to do, I think we should still consider the implications for transformer effects like async
. I added a discussion topic about this below. I would also feel a lot more comfortable if we can show our formal model can extend to those.
As for the syntax, my vibe is positive on [const]
. I was also fine with (const)
. I'd be at least a little bit sad if we end up with ~const
, but in a situation where we ruled out the alternatives for good (read: not nitpicky) reasons, I can live with it.
I agree with Niko's recommendation. In particular, I very much agree with the top-down approach. The language should have a dedicated const
syntax, with minimal complexity.
I am unsure if we will want to go on to have a more complex effect system, and in particular whether we'll want to have user-defined effects. I agree with the observation that 'effect generics raises concerns of Rust "jumping the shark" when it comes to complexity'.
If we don't go on to have a more complex effect system, we shouldn't have the complex syntax of one. If we do go on to have a more complex effect system, we should still have a simpler sugar for a case as common and hopefully pervasive as const
.
With a library team hat on, I do hope we have enough of effects to be able to write, for instance, a map
function that encompasses try_map
and async_map
. With a lang team hat on, I'm uncertain if even that subset of an effect system would have a manageable complexity. But, I'm enthusiastically in support of people trying experiments and designs, with the idea of "keep complexity under control" as one driving principle. Rust has historically managed to take surprisingly complex concepts and present them in a fashion people can work with.
Finally, vibe check on the bikeshed: to
[const]
, much better than (const)
. I'd also take ~const
if that was everyone's preference; that's still better than (const)
. We regularly end up needing cross-pollination between type syntax and expression syntax, and I expect us to have more in the future; for that reason, (const)
should mean the same as const
. (There's a reason our one-tuples are (x,)
rather than (x)
. Though I wouldn't want to see (const,)
either.)
The more we talk about this, the more I really do just want to start with something closer to the modeling – here, Niko's second suggestion of the explicit associated effects notation – while limiting what's allowed to match what's implementable today.
There are, I think, fewer questions and bikesheds along that axis than along the other. Maybe we will later want some elision for this, but doing it a bit more explicitly first will help us to build up the intuitions about what's needed there and how that should be done.
Probably I think about this as though we had tried to avoid adding explicit associated types and generic type parameters to Rust, initially, and instead tried to do everything in terms of impl Trait
. We would have ended up with things like,
and we would have said to ourselves, "maybe the input impl Trait
should always be connected to the output impl Trait
, such that they're the same type." And we would have had to try to come up with all sort of elision rules for this, and I'm just not sure we would have gotten that right the first time. I'm happy we didn't do that and went with something more explicit first.
And so, on that note, even today, when we start talking about things like this,
it just starts being not totally obvious to me how all these elisions tie together. I really just want to name it.
As for how to approach this, I'd prefer to see an RFC that covers a lot of the ground. Particularly around the more explicit dimension, the language semantics seem relatively clear. We can then incrementally stabilize, starting first with what is closest to today's implementation, and following with what's needed to support the other top-down use cases that we want to support.
We did something like this for precise capturing with use<..>
. The RFC covered the whole ground, even though many parts of it, e.g. precise capturing of types, were hard to implement, and we've been stabilizing incrementally.
- Should we focus on marker effects for now?
Yes.
- Should we work "bottom-up", starting first with associated effect declarations for maximum flexibility, or should we work "top down", starting with the
const
traits proposal?
I'd like the design to be bottom up and to stabilize use cases top-down.
- If we do opt to top-down, what const proposal should we use?
That still seems hard.
- And, only if we settle all of those questions, let's bikeshed:
[const]
or some other alternative?
The more we talk about it, the more concerns I have about (..)
or [..]
syntaxes due to the similarity of this problem to conditional trait bounds and the problems with these syntaxes in expression position.
I love going for marker effects, in fact I don't think we should ever go further than it. However, as discussed below, I think it is good to keep the syntax flexible enough to put identifiers in since we can't perfectly predict the future. Even for things like target feature effects which are built in that seems wise.
I initially wanted to say: start bottom up, it is hard but we'll get the better feature in the end.
It's already too easy to work ourselves into a corner and we don't have so many choices anymore with current rust.
However, before we start we should have more of the feature worked out.
However:
I do like "max min" top down with const getting some precedence.
So we have a tiny bit of const, maybe not associated, then make associated work, and then go to more effects. Const traits are something we badly need.
A comment, use fewer attributes, have some plan to remove them in the future in favour of actual syntax. Having worked on them so much, I wish we did more to incorporate more of the language in the syntax of the language. No need to start with that, but at least think about what options we have there.
Other concerns I had, I asked in the discussion topics below.
Regarding attributes: a lot of features have been bolted onto the attributes in Rust, partly because we call them "attributes" and allow them in kind of random positions, like C compilers does, since C23. However, because I suspect that attributes in the C compilers are handled quite a lot differently because they seem to be much more incorporated into certain syntactic structures, as opposed to being more like metadata attached to things. However, like Jana, I think this is going to be a bit of a dead end: I don't expect us to embed attributes as deeply into the language as C does, partly because we also use it as our alternative to a preprocessor (#[cfg]
and such), whereas C has no conflict on that point because the preprocessor is technically its own byzantine sublanguage.
About demoralization, I literally spoke to someone the other day where they expressed that they don't think that Rust will ship const traits within 15 years. I told them to not be ridiculous, the language hasn't even been around that long. It would be much more fair to suggest we might not ship it within 10.
From my perspective, it really feels like the more top-down const trait approach is "ready to go", almost, once we hammer out a syntactic detail or two.
T-lang has become very fond of talking about its "north star" for this or that, but I have a question: what is your north star for an entirely bottom-up design? What does that enable? If it's "make sure it's something we can represent in the compiler"… I am not sure if that concern accurately reflects the current tradeoffs from the perspective of T-compiler members? I know T-lang talks about managing regret from bad decisions, and instituting process to reduce that, but the flipside is that you can easily wind up exploring and spinning your wheels forever if you are not willing to commit.
I like the bottom-up approach that Niko mentions at the bottom (because I expect this to be the easiest way to implement a good, general approach), but I don't necessarily think the stabilization approach should be bottom-up (this might chase off a bunch of bikeshedding, possibly).
In particular I also see the importance of working towards stabilizing things top-down, in part for motivation and in part because I would want to see feature-effects happen not-too-late :-)
TC brought up difficulties expressing certain effects/situations, full generic syntax
Josh: I think the things that would cause issues are exactly harder scenarios to which we might not want to extend this syntax.
TC: What you're describing here concerns me more, not less. I want to make sure what we have is scalable. If when we go to extend to to the places it makes logical sense we start getting scared that it's not obvious enough, then that gives me pause.
Josh: I am actually in favour of having a formalism underneeth. Where we maybe disagree is: should we stabilize anything that resemble the formalism? I'm all for having a formalism, just don't want to ship it right now.
oli: Exploring the formalism is something we definitely should do. But right now stabilising wouldn't give us anything – I can do a PR that will do all the associated syntax and everything for the formalism within a week. But we won't actually gain anything – we won't know if the formalism does anything better or supports more things.
oli: So I still think we should ship this current thing. We should block things like RPIT const bounds. Yes to building an alternative syntax to what we have, but we should ship what we have.
TC: My concern is about the way we express the formalism in the language – and that's what we gain by being more explicit. We still need to get the formalism right; I agree that how we expose that formalism doesn't help us if we get it wrong.
Niko: Two things coming to my mind: one is presentation – if we had this minimal version, I see const trait Default
– I'd be like "ok, that's not terrifying". If I see the do Effect
I might be concerned. The other is: how certain we are we'll get past the basic subset. I'm sure we will – don't know about the timeframe. But I'm wary of having the syntax we're exposing to everyone be do
particularly if we don't go much beyond const generics.
Josh: I think this may depend on the assumption on where we'll end up eventually. If we start with the assumption that we will have generic effect system at some point – then it makes sense that we need the formalism and syntax then surely we need this. If we start with trying to solve shipping const and possibly other things, then the idea "we have to prove we scale a generic effect system before we support scale" then
Tyler: I think that's a good observation. I've ran into a lot of things where we do run into limitations of the language, so I don't take comfort from the idea that "this example is too complicated for the language to express". TC's example made me think of conditional Send
ness. I think formalisation is important. Making sure we have consistent model, and making sure we have concepts that compose well.
Eric Holk: It's worth thinking about whether we're going to the full generic effect system or the smaller-scale cost + things. The square bracket looks nice but it's really complex. If we add the do Effect
later, we'll be adding two complex syntax additions to the language.
oli: I agree, it'd be great if we just had one. But I don't see us getting there quickly. And I think having a simple syntax for const will unlock so much. Going forward with RPITs and other things – they're much less common and smaller.
TC: I want to acknowledge and express my appreciation for all the work oli's done on this over many years. It's been a considerable endeavor.
TC: With respect with what Josh mentioned, when I worry about Rust getting too complicated, I worry about that in the sense of C++ and not in the sense of StandardML. These are different kinds of things. StandardML is hard; it's high concept. But C++ is sprawling. It added a lot of special-case solutions rather than general mechanisms. Every organization that uses it has rules about what subset to use.
TC: For Rust, it's this C++ style of complexity that I think is the major risk for us. We have a lot of places where an effect system unifies the solution to problems: target features, const, panic freedom, scoped allocators, reactor hooks, etc. We could handle these all with one-off solutions, but, for me, it makes the language simpler if we aim for a unifying solution that is a bit more high concept.
Tyler: That's a useful distinction in the terms of different kinds of complexity. I want us to be careful about making mistakes. But Rust has editions and we can fix some of the mistakes over an edition. We have more degrees of freedom than C++ did and does.
Jubilee: Editions are great for fixing "we made a mistake in the syntax" – and a lot of the discussion here is about syntax. A part of the reason I'm interested in const parameters is that it'd be very possible to experiment with modeling target features on top of const
.
Niko: I appreciate your points, TC. Tyler, the point about editions is also good. That argues for keeping the syntax small – the more limited we are in what we syntactically accept the easier it is for us to back away from it later.
Niko: I wonder what data we'd get from having const traits available. Do people have things they think they'd learn from it?
Josh: I think we're very likely to find that a quarter of the ecosystem will be marked as const. As a general "how do we make next steps thought", whether we end up stabilisning sugar for effect generics, we should star with what Niko's proposing here, and use that as a concept of what sugar we can support. And if people want to build the more general purpose and see how to express the sugar in that way. And we can build and stabilize the sugar in parallel. Each of these things should stand on their own merits and we shouldn't require one to demonstrate the other is viable.
Tyler: I think we'd learn a lot from stabilizing anything. Both of these points on the design space are highly useful.
TC: I agree with that.
Niko: I'll write a summary of the main points. TC: I liked your ML vs. C++ comparison.
(The meeting ended here.)
jana (verbally): The formalization includes the const
and default
and mentions effect unions, is all of that needed if we are just modeling const?
nikomatsakis: The formalization was done with the expectation of generalizing it to add'l effects (e.g., target features), but the initial set of effects is just looking at what is needed for the const traits RFC. That said, even there, you need to account for unions because of associated effects. For example:
TC: In the doc, const
is labeled as an "intrinsic" effect, but I think it's actually a marker one. Or more specifically, it's the absence of the marker effect runtime
(and perhaps of other marker effects we haven't named), in the same way that the absence of (the marker effect) ndet
, etc. makes for a total
function.
nikomatsakis: Fair enough, comptime is the better example. I don't think it matters much, I debated if that was even a distinction worth including.
TC: I'd call what the doc calls "transformer effects" instead "control flow effects" and say that certain transformations are part of how we implement these. In another language, these might be implemented with segmented stacks, e.g. In ours, we implement them with opaque types that "carry" a (typically coroutine-style) state machine that consumes resumptions and emits effects.
nikomatsakis: I don't love the transformer name. Maybe control-flow effects is better. The one point I didn't really get into in this doc – in part because I wanted to rule those out-of-scope – is the point about sets versus stacked transformations and how taking a set view on things makes the question of Iterator<Async>
or Async<Iterator>
just… ill-posed, there are just two distinct effects that can occur ("yield to scheduler" or "yield to inner loop"). I think that's a subtle but very important point for avoiding some of the painful problems that occur to monads and so forth.
Jana: here it seems extra important that you can do something if the target feature is not enabled. Possibly prove this at runtime and have the effect available for some part of code. Some effects can be implicitly handled like at main
nikomatsakis: I think this is naturally covered by if
/else
, right?
Jana: fair enough, though now you're forced to do unsafe while we could have a primitive for that
nikomatsakis: well, we can't– or at least – I don't want to start with a primitive because the tests for features can get quite involved. I do think eventually we might want some way to have users be able to supply code for testing if an effect is available or something– though you could also do this by having a marker type representing the feature and a trait is_available()
or something. Anyway, I agree it'd be nice, just not required for MVP. <- jana: totally!
Jana: fair enough. Some kind of function returning like a "proof" of effect availability.
Jana: This might be lack of context, but, was there ever a way worked out to represent "only const", i.e. never at runtime. This is relevant because we're still stuck with the situation that const adds a qualifier that removes features. This is an unavoidable corner we worked ourselves into because the default in rust is that a function does have effects.
nikomatsakis: That's what I called "comptime". I think it can be introduced into this framework without great difficulty but I've not put a lot of thought into it. It seems basically equivalent to the avx512 effect and so forth – i.e., those change how the compiler generates code, but comptime also changes it – by making it a hard error. =)
jana: nods
Josh: Trying to check the mental model here: it seems like you can view this operation either from the inside out as "masking" the effect from a block so it can be run without having the effect, or from the outside in as "introducing" the effect to make it available for code inside the block to use. Are those duals of each other the way they appear to be, or is there some fundamental reason this should be thought of as "masking" rather than "introducing"?
nikomatsakis: I think you can think of it either way. The main thing is that the effect does not "propagate" out from the block to the surrounding context (this is why I called it "masked"). I think that the idea of having a block that "makes the effect available" is better than some of the prior, more masking-oriented versions I thought of (e.g., transforming a T: avx512 Default
to a T: Default
), in terms of fitting the language better.
josh: Good to know that "introducing" works; thanks for clarifying. Another mental model might be "discharging" an effect, for that matter; that might make sense for unsafe
or async
or try
.
nikomatsakis: Yes. I think that "introducing" and "discharging" map to the same underlying construct, so to speak, but are like different purposes to which it can be put to use. I do think the avx512 and unsafe map pretty closely: like, when a function has the effect avx512, it is essentially putting an extra burden on its caller to ensure that these instructons are available. Seems not unlike unsafe
. (Side note: I know that Ralf objects to unsafe being considered an effect, but I don't entirely understand why – unless it's that the real effect if like… the things that you read/write or otherwise do that require justification; I can kind of see it but it also feels like a pedantic argument to me. I probably am missing something. Haven't dug in because I don't think it matters right now.)
[const]
syntaxoli: one conflict we may have in the future is if we have non-keyword effect names. [nopanic] Trait
will parse as "slice of nopanic
type elements" followed by syntax error because slices don't have more types/traits/paths after them
jana: I do like a "delimited" syntax though, parens or brackets alike. square brackets might be quite similar to attributes too, not just array/slice types.
nikomatsakis: Currently we don't allow trait names in the same places as types without some "introduction", e.g., impl
or dyn
, correct? But we would be doubling down on that. I guess that for fn
pointers also we need to consider it.
jana: Parens have a similar problem where (ident) will just parse as that ident, i.e. a parenthesized type.
nikomatsakis: yes. Of course we could also do [do avx512]
or something to disambiguate. I think it's also entirely possible that we just don't wind up doing a lot more "floor" like effects.
jana: I like [do effect]
and don't mind that it's a tiny bit more verbose. I think we might not, but it'd be a mistake not to plan for it since it's easy to work ourselves into a corner like that. Especially if we choose some sort of top-down model to developing it like you describe at the end of the text above.
Josh: Without going into detail on how, are you confident that in the model you could extend to generic effects (e.g. handling Result
, or handling "maybe async
") in the future?
nikomatsakis: You mean like a try<E>
effect?
Josh: Well, two different kinds of "generic" apply here. There's the effect being generic (try<E>
), and there's being generic over effects (map over a function that might be async
).
nikomatsakis: I guess the short answer is yes.
nikomatsakis: Longer answer: the effects as given already have types in them, which is the major complication that something like try<E>
brings over async. When it comes to async, I don't think the model has trouble with it, there's some user-facing decisions we have to make about how we do things, but I think this is compatible with various proposals that have been made (e.g., the do
-like proposal I discussed at some point). One of the questions I'm interested in is whether a const trait Default
, as written, can be extended to also permit (e.g., async Default
), or if more is needed. I remember having insights around this question and I've forgotten what they were. The core idea being: when you declare a const trait
, you're already essentially saying: "my base functions will only do const things, but I am also going to call these methods that may do other things", and I don't think it matters very much if those other things are "await to the scheduler" or "panic" (obviously there are impl differences to consider, but from the model perspective, I don't think it matters).
const Trait
when you meant [const] Trait
?Josh: It seems extremely likely to me that someone will write a bound of T: const Default
when they meant to write T: [const] Default
. What would a lint look like to catch this? Could we detect if your function body doesn't actually require const Default
(e.g. it doesn't call Default
in a const { ... }
block)?
nikomatsakis: It seems very lintable.
oli: clippy already has a "this function could be const fn
" lint. That duplicates some work from rustc checking function bodies, but it's pretty robust. Similarly we could add a lint to rustc to look for actual const
uses of const Trait
bounds, although the uses may be hidden. So we'd likely need to do sth where we change the bound and re-typeck the body, which is likely very expensive. So we'll have to do some experimentation. It's a similar issue to "do you need T: Debug
on your function? it doesn't use that trait bound".
const fn
syntax to other effectstmandry: I generally agree we should start with marker + intrinsic effects (I've been saying so myself), but I also want to look ahead:
If const fn
can have constness conditional on its bounds, would we ever want async fn
to mean the same? If not, how can we resolve the ambiguity?
nikomatsakis: Solid question. Maybe it'd be a good idea to write a follow-up and spell it out. One question I'd like to dive into is "how much certainty do we need here" – formal model makes sense, I don't want to force us to know the syntax in advance.
oli: is this actually going to solve the problem TC is worried about? We can change the current syntax (or add a second syntax for it) to support associated effect bounds just for the cases we can support right now. But that doesn't actually guarantee we can support more things (even tho I strongly believe the current system is 100% forward compatible to it). The only way we'll know for sure is to actually implement associated effects inside the compiler. Otherwise we're just giving the appearance of associated effects working as intended, which obviously doesn't help anyone.
jubilee: Yeah, we have a working model for const trait bounds but I'm not sure it is as easily extensible to very-differently-shaped effects.
Send
tmandry: I want to consider this too. The example TC gave
makes me think of something like the following
tmandry: I'm trying to remember right now why traits need to opt in to constness instead of being able to drop const
in front of any trait and implement that.
jubilee: Yes, Amanieu has a design in mind for adding whether SIMD attributes have been checked for availability to the core library which would be hypothetically possible to add without requiring the operating-system level support (and may help for cases like embedded where you are the operating-system level support, and can personally directly enable or disable a feature). ↩︎
jubilee: Probably you do in fact want target_feature
or some other qualifier for a few different reasons, because the identifiers for features are not guaranteed to be unique between different architectures as they are very fond of reusing random three-letter acronyms and AVX512 isn't just AVX512, it's actually AVX512F + AVX512BW + AVX512VL + AVX512… you get the idea. And that set of features "only" gives you "the ability to use AVX512 with all element granularity you might want." The non-guarantee for non-reuse kinda means that you only want to have strings, or you only want to have them be accessible as true with-path identifiers instead of magic keywords. ↩︎
re: demoralization ↩︎