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: delegation of implementation #1406

Closed
wants to merge 7 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions text/0000-delegation-of-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
- Feature Name: delegation_of_implementation
- Start Date: 2015-12-12
- RFC PR:
- Rust Issue:

# Summary
[summary]: #summary

Provide a syntactic sugar to automatically implement a given trait `Tr` using a pre-existing type implementing `Tr`. The purpose is to improve code reuse in rust without damaging the orthogonality of already existing concepts or adding new ones.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/rust/Rust/ (proper noun)


# Motivation
[motivation]: #motivation

Let's consider some existing pieces of code:
```rust
// from rust/src/test/run-pass/dropck_legal_cycles.rs
impl<'a> Hash for H<'a> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state)
}
}
```
```rust
// from servo/components/devtools/actors/timeline.rs
impl Encodable for HighResolutionStamp {
fn encode<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
self.0.encode(s)
}
}
```
We can see a recurring pattern where the implementation of a method only consists in applying the same method to a subfield or more generally to an expression containing `self`. Those are examples of the well known [composition pattern][object_composition]. It has a lot of advantages but unfortunately it also implies writing explicit boilerplate code again and again. In a classical oop language we could also have opted for inheritance for similar cases. Inheritance comes with its own bunch of problems and limitations but at least it allows a straightforward form of code reuse: any subclass implicitly imports the public methods of its superclass(es).

Rust has no inheritance (yet) and as a result composition is an even more interesting pattern for factoring code than in other languages. In fact it is already used in many places. Some (approximate) figures:

Project | Occurences of "delegating methods" |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Percentages would be much more useful than numbers here, I don't know what these values mean without a referent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. These figures may not be that useful in the end. My purpose was just to show that the composition pattern is already used in rust projects. I'm sure the percentages are small. (But I do not think it invalidates the proposal: composition is currently handicaped by its heavy syntax so it may not be used as often as it could be)

------------------| ---------------------------------- |
rust-lang/rust | 845 |
rust-lang/cargo | 38 |
servo/servo | 314 |

It would be an improvement if we alleviate the composition pattern so that it can remain/become a privileged tool for code reuse while being as terse as the inheritance-based equivalent. Related discussions:
* [pre-rfc][pre_rfc]
* [some related reddit thread][comp_over_inh] (I didn't participate in)

[pre_rfc]: https://internals.rust-lang.org/t/syntactic-sugar-for-delegation-of-implementation/2633
[comp_over_inh]: https://www.reddit.com/r/rust/comments/372mqw/how_do_i_composition_over_inheritance/
[object_composition]: https://en.wikipedia.org/wiki/Object_composition

# Detailed design
[design]: #detailed-design

## Syntax example

Let's add a syntactic sugar so that the examples above become:
```rust
impl<'a> Hash for H<'a> use self.name;
```
and
```rust
impl Encodable for HighResolutionStamp use self.0;
```

Again this feature adds no new concept. It just simplify an existing code pattern. However it is interesting to understand the similarities and differences with inheritance. The *delegating type* (`H<'a>` in the first example) implicitely "inherit" methods (`hash`) of the *delegated trait* (`Hash`) from the *surrogate type* (`&'static str` which is the type of the expression `self.name`) like a subclass inherit methods from its superclass(es). A fundamental difference is that the delegating type is not a subtype of the surrogate type in the sense of Liskov. There is no external link between the types. The surrogate may even be less visible than the delegating type. Another difference is that the developer has a total control on which part of the surrogate type to reuse whereas class hierarchy forces him/her to import the entire public interface of the superclass (this is because a superclass plays two roles: the one of the surrogate type and the one of the delegated trait).

## Partial delegation

If we consider this code
```rust
// from rust/src/libsyntax/attr.rs
impl AttrMetaMethods for Attribute {
fn check_name(&self, name: &str) -> bool {
let matches = name == &self.name()[..];
if matches {
mark_used(self);
}
matches
}
fn name(&self) -> InternedString { self.meta().name() }
fn value_str(&self) -> Option<InternedString> {
self.meta().value_str()
}
fn meta_item_list(&self) -> Option<&[P<MetaItem>]> {
self.node.value.meta_item_list()
}
fn span(&self) -> Span { self.meta().span }
}
```
we can identify the recurring expression `self.meta()` but 2 of the 5 methods are more complex. This heterogeneity can be handled simply if we allow partial delegation like in
```rust
impl AttrMetaMethods for Attribute use self.meta() {
fn check_name(&self, name: &str) -> bool {
let matches = name == &self.name()[..];
if matches {
mark_used(self);
}
matches
}
fn meta_item_list(&self) -> Option<&[P<MetaItem>]> {
self.node.value.meta_item_list()
}
}
```
Only missing methods are automatically implemented.

In some other cases the compiler just cannot generate the appropriate method. For example when `self` is moved rather than passed by reference, unless the delegating expression produces a result that can itself be moved the borrow checker will complain. In that kind of situations the developer can again provide a custom implementation where necessary and let the compiler handle the rest of the methods.

## Delegation for other parameters

If `Self` is used for other parameters, everything works nicely and no specific treatment is required.
```rust
// from rust/src/libcollections/btree/map.rs
impl<K: PartialOrd, V: PartialOrd> PartialOrd for BTreeMap<K, V> {
fn partial_cmp(&self, other: &BTreeMap<K, V>) -> Option<Ordering> {
self.iter().partial_cmp(other.iter())
}
}
```
becomes
```rust
impl<K: PartialOrd, V: PartialOrd> PartialOrd for BTreeMap<K, V> use self.iter();
```

## Associated types/constants

Unless explicitly set associated types and constants should default to the surrogate implementation value of the corresponding items.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How could a type explicitly set associated types different from the surrogate implementation without introducing a type error?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will certainly introduce a type error. But if you need diverging associated types there's a high chance you want something more complex than delegation anyway. In that case you will certainly have to provide a specific implementation for most of your methods. The proposed feature is only worth it if the delegating type has a behaviour "similar to" the surrogate type. The more it diverges the less valid the entire approach becomes.

An alternative could be to allow additional delegating expressions for associated types. This would create a bridge between DelegatingType::AssociatedType and SurrogateType::AssociatedType and enable the compiler to transform one type into another where required. But in the end I felt that the situations where this would be the expected behaviour might be so specific that I didn't mention that possibility.


## Inverse delegating expressions

Can we handle cases as this one
```rust
// from servo/components/layout/block.rs
impl<T: Clone> Clone for BinaryHeap<T> {
fn clone(&self) -> Self {
BinaryHeap { data: self.data.clone() }
}

fn clone_from(&mut self, source: &Self) {
self.data.clone_from(&source.data);
}
}
```
where `Self` is used as a return type? Yes but we need a second expression for that.
```rust
impl<T: Clone> Clone for BinaryHeap<T> use self.data, BinaryHeap { data: super.clone() };
```
Here the `super` keyword corresponds to an instance of the surrogate type. It is the symmetric of `self`. The whole expression must have type `Self`. Both direct and inverse delegating expressions may be given at the same time or possibly just one of them if only one conversion is needed.

## Combined delegation

It would be nice if delegation could be combined for multiple traits so that
```rust
// from cargo/src/cargo/core/package_id.rs
impl PartialEq for PackageId {
fn eq(&self, other: &PackageId) -> bool {
(*self.inner).eq(&*other.inner)
}
}
impl PartialOrd for PackageId {
fn partial_cmp(&self, other: &PackageId) -> Option<Ordering> {
(*self.inner).partial_cmp(&*other.inner)
}
}
impl Ord for PackageId {
fn cmp(&self, other: &PackageId) -> Ordering {
(*self.inner).cmp(&*other.inner)
}
}
```
could be reduced to the single line
```rust
impl PartialEq + PartialOrd + Ord for PackageId use &*self.inner;
```

## Function-based delegation

Sometimes implementations are trait-free but the same pattern is found like in
```rust
// from rust/src/librustc/middle/mem_categorization.rs
impl<'t, 'a,'tcx> MemCategorizationContext<'t, 'a, 'tcx> {


fn node_ty(&self, id: ast::NodeId) -> McResult<Ty<'tcx>> {
self.typer.node_ty(id)
}
}
```
Here we have no trait to delegate but the same method signatures are reused and semantically the situation is close to a trait-based implementation. A simple possibility could be to introduce a new trait. An alternative is to allow delegation at method level.
```rust
impl<'t, 'a,'tcx> fn node_ty for MemCategorizationContext<'t, 'a, 'tcx> use self.typer;
```

## More complex delegation

`Self` can also appear inside more complex parameter/result types like `Option<Self>` or `&[Self]`. If we had HKT in Rust a partial solution based on [functor types][functors] might have been possible. It could still be possible to handle specific cases like precisely options and slices but I have not thought hard about it. The complexity might not be worth it.

[functors]: https://wiki.haskell.org/Functor

# Drawbacks
[drawbacks]: #drawbacks

* It creates some implicit code reuse. This is an intended feature but it could also be considered as dangerous. Modifying traits and surrogate types may automatically import new methods in delegating types with no compiler warning even in cases it is not appropriate (but this issue is the same as modifying a superclass in OOP).
* The benefit may be considered limited for one-method traits.

# Alternatives
[alternatives]: #alternatives

## OOP inheritance

As mentioned before, inheritance can handle similar cases with the advantage its concepts and mechanisms are well known. But with some drawbacks:
* Multiple inheritance is possible but to my knowledge no serious proposition has been made for Rust and I doubt anyone wants to end up with a system as complex and tricky as C++ inheritance (whereas delegation is naturally "multiple delegation")
* As said before inheritance mixes orthogonal concepts (code reuse and subtyping) and does not allow fine grain control over which part of the superclass interface is inherited.

## Multiple derefs

Some people noticed a similarity with trait `Deref`. A main limitation is that you can only deref to a single type. However one could imagine implementing multiple derefs by providing the target type as a generic parameter (`Deref<A>`) rather than as an associated type. But again you can find limitations:
* As for inheritance visibility control is impossible: if `B` can be derefed to `A` then the entire public interface of `A` is accessible.
* `Deref` only offers a superficial similarity. If `A` implements trait `Tr`, instances of `B` can sometimes be used where `Tr` is expected but as a counter example a `[B]` array is not assignable to `fn f(t: [T]) where T : Tr`. Derefs do not interact nicely with bounded generic parameters.

## Compiler plugin

I was suggested to write a compiler plugin. But I was also told that [type information is not accessible][type_information] (unless you can annotate the surrogate type yourself, which implies you must own it). Moreover I'm not sure a plugin could easily solve the partial delegation cases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a macro requires type information any more than #[derive] does (and it doesn't). If you have a macro like #[delegate(Trait => member)] or whatever, it can generate an impl with the body of each method calling that method on self.member.

More complex cases would be more difficult to implement, of course, but they don't introduce any dependence on type information. I think even if this were built into the language, it would be sugar that would be implemented in AST -> HIR, without type information.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that, as far as I know, there is (currently) no way to lookup Trait in a compiler plugin so there's no way to know what methods need to be implemented.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Hopefully when syntax extensions are revisited this will be addressed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this would require two macro expansion phases (one for macros that can add new namable items and one for macros that can lookup named items).


[type_information]: http://stackoverflow.com/questions/32641466/when-writing-a-syntax-extension-can-i-look-up-information-about-types-other-tha

## Do nothing

In the end, it is a syntactic sugar. It just improves the ease of expression, not the capacity to express more concepts. Some simple cases may be handled with deref, others with trait default methods.

One of my concerns is that the arrival of inheritance in Rust may encourage bad habits. Developers are lazy and DRY principle dissuades them from writing repetitive code. The temptation may be strong to overuse inheritance in situations where only code reuse is required (resulting in unnecessary subtyping hierarchy and uncontrolled interface exposure).

# Unresolved questions
[unresolved]: #unresolved-questions

The exact syntax is to be discussed. The proposed one is short but does not name the surrogate type explicitly which may hurt readability.