Skip to content
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

RFC: const bounds and methods #2237

Closed
wants to merge 7 commits into from

Conversation

Projects
None yet
@Centril
Copy link
Contributor

commented Dec 6, 2017

Rendered


Allows for:

  1. const fn in traits.

  2. over-constraining trait fns as const fn in impls. This means that
    you may write const fn foo(x: usize) -> usize {..} in the impl even tho the
    trait only required fn foo(x: usize) -> usize {..}.

  3. syntactic sugar const impl for impls which is desugared by prefixing
    all fns with const.

  4. const bounds as in T: const Trait satisfied by Ts with only
    const fns in their impl Trait for T {..}. This means that for any
    concrete MyType, you may only substitute it for T: const Trait iff
    impl MyType for Trait { .. } exists and the only fn items inside
    are const fns. Writing const impl MyType for Trait { .. }
    satisfies this.

@Centril Centril added the T-lang label Dec 6, 2017

@Centril Centril requested a review from aturon Dec 6, 2017

@clarfon
Copy link
Contributor

left a comment

I've wanted const fns in traits for the longest time and I think that this is honestly the best option I've seen, at least for the first three parts. The fourth one makes me feel a bit uneasy, but I left a large comment on the RFC source about my opinions on it.

Currently, this RFC also proposes that you be allowed to write `impl const Trait`
and `impl const TraitA + const TraitB`, both for static existential and universal
quantification (return and argument positiion). However, the RFC does not, in
its current form, mandate the addition of syntax like `Box<const Trait>`.

This comment has been minimized.

Copy link
@clarfon

clarfon Dec 6, 2017

Contributor

I think that const Trait objects should be elaborated in a bit more detail. What does it mean to have &const Trait or Box<const Trait>? Right now, AFAIK, const eval doesn't cover references, so something like:

const NUMBER: &usize = 5;
const NUMBER_2: usize = *NUMBER;

would fail. If this behaviour were to be changed (and IMO, it makes sense to do so), then something along the lines of &const Trait would be reasonably well-formed. That said, Box<const Trait> still makes no sense.

Longer term, it might make sense to simply use inference to determine if a function can be executed in constant time or not. For example, take the following (not very efficient) function:

const fn pow<T: const Mul + const One>(val: T, exp: usize) {
    if exp == 0 {
        T::one()
    } else {
        val * pow(val, exp - 1)
    }
}

To me, it makes more sense to completely forgo the const Mul + const One in favour of Mul + One. This would allow the function to be called for inputs which are not const which do not support const fn, but also let the function be called in a const context if, for example, T was u64.

Perhaps a clippy lint could warn for calling const fns like this one with a constant value where the requisite traits were not const as well. However, in general, it would be allowed by the language.

I feel like this sort of thing would make a lot more sense than simply providing a const Trait definition which opens up a whole interesting world of things.

That said, adding const impl Trait syntax would be totally fine IMHO. The only issue would be that things like impl const Default + Iterator would no longer be expressable, but I'm not sure what the benefit of that would be. At that rate, why not simply define a type directly which has all of your requirements?

This comment has been minimized.

Copy link
@clarfon

clarfon Dec 6, 2017

Contributor

Also adding further to this: &const Trait only makes sense if it's &'static const Trait, because consts all have 'static lifetimes now. Which kind of further makes things a bit confusing.

This comment has been minimized.

Copy link
@clarfon

clarfon Dec 6, 2017

Contributor

Another addendum: const impl Trait in function signatures makes sense considering how const impl Trait { ... } is allowed by the third option. Which, IMHO, is another good justification for option number three.

This comment has been minimized.

Copy link
@Centril

Centril Dec 6, 2017

Author Contributor

Thank you for a great and elaborate review!

Good point on const impl Trait and the const impl Trait { .. } syntax - I hadn't thought of it. I will surely add this to the motivation. Thanks <3.

Writing Box<const Trait>, entails that it can only be constructed by some T for which T: const Trait holds. Other than that, it is just Box<Trait>. The same applies to &(const Trait). The only expressive power you have gained with Box<const Trait> is the ability to restrict to const fns in the trait impl, nothing else. Since this is marginally useful, I have opted to not propose it yet. Perhaps if not added in this RFC, it can be added in a subsequent RFC. I have written a bit on &'static const Trait now.

Type inference is nice, but also more complex (algorithmically) and is considerably more costly wrt. compile times. The fact that Rust already has long compile times is something we should try hard not to increase too much. In addition, there is certain expressive power to the ability of restriction - do note that T: const Trait is also allowed for normal fns and impl<T: const Trait>. I think the explicitness and uniformity is a good thing.

The impl const Default + Iterator thing is fully expressible with this proposal - as an example of what you can use this for is to plug into the universal quantification here:

fn foo<T: const Default + Iterator>(bar: T) {
    // Use the const Default bound:
    const BAZ: T = T::default();

    // Use the iterator bound:
    BAZ.foreach(|elt| { dbg!(elt); });
}

EDIT: read a bit too fast - -> const impl TraitA + TraitB syntax does seem interesting - tho I'd like not to lose the ability to write -> impl TraitA + const TraitB as used in foo above. Of course you could allow both -> const impl Trait and -> impl const Trait. I don't think similarity of -> const impl Trait and const impl is enough justification to reduce expressivity.

This comment has been minimized.

Copy link
@clarfon

clarfon Dec 6, 2017

Contributor

As I mentioned, I'm having trouble understanding in what context you'd want a impl T1 + const T2; the case you mentioned is worthwhile but I'm not sure if it bears the weight of the syntax implications it holds. As stated, you can always use newtypes to get this kind of expressiveness, but in most cases when you're returning a value you either want it to work in all const contexts or none, IMHO.

This comment has been minimized.

Copy link
@Centril

Centril Dec 6, 2017

Author Contributor

I'm for sure open to const impl TraitA + Trait B as a syntax if impl const TraitA + const TraitB is too weird to swallow - but my base position is always that expressivity is more important than syntax, even if syntax may become clunky as a result of it. If there is popular demand for this change I will certainly do it.

This comment has been minimized.

Copy link
@Centril

Centril Dec 6, 2017

Author Contributor

This may be a supremely bad idea, but you could also do: -> const impl TraitA + impl TraitB.

@nikomatsakis

This comment has been minimized.

Copy link
Contributor

commented Dec 6, 2017

r? @nikomatsakis

@ExpHP

This comment has been minimized.

Copy link

commented Dec 7, 2017

Kneejerk: const impl really lacks motivation. Rust has no other shorthands like this. Also, this cited reason:

It also aids searchability by allowing the reader to know directly from the header that a trait impl is usable as a const trait bound, as opposed to checking every fn for a const modifier.

seems unusual to me. Doesn't it hurt searchability by creating more locations that the reader needs to search? (quick! What are the trait bounds on HashMap::len?)

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

@ExpHP By this I mean that you don't have to look at every fn in an impl on a rustdoc page to check that all of them are const fns to tell if it satisfies a T: const Trait bound, you can just check the header for const impl and you know this instantly.

EDIT: Not mentioned in the motivation directly but well in the guide-level-explanation and drawbacks is the usefulness of const impl in migrating existing code to this new model wherever applicable in a more ergonomic fashion. I could also mention this in the motivation if you wish.

@ExpHP

This comment has been minimized.

Copy link

commented Dec 7, 2017

Sorry, you are right, I misread it as referring to searchability of a fn's constness.


The const Trait bounds are a really neat idea to solving the bifurcation problem. The only trouble that comes to mind at the moment is how const fns would probably not be generated by e.g. #[derive(Copy, Clone)], requiring manual impls.

@eddyb

This comment has been minimized.

Copy link
Member

commented Dec 7, 2017

I should mention that IMO a solution that can't be integrated with auto-deriving is a non-starter.

EDIT: to be more clear, I mean existing (builtin) #[derive] clauses, with no user intervention.

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

@ExpHP Yeah, that is a problem I discussed with @eddyb. My best solution was the syntax: #[derive(const Copy, const Clone)].

@rpjohnst

This comment has been minimized.

Copy link

commented Dec 7, 2017

It might be better just to derive the most-const-possible implementation. I'm not even sure you could derive the same traits twice like that.

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

@eddyb, @ExpHP: Another possibility that just crossed my mind (and thanks for bringing the omission in the RFC to my attention, btw) is:

#[derive_const(Copy, Clone)] // No bc-break because we have reserved all derive_ prefixed attributes.
struct Foo;

Which do you prefer?

@rpjohnst Oh yeah - this is the question "Does T: const Trait specialize T: Trait ?" which I ask in the unanswered questions. cc @aturon on this.

@ExpHP

This comment has been minimized.

Copy link

commented Dec 7, 2017

It might be better just to derive the most-const-possible implementation.

I doubt this is possible, considering that derive impls can't even introspect on the appropriate trait bounds!

I'm not even sure you could derive the same traits twice like that.

...ah drat. Yeah, I think you're right. (what am I even saying, I'm just guessing based on how proc macros work. I have no idea how the builtin derives work. :P)

This brings to light a drawback that this RFC has compared to e.g. ConstDefault, which is that you cannot have different trait bounds on T for impling const Trait and Trait. Or is this the same as the specialization issue you just mentioned?

What I mean is: It should be possible to have a type Foo<T>(T) such that Foo<u8>: const Default and Foo<SomeNonConstType>: Default

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

@ExpHP Yep, I think it's the same question. Tho I have to ask why you'd want two impls of Clone where one is const and one is not. We should also not "let the perfect be the enemy of the good".

Btw - which syntax do you prefer of the two alternatives?

@ExpHP

This comment has been minimized.

Copy link

commented Dec 7, 2017

Btw - which syntax do you prefer of the two alternatives?

I think I'd rather think more about the implications before shedding on the syntax, but between the two, I'm not sure if #[derive(const Copy)] is even supported syntax for attributes.

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

With respect to overlap, the major problem arises for things like:

T: const Default, U: const Default => (T, U): const Default
T: Default,       U: Default       => (T, U): Default

With respect to derive the value of being explicit about constness in derive is that custom derive can get the information.

@rpjohnst

This comment has been minimized.

Copy link

commented Dec 7, 2017

It might be better just to derive the most-const-possible implementation.

I doubt this is possible, considering that derive impls can't even introspect on the appropriate trait bounds!

Hmm, good point. A more general solution might be to allow functions to be const if their implementation is const. I'm not sure how to do that without introducing monomorphization errors or global inference, though.

@comex

This comment has been minimized.

Copy link

commented Dec 7, 2017

Even though this RFC is a sensible extension of the existing fn/const fn distinction, I really dislike that distinction, so I dislike where this ends up. I've seen this story before: after all, Rust const fn is almost the same as C++ constexpr functions.

Today, rustc tells me that "blocks in constant functions are limited to items and tail expressions", which is basically the same limitation as existed in C++ when constexpr functions were first specified in C++11. Pretty soon this limitation was decided to be unnecessary, given that it required algorithms to be rewritten using recursion instead of iteration - which is both unnatural in C++ and inefficient - when it would add little compiler complexity to just support the full language. So C++14 removed the restrictions. If I'm not mistaken, Rust is on track to do the same thing once miri-based constant evaluation is finished.

(See also: "Relaxing constraints on constexpr functions", proposal and decision/wording.)

But that already gets you to a point where a large fraction of functions and methods can be marked const/constexpr, and in a high-quality library, everything that can be so marked should be, to maximize usefulness. So, for example, if you look at a C++ STL implementation, you'll see constexpr littered everywhere. This is unnecessarily verbose, and since I value Rust's general succinctness, I don't want to see the same happen here.

The biggest limitation in C++ is that for now there's no heap allocation in constexprs. So no new or delete, and no containers that use the heap for their contents. I don't know if there are any proposals to change that – but in Rust, miri itself already supports heap allocation, and I vaguely remember hearing someone talking about potentially allowing 'heap allocations' in const fns. (Also, this RFC hints at it – perhaps unintentionally – by mentioning the possibility of Box<const Foo>, which wouldn't be useful if Box couldn't be created in const fn context.) It should be possible at least in cases where the heap-allocated values are destroyed before runtime, and potentially in other cases as well (the initial 'heap' data could be written out as static data, but the problem would be ensuring that the allocator didn't get confused by attempts to free it). And, IMO, that would be a good thing! If nothing else, supporting recursive enums in const fn will require some form of heap or pseudo-heap allocation. And not supporting them would feel like an arbitrary limitation that people would work around with giant static arrays or other unsightly hacks.

But… if heap allocation becomes supported, const-ineligible fns will be the exception rather than the rule. One disqualifier would be FFI. Another would be pointer-to-int conversions, perhaps used for hashing, but I don't think Hash impls usually rely on that(?). Another might be randomization, like for HashMap seeds, but that's a solvable problem (you can start with a fixed seed and randomize on growth).

Overall, I feel like const eligibility could be treated like an auto trait, like Send or Sync: enabled by default, and ineligibility implicitly propagates to dependencies (in this case, caller fns, as opposed to structs containing ineligible fields), because it's common enough that the usual policy of requiring explicit opt-ins/bounds wouldn't be worth the cost in verbosity.

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

@comex I'm not sure if you're talking about a literal trait, or just something trait-like. But given that satisfying a trait (let's call it Const) is a property of a type, and not a value and that a value of a certain type may or may not be const - how would type checking and inference work? Also: how do you ensure that it is not very expensive compared to a simple impl lookup + checking a flag on the impl (what this RFC proposes)? I really need a lot more details (like example code and at least vaguely defined semantics) to either write a new RFC or modify the current in the direction you want or to even understand fully what you are really saying.

Also - is your only point of contention 4. (bounds) or everything?

@burdges

This comment has been minimized.

Copy link

commented Dec 7, 2017

Is Default the only use case currently being proposed?

It'd be lovely if Index, IndexMut, and SliceIndex worked for fixed length arrays, but your const Trait does not appear to help. The array_ref! crate handles this now of course, but it'd be more elegant if the language itself did.

We could almost imagine a curried form of SliceIndex being a const trait if an associated type constructor could optionally be based on const self :

pub trait SliceIndex<T: ?Sized> {
    type Output<const self>: ?Sized;  // Crazy pants ATC
    const fn get(self) -> impl FnOnce(slice: &T) -> Option<&Self::Output<self>>;
    const fn get_mut(self) -> impl FnOnce(slice: &mut T) -> Option<&mut Self::Output<self>>;
    const unsafe fn get_unchecked(self) -> impl FnOnce(slice: &T) -> &Self::Output<self>;
    const unsafe fn get_unchecked_mut(self) -> impl FnOnce(slice: &mut T) -> &mut Self::Output<self>;
    const fn index(self) -> impl FnOnce(slice: &T) -> &Self::Output<self>;
    const fn index_mut(self) -> impl FnOnce(slice: &mut T) -> &mut Self::Output<self>;
}

It's not quite right though because if self is not const then Output still exists but must itself still be constant over the non-const self. I donno about expressing that but afaik nobody plans to stabilize SliceIndex anyways.

There is no obvious similar way to adapt Index and IndexMut so we'd need to add explicitly cosnt variants anyways.

pub trait ConstIndex<Idx> {
    type Output<const Idx>: ?Sized;  // Vanilla const ATC
    fn const_index<const I: Idx>(&self) -> &Self::Output<I>;
}
pub trait ConstIndexMut<Idx> : ConstIndex<Idx> {
    fn const_index_mut<const I: Idx>(&mut self) -> &mut Self::Output<I>;
}

We cannot afaik add this associated type constructor and method to Index and IndexMut without breaking various collections.

tl;dr We might be fairly close to correcting the type of expressions like slice[0..16] using ATCs with const type parameters, but this RFC does not appear to help.

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

@ExpHP Thinking about it some more I can't see a reason why this would not be possible:

struct Foo<A>(A)

impl<A: Default> Default for Foo<A> {
    default fn default() -> Self { Foo(A::default()) }
}

const impl<A: const Default> Default for Foo<A> {
    fn default() -> Self { Foo(A::default()) }
}

They do not overlap and preserve coherence because if the substituted-for type MyType is const Default the latter impl is chosen, while the former is when only Default is satisfied. In other words, when fully applied Foo<MyType> has a set of impls that is either empty or singleton.
Thus specialization should be possible.

However, you may of course not have:

struct Bar;

impl Default for Foo {
    default fn default() -> Self { Bar }
}

const impl Default for Foo {
    default fn default() -> Self { Bar }
}

as there is nothing to specialize. However, notably having only the latter impl is sufficient to use it in contexts where only T: Default is required.

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

@ExpHP

Kneejerk: const impl really lacks motivation. Rust has no other shorthands like this.

Come to think of it now and speaking of specialization there's the default impl syntax, which is a short-hand when you specify all trait items in the default impl.

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 7, 2017

@burdges Doesn't this work?

const impl<T, const N: usize> Index<usize> for [T; N] {
    type Output = T;
    fn index(&self, index: usize) -> &Self::Output {
        // ..
    }
}

This impl satisfies the bound X: const Index<usize>.

I'm not sure about the other traits as mut was involved at places and I'm not sure about the interaction with const.

@eternaleye

This comment has been minimized.

Copy link

commented Dec 30, 2017

@Centril: The distinction you are drawing is merely whether an effect can (reader, state, unsafe) or cannot (non-const, partial) be masked, in the sense of the original POPL88 paper. Reader and State are masked by the appropriate run; unsafe fn by unsafe { }, etc.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

commented Dec 30, 2017

@alexreg Yes, it's just some fancy jargon for similar ideas. Kind of like how "gravity" is fancy jargon for "things fall downwards, as anybody can see". It's worth knowing the fancy jargon to be able to get a sense of the prior art, learn from it, avoid reinventing the wheel (or at least doing it badly), and so on.

To be clear: I wasn't proposing a different design for how things should work. I was suggesting a way of thinking and communicating about it (again, among ourselves, not in the public-facing language documentation) that could be helpful in finding a good design.

@eternaleye Hmm. By the different polarities do you mean to imply that it's actually unsafe that's "the effect" in one case, and "not unsafe" in the other? I hadn't really conceptualized these uses of unsafe as separate things so far. (Of course in both cases you have the "introduction form" (unsafe fn, unsafe trait) versus the "elimination form" (unsafe { }, unsafe impl); I don't think that's what either of us are talking about.)

@Centril I'm not sure if I've understood your question/concern correctly - but if we're talking about things like "the impl should be const when the impls it depends on are const", then we're already talking about polymorphism in something, just without using the word. What are you thinking of w.r.t. needing additional constraints/bounds?

And yeah, an effect type system doesn't really care about whether the effects that it tracks are built-in to the language versus expressed in library code. It works for both.

(I haven't read the 1ML effects paper either.)

@eternaleye

This comment has been minimized.

Copy link

commented Dec 30, 2017

@glaebhoerl: Yes; I'm using "negative-polarity" to mean that the marker denies an effect (like const fn) rather than denoting it (like unsafe fn)

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 30, 2017

@glaebhoerl

PS: I think I need to chill a bit and read some papers... =)

OK; I think I can communicate this better-ish via code-ish...

effect<C> C  // I understand this as: forall effects C. C-implement
impl<T: C Default> Default for MyStruct<T> {
    fn default() -> Self {
        MyStruct(T::default()) // <-- What effects does the body assume?
    }
}
effect<C> C
impl<T: C Default> Default for MyStruct<T> {
    fn default() -> Self {
        do_some_IO(); // <-- What effects does the body assume now?
        MyStruct(T::default())
    }
}

I'm going to go nuts with the strawmanning and pulling ideas out of thin air now... These ideas may all be stupid, but here goes...

// A hierarchy of effects as provided by the compiler.
// Probably a lot more to it but for now let's keep this as a
// "simple sub-effecting scheme" (think subtyping).
// Question: How does this compose with custom effects?

effect total { ... }
effect no_panic : total { ... }
effect const : no_panic { ... }
effect safe : const { ... } // Safe is default
effect unsafe : safe { ... }
effect<C> C // The safe effect is allowed, but not unsafe.
impl<T: C Default> Default for MyStruct<T> {
    fn default() -> Self {
        do_some_IO(); // Legal.
        MyStruct(T::default())
    }
}
effect<C: safe> C // Same as above.
impl<T: C Default> Default for MyStruct<T> {
    fn default() -> Self {
        do_some_IO(); // Legal.
        MyStruct(T::default())
    }
}
effect<C: const> C // safe effect is illegal, only const and more restrictive is allowed
impl<T: C Default> Default for MyStruct<T> {
    fn default() -> Self {
        do_some_IO(); // TYPE ERROR.
        MyStruct(T::default())
    }
}
effect<C: no_panic> C // Only no_panic and more restrictive allowed
impl<T: C Default> Default for MyStruct<T> {
    fn default() -> Self {
        panic!() // TYPE ERROR.
        MyStruct(T::default())
    }
}
effect<C: total> C // Only total is allowed, there's nothing more restrictive.
impl<T: C Default> Default for MyStruct<T> {
    fn default() -> Self {
        loop {} // TYPE ERROR.
        MyStruct(T::default())
    }
}
@eternaleye

This comment has been minimized.

Copy link

commented Dec 30, 2017

@Centril: Your use of ? is confusing me - ?Sized means "possibly not sized", but you use ?total to mean "certainly total".

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 30, 2017

@eternaleye

@Centril: Your use of ? is confusing me - ?Sized means "possibly not sized", but you use ?total to mean "certainly total".

Right right - the intended effect was "if you don't specify anything, safe is assumed. I'll edit away the ?s.

@eternaleye

This comment has been minimized.

Copy link

commented Dec 30, 2017

There are a few other issues I have with how you've framed it:

  • You have a relatively unprincipled mix of "effects" (e.g. "unsafe", "partial", "panic") and "restrictions" ("safe", "total", "no_panic"). These are not miscible; bounding on an effect is covariant to bounding on its corresponding restriction. Moreover, you seem to prefer the "restriction" formulation, which is deeply problematic (one can't add restrictions, which is needed for my second example).
  • Your examples are all wrapped in trait impls, which I feel is superfluous (all of the relevant behaviors can be illustrated with functions). This confused me when reading your last example - I was looking for C fn and only after thought to check for C impl.

Currently, Rust has three effects:

  • unsafe, which allows calling unsafe fns and dereferencing raw pointers
  • impure, the rough inverse of the const restriction
  • untrusted, which indicates that commonsense invariants (like Ord being a total order) may be unsatisfied (the inverse of the unsafe trait/impl restriction).

Today, impure and untrusted are default-permitted, and unsafe is default-forbidden. In addition, unsafe and impure can only be controlled on functions, while untrusted can only be controlled on traits.

This RFC proposes:

  • Making it possible to control impure on traits and impls
  • Making it possible to have effect bounds (only for ?impure at first)

The discussion has also covered the following effects:

  • panic
  • partial

It may be worth extending untrusted control onto functions, as well - some free functions' behavior may uphold invariants that unsafe code relies on may rely on.

Here's a baseline example of the structure, without any extraneous detail. foo is a fn with exactly the C effects, as it is (itself) making use of nothing beyond the defaults (which get bundled into C).

effect<C> // forall (C: effect) (impure C) (untrusted C)
C fn foo<T>() -> (T,) // foo is a fn with effect C
    where T: C Default { // whenever T has a C impl of Default
    File::open("/etc/hosts");
    (T::default(),)
}

Here, we give up a default effect, namely impure:

effect<C: ?impure> // forall (C: effect) (untrusted C)
C fn foo<T>() -> (T,) // foo is a fn with effect C
    where T: C Default { // whenever T has a C impl of Default
    File::open("/etc/hosts"); // Compile error
    (T::default(),)
}

Here, we request an additional effect, one not present in what we depend on:

effect<C> // forall (C: effect) (impure C) (untrusted C)
C+unsafe fn foo<T>() -> (T,) // foo is a fn with effects C and unsafe
    where T: C Default { // whenever T has a C impl of Default
    return (T::default(),);
    std::instrinsics::unreachable()
}
@eddyb

This comment has been minimized.

Copy link
Member

commented Dec 30, 2017

@glaebhoerl Such a "transparent"/"effect-inline" function would still be unrestricted at the definition side, but in generic contexts the viral nature would only propagate based on use, effectively gaining the C++ deferred checking of templates, but for effects instead of types.

E.g. if you have an auto-derived Clone::clone on a struct Newtype<T>(T); that's marked in this way, and Newtype(true).clone() appears in a constant context, then only bool::clone needs to also be marked (and presumably be CTFE-friendly itself), no other Clone impls are involved.

So you would never have any preconditions for marking functions/impls/modules as such, it's purely a choice of "freezing" some/all fn bodies, semver-wise, and exposing them to "effect inference".

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Dec 30, 2017

Here's an exerpt, slightly edited to remove irrelevant stuff, from #rust-lang @ irc.mozilla.org. The full conversation is found here.

   centril || eternaleye: I used the constraint formulation cause that's the one
           || I'm most comfortable with expressing ;)
eternaleye || centril: Yeah, but it doesn't compose :P
   centril || eternaleye: Yeah, I realize it was not the best formulation, it's
           || only for me to get my ideas across
eternaleye || centril: Anyway, I had to edit my post a good bit to fix drafting
           || mistakes, so maybe refresh
eternaleye || But does my overall framing of it make sense to you?
   centril || eternaleye: safe => impure - isn't that just a renaming?
eternaleye || Impure doesn't correspond to safe
eternaleye || It corresponds to const
eternaleye || Anything that is not const is impure
   centril || eternaleye: right, but essentially that was what safe in my "effects" meant
eternaleye || centril: Anyway, what you tried to do is a hierarchy, but currently Rust has three
           || _orthogonal_ effects
eternaleye || You only get a hierarchy at all if you introduce partial/panic
eternaleye || Because partial and panic have a subtyping relation (panic is a restricted form of
           || partial, which can only diverge by a single mechanism)
   centril || eternaleye: that feels like inverting the names and order?
   centril || what is the base effect?
eternaleye || Currently, Rust has `untrusted` and `impure` defaulted. If we added `partial`,
           || it would also be defaulted (pulling in `panic` as a result)
eternaleye || centril: There is no "base effect" - the base is the absence of effects
   centril || eternaleye: ok, but what does the base entail?
   centril || eternaleye: "bounding on an effect is covariant to bounding on its corresponding
           || constraint" <-- unpack this?
eternaleye || If you had C: ?impure + ?partial + ?panic + ?untrusted, then C would only permit
           || referentially-transparent (?impure) bounded (?partial) non-panicking (?panic)
           || functions that uphold their documentation invariants as a matter of ABI (?untrusted)
   centril || eternaleye: does not ?partial => (?impure + ?panic) ?
   centril || (?partial sounds to me like total)
eternaleye || Not necessarily
eternaleye || A function can be both total and impure
eternaleye || Consider accessing a static, but in a strictly decreasing way.
   centril || but at the very least it can't panic...
eternaleye || You're getting it backwards. Partial implies panic, but the question marks don't
           || mean what you think they do.
   centril || Yes, I'm having a hard time unpacking what you're saying
   centril || ill read your comment in full and get back, sec
eternaleye || centril: C: partial means that C allows _all_ forms of partiality - C is "partial or
           || worse" C: ?partial isn't a bound, it's a syntax for taking off an invisible default
           || bound
   centril || ah
eternaleye || C: !partial is what you were interpreting C: ?partial as
   centril || yes
eternaleye || T: ?Sized doesn't accept only unsized types, after all :P
   centril || eternaleye: right, that interpretation makes sense
eternaleye || The `forall` annotations on my examples are meant to illustrate that
   centril || eternaleye: can you talk a bit more about untrusted ?
eternaleye || Sure!
eternaleye || That's for things like `unsafe trait Send`
   centril || eternaleye: right, and that is different from unsafe fn and unsafe {..}
eternaleye || `unsafe` used that way is completely unrelated to `unsafe fn` in terms of the effect
           || it denotes
eternaleye || To the point that it's a restriction, rather than an effect
   centril || eternaleye: right, it's unfortunate that it is annotated that way?
eternaleye || Its absence means "implementations may break promises the trait docs make"
   centril || eternaleye: yeah, unsafe trait doesn't make sense as an effect - it doesn't do
           || anything at all really other than requiring a syntactic "yes, I know about the API
           || contract"?
   centril || eternaleye: right, but it has no executable content?
eternaleye || It has executable content, it's just that the compiler can't verify that the
           || executable content restrictions were obeyed
   centril || hmm, ok.. =)
eternaleye || It's still in effect though - a sufficiently careful language would make ptr::read
           || require that its argument was derived without an effect.
   centril || eternaleye: so  !partial => !panic  ?
eternaleye || Yes
   centril || eternaleye: but assuming it was in the surface lang you'd write C: ?partial  instead?
eternaleye || Panic permits a subset of the behaviors of partial. Permitting all of partial's
           || behaviors permits panic's behaviors, and banning all of partial's behaviors bans
           || panic's behaviors
   centril || eternaleye: right that makes sense
eternaleye || ? is no guarantee - neither a guarantee of presence nor a guarantee of absence
eternaleye || Sort of like T: Default, then invoking it with something that also supports Clone
eternaleye || Your code can't use clone, because your bound didn't include it
   centril || eternaleye: so in the hypothetical surface lang you'd write  C: !partial  then?                
eternaleye || But the code you call, behind the abstraction boundary, might
   centril || eternaleye: given that we had  panic and partial , what would be default-permitted? 
           || panic + partial + impure + untrusted ?
eternaleye || Yes
   centril || and so ?partial gets rid of the default bounds  panic + partial ?
eternaleye || Anything else would be a compatibility break
eternaleye || No, it gets rid of the default bound on partial
   centril || eternaleye: ah
eternaleye || centril: `effect<C: !partial> C fn foo(f: &C Fn())` means that foo's body and f _must
           || not_ be partial; the same with `?partial` means that foo's body must not be partial,
           || but f may be.
eternaleye || Because `foo` is _generic over_ partiality now (?partial)
eternaleye || But f knows whether it's partial
   centril || eternaleye: aaah :P
eternaleye || Whereas C: partial is fixing partiality to be supported, and !partial is fixing it to
           || be _unsupported - neither is generic over partiality
eternaleye || Just like ?Sized makes you generic over sized/unsized
   centril || eternaleye: can you also talk a bit about custom effects? "handlers" and whatnot
eternaleye || centril: Effects with handlers are really just a sugar on monads - you denote the
           || monad's constructors, so that the language can make them _look_ like they just return
           || the wrapped value
   centril || eternaleye: the monads type constructor or the functions pure +
           || [bind/join/kleisli-composition] ?
eternaleye || centril: The type-specific constructor functiond
   centril || eternaleye: interesting! =)
eternaleye || Some/None/Ok/Err/etc
eternaleye || Bind, join, etc. vanish
   centril || eternaleye: Could you please edit/add a post with the things you've explained to me
           || thus far? It has been super helpful!
   centril || Thanks <3
eternaleye || Because the language knows exactly where to insert them for you
   centril || eternaleye: Also; any comments you have on what the surface language should look like
           || (including using the effects syntax if you like that) would be neat
eternaleye || The panic effect, in fact, is basically Result - `panic!()` is sugared `Err`, while
           || `return` is sugared `Ok`
eternaleye || Whereas with the partial effect, the Err side of Result carries a closurized recursive
           || call.
   centril || eternaleye: I think the effects polymorphism in surface lang is very interesting to
           || have and certainly open to it
eternaleye || Agreed
   centril || eternaleye: if you don't have the time to update the comment / add another one, can I
           || copy/paste the conversation into the RFC?
eternaleye || Though I don't think it's a tack that Rust will take
eternaleye || (much like mutability polymorphism)
eternaleye || Feel free
   centril || eternaleye: hmm well, perhaps we can take a path that leaves us open to effect
           || polymorphism at a later stage syntactically ?
eternaleye || centril: I think that effect polymorphism is a bit too much for the strangeness
           || budget, considering the target audience of rust
eternaleye || I am 100% willing to acknowledge that I'm on the academic end of the target audience.
   centril || eternaleye: is it tho..? Rust seems to have success bringing in pythonistas to
           || Haskllers and some C++ers - it seems to be a mixed bag
   centril || eternaleye: yeah, so am I
eternaleye || While effect polymorphism would definitely be a stumbling block for people who are
           || immigrating from C++
   centril || eternaleye: So perhaps we don't need to add polymorphism now, but make us compatible
           || with such syntax in the future?
   centril || eternaleye: Btw... considering how easy it was to transition to effects-polymorphism
           || syntactically in the RFC-thread, I think we're not that far off
   centril || eternaleye: we special case the keyword `const` as sugar for `!impure`, remove the
           || forall quantifier `effect <C..>` and we're home
eternaleye || centril: Sure, but then we can't fix #[derive(Clone)]
eternaleye || It needs ?impure
   centril || eternaleye: (the universal quantifier makes me naturally ask: are there existentially
           || quantified effects?)
eternaleye || ST, maybe?
   centril || it may be a question devoid of meaning, but..
   centril || eternaleye: ST = ?
eternaleye || The ST monad
   centril || oh that one
   centril || eternaleye: so if we introduce  `const == !impure` but also allow ?impure  ?
   centril || is that blowing the strangeness budget?
   centril || but no universal quantification of effects
eternaleye || centril: My point is that #[derive(Clone)] basically needs const polymorphism
eternaleye || But it may not need the full monty
   centril || eternaleye: by const polymorphism you mean?
   centril || (in our  effect<C: ..>  syntax.. if you may)
eternaleye || (Strawman) maybe `const? fn foo<T: const? Default>() -> T` would work
eternaleye || And basically steal from lifetime inference
eternaleye || Avoids supporting multiple effect bounds
eternaleye || Or exposing effects as values
     eddyb || eternaleye: ooooh
     eddyb || eternaleye: that could maybe work for conditional traits in impl Trait too
eternaleye || eddyb: it does read pretty naturally...
     eddyb || cc aturon nmatsakis woboats cramertj
     eddyb || -> impl Iterator + ?Clone + ?DoubleEndedIterator
   centril || eternaleye: so questions: 1. why the moving of ? to the end (to avoid parsing
           || ambiguities?) 2. const? means the same as ?impure  ?
     eddyb || or something of that sort
eternaleye || centril: 1.) Yes, plus legibility 2.) Yes
   centril || eternaleye: cool, dumping conversation to RFC tread then =)
@alexreg

This comment has been minimized.

Copy link

commented Dec 31, 2017

@eternaleye Thanks for the links. It seems to be a formalisation of what has already been discussed above here, with nothing but very subtle differences in semantics. (I'll need time to digest it though.) Would that be wrong to say?

@alexreg

This comment has been minimized.

Copy link

commented Dec 31, 2017

@alexreg Yes, it's just some fancy jargon for similar ideas. Kind of like how "gravity" is fancy jargon for "things fall downwards, as anybody can see". It's worth knowing the fancy jargon to be able to get a sense of the prior art, learn from it, avoid reinventing the wheel (or at least doing it badly), and so on.

To be clear: I wasn't proposing a different design for how things should work. I was suggesting a way of thinking and communicating about it (again, among ourselves, not in the public-facing language documentation) that could be helpful in finding a good design.

No problem, that's fair enough. Thanks for introducing the formalism and past literature on the subject. I'll try to give it a read myself soon.

@Lokathor

This comment has been minimized.

Copy link

commented Jan 14, 2018

@Centril asked that I post here about my use case for unsafe and Deref interaction. It's a rather obvious one when you think about it, but here goes:

So I've got all these *mut T values in my program. Nevermind why, I just do. No matter how much you try to make Rust safe the lowest level code will always have raw pointers in it somewhere. Well, they're not ergonomic to use at all. You have to manually deref them every time you want to access a field, and deref is lower priority than field access so you have to wrap it in parens, and if you need to access a field off of a pointer off of another pointer it gets worse:

(*(*Foo).Field).OtherField // just terrible

Now, as we all well know, de-referencing a raw pointer of dubious origin is unsafe (might point out of bounds, or it might point to invalid bits for that type even if it's in bounds), but to have rustc let us use auto-dereferencing we have to have a Deref impl on that type, which is an impl that specifies safe methods. It will compile to just outright lie to rustc and say unsafe { &*self } or something, but obviously that's very, very bad. So what's needed is:

  • either a special deref and access operator like -> just for raw pointers (probably not good)
  • or a way to specify that you're implementing Deref on a type but doing it unsafely and that utilizing that trait impl must be done only within unsafe blocks (good, and from what i gather in this thread it might be possible some day if this RFC goes through?).
@alexreg

This comment has been minimized.

Copy link

commented Jan 14, 2018

@Lokathor How does this have anything to do with const traits?

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Jan 14, 2018

@alexreg Not much ;) Just effects and unsafe as we discussed before.

@scottmcm

This comment has been minimized.

Copy link
Member

commented Feb 4, 2018

As I continue to look at random issues, I noticed this one: #1926

It's phrased as macros right now, but as it's just parsing, it feels like something that could definitely be handled with a const fn, if you could impl const FromStr.

One thing, though, that I couldn't figure out how I'd do, so it might be good to address in the RFC: how can str::parse be const iff F: const FromStr?

@Centril

This comment has been minimized.

Copy link
Contributor Author

commented Apr 12, 2018

I'm going to go ahead and close this RFC for now.
This RFC needs to be redesigned to address all the comments made and in particular be more fleshed out wrt. "I am const if the stuff I depend on are const".

To all those interested in this design space I recommend joining me at https://github.com/Centril/rfc-effects to help flesh out the design for the v2.0 of this RFC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.