Skip to content

Instantly share code, notes, and snippets.

@Popog
Created June 25, 2016 22:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Popog/1484141070c2a17aaa204f2ec5725d5b to your computer and use it in GitHub Desktop.
Save Popog/1484141070c2a17aaa204f2ec5725d5b to your computer and use it in GitHub Desktop.
  • Feature Name: impl_else
  • Start Date: 5/16/2016
  • RFC PR: (leave this empty)
  • Rust Issue: (leave this empty)

Summary

Allow multiple conflicting impls via the else keyword.

impl<T: ?Sized> AttemptDeref for T
where T: Deref {
    type Output = <T as Deref>::Target;
    fn attempt_deref(&self) -> Self::Output {
        *self
    }
} else<T: ?Sized> AttemptDeref for T {
    type Output = T;
    fn attempt_deref(&self) -> Self::Output {
        self
    }
}

Motivation

As it stands, it is not possible to create conflicting impls. While RFC 1210 makes targetting more efficient implementations of a trait easy, it has a few shortcomings.

The first is that default associated items are always subject to being overridden by future impls. This means that they cannot be resolved to a specific type/constant when compiling a crate.

The second issue is that specializations require a direct hierarchy of specificity. default functions and associated items must be for less specific impl than the "specialized" impls that override them.

pub trait Trait1 {}
pub trait Trait2 {}

pub trait Trait3 {
    fn test(self);
}

impl<T: Trait1> Trait3 for T {
  default fn test(self) { println!("Trait1") }
}

impl<T: Trait2> Trait3 for T {
  fn test(self) { println!("Trait2") }
}

As there is no relationship between Trait1 and Trait2, a type which implements both would have no clear priority which Trait3 impl to choose.

Adding else clauses would create an ordered priority between conflicting impls. Associated items would also be well defined and not subject to being overridden.

impl<T: Trait1> Trait3 for T {
  fn test(self) { println!("Trait1") }
} else <T: Trait2> Trait3 for T {
  fn test(self) { println!("Trait2") }
}

A final benefit is that type_traits similar to those found in C++'s can be easily created.

trait RemoveReference {
    type Type: ?Sized;
}
impl<'a, T: ?Sized> RemoveReference for &'a mut T {
    type Output = T;
} else<'a, T: ?Sized> for &'a T {
    type Output = T;
} else<T: ?Sized> RemoveReference for T {
    type Output = T;
}

This approach is more limited than C++'s SFINAE, but it is still quite powerful

Detailed design

A single impl else block would essentially act as multiple impls but with the exception that conflicting implementations between the different impls would be resolved to the first impl in the block that matched.

Within an impl else block, all impls must be involved in internal conflicts, but cannot be involved in conflicts with other impl or impl else blocks. When represented as a graph with impls as nodes and conflicts as edges, the graph should be connected; no node should be unreachable. This is to discourage placing unrelated impls into impl else blocks.

This approach should integrate cleanly with the specialization of RFC 1210. default methods and associated items in an impl else block should function just as if they're in an independent impl.

Drawbacks

None at the moment.

Alternatives

More explicit syntax impl... else impl.... Not sure this is necessary as unlike if statements, there is no concept of a trailing else.

Some kind of where T: !Trait proposal.

Unresolved questions

Will this also work for non-trait impl blocks?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment