# Effect as bounds on behavior Niko's proposal made me rethink how I conceive of effects. Allow me to riff on a mental model focused on subtractive effects like `const`, `nopanic`, `noalloc`, `nothreads` etc. ## Frame Effects, like lifetimes, could be inferred whole-program; we choose not to. A signature is therefore a contract: by writing `const fn`, I: - commit (compiler-checked) to never do a runtime-only operation in my function; - which allows you to use my function in const context. `const impl` is similar: I commit to a bound on this impl and future changes of it, which you can therefore rely on. ## Traits Without traits, all is easy. The fun starts with traits: 1. A trait declaration may want effect bounds on some or all methods (`const fn method();`); 2. A trait implementation may want a stricter bound than the declaration (`const impl Default for u32`); 3. A trait declaration may add future default methods, hence must commit to a bound on these methods' effects; - An impl thus cannot declare a stricter bound than this since it may unknowingly carry these default methods in the future. 4. A function may want to bound the effects of a trait parameter (`T: const Default`); 5. A function may have behavior conditional on its trait bound methods' behaviors, because it intends to call those methods (`~const fn foo<T: ~const Default>()`); 6. `Drop` is normally implicit but we must be able to talk about its runtime behavior; `T: const Destruct` and `T: ~const Destruct` serve this purpose. Design decision: trait bounds cannot talk about the behaviors of individual methods (RTN-like), they can only talk about a global bound on all method behaviors. ### Nightly behavior in this model (I think!) - a `const fn` can have `T: ~const Trait` bounds, and declares it will never do runtime behavior outside of the behavior of the trait methods on such `T`s; - a `T: const Trait` bound requires an impl that declared none of its methods would ever do runtime operations; - this implicitly requires the trait declaration to have declared that future default methods would also not do runtime operations. - a trait can individually bound each of its methods to be `const`, just like normal fns. This is orthogonal to constness of the trait itself; - a `const trait Trait` can have `T: ~const OtherTrait` bounds, and declares that none of its default methods will ever do runtime behavior outside of the behavior of the methods from the `~const OtherTrait` bounds. - note that this is only a commitment on default methods; - this is necessary for impls of this trait to commit to a bound on its methods. - `const impl Trait for X` can have `T: ~const OtherTrait` bounds, and declares that none of its methods will ever do runtime behavior outside of the behavior of the methods from the `~const OtherTrait` bounds. - because of future default methods, this requires the trait to be declared `const trait`. ### Niko's proposal in this model Niko's proposal doesn't quite fit this model, because it allows methods to opt-out of the bound on the whole trait/impl. ### Why `~const fn`? This raises a question: what's the point of writing `~const fn` in Niko's proposal? Can we somehow make `const fn` be a usefully stronger commitment than `~const fn`? (Note that this model in this doc does _not_ allow for "function that is only callable at compile-time"). ### Why `const trait`? `const trait` is the most confusing part of this. One may think it constrains impls or methods but no! It's only about _default methods_. I 100% expect users to assume that "`const trait ` => the methods are `const`". Even this shuold be fine: ```rust! const trait Trait { // The default body is `const`, but implementors may provide a non-const implementation fn method() -> u32 { 42 } } ``` ### More effect bounds `nopanic` would be awesome for unsafe code, the ability to temporarily move out of `&mut T`, and linear types. I can imagine other bounds being useful for unsafe code in general, very curious if y'all have ideas. See [Zulip thread](https://rust-lang.zulipchat.com/#narrow/channel/328082-t-lang.2Feffects/topic/Fine-grained.20effects.20for.20safety). ### Notation for conditional trait bounds The real question is: ```rust! nopanic fn foo<T: Default>() -> T; ``` Should `nopanic` extend to the trait bound? Is it ok for this function to panic if `default()` panics? Is it ok if this function can't call `default()`?? Is this like imperfect derive??? ```rust! impl<T: Default> Option<T> { // Here the API is clearly that the "Default" impl is something external, so I naturally understand `nopanic` to mean "as far as the implementation of this simple function is concerned". pub nopanic fn unwrap_or_default(self) -> T { ... } } ``` -> maybe intuition pump with `nopanic Trait<T>`, do we expect propagation to bounds on `T`? -> maybe the real question is what happens with multiple effects: ```rust pub nopanic noalloc noloop const unwrap_or_default<T: Default>(self) -> T ``` would be madness to replicate the bounds. In which cases to I actually want the unconstrained bound? If I'm not calling a method.