Pre-RFC: Function properties

Pre RFC: Function properties

A function property is a thing that functions can have if any function they calls have that property, plus additional restrictions specific to each property.

There can be several function properties (The exact set of properties doesn't really matter):

  • no_panic
  • no_oom
  • no_alloc
  • no_recursion (can also be used to enforce a maximum size of stack at compile time)
  • no_unbound_loop
  • const (the current const fns, constness of a function is a function property)

Function properties are annotated via special @prop syntax:

@no_oom
// @no_panic is not valid, since x + 2 may overflow
@const // or simply const, since const is a keyword
fn foo(x: i32) -> i32 {
    x + 2
}

Function properties can be considered as inverse of an effect system. For example, if we consider panic an effect, no_panic means lack of panic effect.

Function properties enables compile time checking of them, and guarantee them in library apis. And it can enable analysis for other language features, for example if we annotate sizeof<T> with no_panic, no_recursion, no_unbound_loop and const, we can use it in

struct Foo<T> { bytes: [u8; sizeof::<T>()] }

without wellfoundness concerns.

Too many annotations everywhere

In order to reduce the burden of annotations, some shortcuts can help:

  • no_fail: no_panic + no_oom
  • terminates: no_recursion + no_unbound_loop
  • total: terminates + no_panic

In addition to that, properties can defined on modules or inherent impls, and they will be applied to each function.

Trait functions and generics

We can apply a function property on a trait impl, and all of functions will get that property (like an inherent impl). In generic bounds, @prop Trait means that impl of the trait needs to have that property. So we can write this function:

@no_panic
@no_oom
fn foo<T: @no_panic @no_oom Trait>(t: T) {
    t.trait_func();
}

But it will restricts the callers of foo. We want to allow impl of T: Trait to panic if caller of foo doesn't needs no_panic. Generic properties solve this problem:

@P ~ { no_panic, no_oom } // P is a subset of { no_panic, no_oom }
fn foo<T: @P Trait>(t: T) {
    t.trait_func();
}

This means foo has property P which is a subset of no_panic + no_oom, depends on implementation of the Trait. There is currently an unstable ~const that solve this problem for const fns in a different way. ~const Trait can be non-const in normal context and should be const in const context. A function is const regardless of what it is called with. But with generic properties, a function is const or no_panic if the trait satisfy const or no_panic. These are just different points of view and in action they are equal, like:

const fn triple_add<T: ~const Add<Output=T>>(a: T, b: T, c: T) -> T {
    a + b + c
}

is equivalent to:

@P ~ { const } // or P ~ { const, total, no_fail }
fn triple_add<T: @P Add<Output=T>>(a: T, b: T, c: T) -> T {
    a + b + c
}

We can add ~prop trait as a sugar, or even use it instead of that generic property syntax described above. But personally I found above syntax easier to understand.

2 Likes

I'm pretty sure the language is still a few years away from the point where the lang team will seriously consider effect systems.

11 Likes

Since there is already one of them (const fns) in the language, what is the problem of introducing similar things, with minimal additions? The forward compatibility problems that will arise are probably present today, with const fns.

1 Like

I agree with @PoignardAzur above. But while we're listing "effects", no_alloc would also likely be of interest. Some property which correlates with "interrupt safe" may also be interesting.

One problem I see is that attributes are now getting tied up in the syntax much more intimately (with the proposal here at least).

I also see a problem with having to annotate every API with umpteen properties (and add new ones as they arise) speeding up the MSRV floor fairly quickly because APIs not marked as such cannot be used in new code until it has been updated for it.

Open questions:

  • is there any scoping for property names?
  • must the attribute match the @name?
  • how are collections of properties resolved? (#[no_fail] fn foo<T: @no_panic @no_oom Trait>() {})
  • can crates define their own properties (or at least give names to collections thereof)?
    • if so, can they be used?
    • how complicated will error handling for #[unrecognized_custom_property] be? (is it a proc macro? is it a property?)

I think the biggest issue with the proposed syntax is the novelty of it. Taking a new sigil (@) and tying it up with attributes is completely new and I'd like to see more investigation of alternatives on that front (assuming the end goal is even feasible right now).

1 Like

I changed the #[prop] fn to @prop fn, which I think address most of your points (thank you).

I consider no_fail as a property alias, so this would compile.

I think this is the effect system that we are few years away of, but this should try to be forward compatible with user defined properties/effects.

What properties do Iterator::next and Iterator::map have?

1 Like

No. Just plain, no.

To recap: the process of submitting an RFC with a request for a new feature, one needs to provide ample motivation for it as well as how it will be taught, taking into consideration the complexity budget to show the benefits outweigh the costs.

Yes, effect systems are a cool theoretical concept that rust actually already experimented with before (pre rust 1.0) and rightfully removed it.

It is not sufficient to have a cool idea. It is crucial to understand the costs in complexity vs the benefits gained. For a general purpose language such as Rust this adds a lot of complexity for very little benefit.

Rust already pretty much maxed out on its complexity budget with the borrow checker which has far greater core benefits. And there's a whole lot of effort to mitigate the costs such as the effort invested in improving documentation & the compiler error messages and diagnostics. And yet, Rust is still known to have an initial steep learning curve and newcomers need to fight the borrow checker for a while before they grasp the concepts or give up.

Effect systems are a good tool when you are willing to take the complexity costs. At which stage reaching for a dedicated tool such as coq is the better choice. For the common use case of a general purpose language, this is simply not necessary and just makes the language that much more complicated.

6 Likes

Iterator is a trait, so its impl decide about it.

This is not the famous and scary effect system. You probably have something like this in mind. This is almost like noexcept from c++ or throw signature in java (even java's one is way more complex). Do you consider noexcept a complex and cool theoretical concept?

If you are referring to this, we have it currently in stable rust. Just impure fn is now normal fn and fn is now const fn (and unsafe is unsafe, which isn't no longer an effect).

Little benefit? Just see the count of threads about no panic in this forum. no_recursion can be used to compute maximum stack size and prevent stack overflow statically. no_oom would guarantee that we don't accidentally call code that can cause oom when it is inappropriate (this RFC also solves this problem by removing oom functions via cfg, but it should be used for a whole crate) and usability of const expressions in generics that I mentioned above is also useful.

3 Likes

It depends what you think it should do. As it says on cppreference,

Note that a noexcept specification on a function is not a compile-time check

So it's actually essentially the same as putting let _guard = AbortOnUnwind; at the beginning of the function body in Rust. (In C++ it calls std::terminate if the noexcept function actually throws at runtime, basically the same as the abort that Rust would do.)

And thus yes, if you want "enables compile time checking of them, and guarantee them in library apis" I do consider it complex. (And I'd expect it to actually be properly checked in Rust -- the same way that C++ concepts are mostly a hint but Rust generic bounds are actually checked.)

Does it sound nice? Of course. But we still haven't even figured it out for const yet -- see all the conversations about ?const and ~const and impl const Trait and such. I agree with @PoignardAzur that new effects are unlikely any time soon. I think we need better behaviour-unification of const and unsafe before considering more.

8 Likes

My point was simple. You have not demonstrated at all the practical real life usefulness of your suggestions, nor that it merits the additional costs to the language.

As others have pointed out, yes, I too consider this both very costly and pretty much useless in C++ as well as a more principled approach more idiomatic to Rust. You seem to ignore the costs entirely. Each property you add to the function signature increases the cognitive load exponentially.

I do not need this level of reasoning for the vast majority of use cases in a general purpose language. This harms productivity and onboarding ability considerably. As I said, If I did need this level of reasoning to prove correctness in a specific area than I would have reached for a dedicated tool such as coq.

Const does not mean pure as was intended in the old system. I also am on the record objecting to const functions. The recent discussions about adding additional syntax cases such as ~const just proves my point. It is a mountain of complexity pilled on top of an already complex language with no consideration for the enormous costs involved.

There is a reason why at my day job, a C++ shop, everyone is so enthusiastic about moving to Go, including management. Yet Rust is relegated to a tiny insignificant slack channel and not even considered for future projects.

You're arguing the equivalent of saying that having a flying car would be cool. Yes it would. This does not mean we ought to give flight lessons to all the 16 y/o so they could borrow their parents' flying pick-up truck to take their date to the prom. Nor have you considered the complexities of managing air traffic or parking for that matter. It is much more practical and efficient to just book a flight with a commercial airline company.

You have not provided any motivation as to why it would be useful in a general purpose language for say a junior engineer at $company to need to reason about no_oom or no_panic. Especially since this would spread like the plague in the code considering that array access for example would panic on out of bounds and that memory allocation is a core crosscutting concern.

4 Likes

Anyways, we have the need for unification and thereby an abstract effect system; Moreover, Rust is low level language, and we are notacibly constrained in implementation.

My bet is that we can extend labels to support at least our current effects. But anyways, effect system both needs and deserves its own primitives: they are to be developed yet.

Please note that they aren't always equivalent. ~const Drop bound is invisible when called from non-const fn, but from const contexts this bound will check whether it can be dropped in const contexts. Removing const when called from runtime makes T: Drop which requires an explicit impl of Drop.

I fully understand your concerns regarding the complexity. I'm already a bit scared that rust added so many features since 2015 that it is a bit too complex already.

On the other hand, some features are useful in some use-cases. That's why new features are added.

One of the solutions could be to have a (as small as possible) set of generic features. For example, macros are one of such features. I believe (I cannot prove that though) extending macros to be also postfix could simplify async/await implementation.

And since we have const, what kind of complexity would we add if we made it more generic, e.g. there would be other effects handled similarly to const? const fn syntax couldn't disappear but we could still have @{const, nopanic} or #{const, no panic} or #[prop(const, no panic)] just like we have #[allow(...)] etc.

1 Like

I agree with this part. Yes, we need to have a small set of orthogonal and composable features.

This is incorrect as I've explained and illustrated in previous comments. It is not sufficient to have a feature that solves a problem. The bar to add features to a general purpose language, especially a complex one such as Rust, should be way higher. It should be at least: the feature is useful for most people most of the time.

1 Like