Trait-based overload, total ordering and transparent trait bounds


#1

Hi,

I stumbled upon C++ Concepts Lite presentation, and saw some interesting things which are missing from Rust, but can be quite useful. I mean, ‘weak’ type bounds and bounds-based overload.

Here are main theses:

  1. You can do blanket impl for some trait with overlapping bounds if those bounds form totally ordered subtyping hierarchy

    trait Actor {
    fn action(&self);
    }
    impl Actor for T {
    fn action(&self) { /* some default action / }
    }
    impl<T: Iterator> Actor for T {
    fn action(&self) { /
    some action for iterator / }
    }
    impl<T: ExactSizeIterator> Actor for T {
    fn action(&self) { /
    action for exact size iterator */ }
    }

    fn do_stuff<T: Actor>(actor: &T) {
    actor.action();
    }

The main idea is to call some implementation based on what type T actually supports.
2. Constraints on type are ‘weak’, i.e. they allow overloaded function from (1) to check for type properties not available to outer function:

fn do_outer_stuff<T: OtherActor>(actor: &T) {
    ...
    do_stuff(actor);
    ...
}

Here, do_outer_stuff can only do what OtherActor provides; but do_stuff can be more specific.
3. p.1 can have some nice sugar:

fn do_stuff<T>(actor: &T) { ... }
fn do_stuff<T: Iterator>(actor: &T) { ... }
fn do_stuff<T: ExactSizeIterator>(actor: &T) { ... }

some constraints can apply here, like overload only by a single argument, and constraints should form total ordering

To sum up, this looks to play nicely with the latest post from Aaron Turon: https://aturon.github.io/blog/2015/09/28/impl-trait/, in particular the first proposed approach with type elision. This is effectively the same - a constrained type T remains the same, we’re only restricted to the operations we specified in constraints. But some other function can apply its own resolution.

I welcome to discuss this idea. Would be nice to see core team member’s opinion.

Thanks


#2

Point (1) is exactly (or, very close to) impl specialisation.

You’ll have to be more specific about what you mean for point (2), I’m not sure I understand from the example.

Incidentally, the impl Trait proposals are not handling this sort of type-system extension; they are really just sugar for explicitly writing out the types (possibly with wrapper types and manually structified closures).


#3

On (1): very nice, I didn’t know such RFC exists
On (2): I mean that in case of T: AnyTrait constraint, T doesn’t become only AnyTrait; it just means you can use it only as AnyTrait in the scope where constraint applies. Let’s say I have some fancy function over iterators:

fn iterate_fancy<T: IntoIterator>(iter: T) {
    for item in iter {
        /* do some computation */
    }
}

Then, I see at some point that my function does wrong. Ok, I wanna do debug output for debuggable items of iterator, and do nothing if item type isn’t Debug. With (2) and (3), I can do:

fn debug_opt<T>(_: &T) { println!("some unknown item") }
fn debug_opt<T: Debug>(item: &T) { println!("Iteration item: {:?}", item) }

fn iterate_fancy<T: IntoIterator>(iter: T) {
    for item in iter {
        /* do some computation */
        debug_opt(&item);
    }
}

The main idea is that we cannot use Item as Debug, or PartialEq, or anything else inside iterate_fancy because we didn’t specify such constraint, i.e. compiler cannot guarantee that our item will have needed trait impl. But if some other function (or trait) provides two alternatives, one of which complies with current constraint, and the other is more specific, then this function can be applied at monomorphization - because if our T is Debug, we have overload for it, but even if it’s not, we also have default overload case. And so there’s no error, only selection between alternatives.

UPD: Oh, and about impl traits. I’m just saying that approach is similar - impl trait shows the trait our return type adheres to. But it doesn’t prohibit further constraint application - i.e. impl Trait doesn’t lose Clone, or Drop, or Debug, or Send - it’s just hidden from current scope. And if we have impl Iterator, we can pass it to debug_opt. If our type behind impl is Debug it will apply T: Debug alternative, otherwise a less restrictive will be applied.

Hope I’ve explained my thoughts clear enough :smile: