hobofan / ambassador

Delegation of trait implementations via procedural macros
Apache License 2.0
245 stars 13 forks source link

Add support for non-self methods and non-member delegation #15

Open Cydhra opened 4 years ago

Cydhra commented 4 years ago

Delegating stuff that takes self or &self covers many use cases, but there are more. Imagine a simple example like this:

use ambassador::delegatable_trait;
use ambassador::Delegate;
use num::BigUint;

struct FastSqrt;

trait DiscreteSquareRootProtocol {
    fn sqrt(value: BigUint, modulus: BigUint) -> BigUint;
}

impl DiscreteSquareRootProtocol for FastSqrt { 
    // snip
}

#[derive(Delegate)]
#[delegate(DiscreteSquareRootProtocol, by = FastSqrt)]
struct WrapperProtocol;

This example is obviously a little bit constructed, as it doesn't make very much sense to not have self as a parameter in the sqrt function. But it's an example that I could quickly come up with, that does not have as much strings attached as my original use case. To give some perspective: I'm currently writing a library with a huge number of protocols with different default implementations (imagine a second implementation impl DiscreteSquareRootProtocol for DeterministicSqrt) and I want the user to assemble a "super-protocol" that implements all those traits and just delegates them to the default implementations. For technical reasons, none of those traits has self as a parameter. (Tbh my protocols heavily rely on generics, so this library won't help me anyway, but I thought delegation to types would be nice, anyway).

I intentionally chose to use by as the parameter in the delegate macro, to differentiate between type-delegation and struct-field-delegation, but if this proofs to be unnecessary, target will do just as fine.

hobofan commented 4 years ago

Thanks for the input!

I did consider adding support for delegating associated functions, but they weren't a big priority, as I personally didn't encounter a use-case for them, but the one you provided sounds interesting. I would even generalize it and expand it to delegation of associated items in general.

I intentionally chose to use by as the parameter in the delegate macro, to differentiate between type-delegation and struct-field-delegation, but if this proofs to be unnecessary, target will do just as fine.

I think that was a good call, and as I see it, having a separate keyword for delegation of associated items is probably necessary. When you consider a trait that has both associated items and methods that take &self, the delegation for the two groups would work quite differently, especially for enums, where the type of the target for each variant field can be different, while it would be fixed for type-delegation.

After thinking about it for a bit, I think really nailing this feature will take a lot of careful consideration of corner cases, and we also need to make sure that it won't make delegation as a whole too confusing.


Having said all that, I think a minimal initial feature set, like the one outlined in your example (no-field structs + traits with only associated functions), shouldn't pose a problem and could certainly become a part of the crate in the near future!


Tbh my protocols heavily rely on generics, so this library won't help me anyway, but I thought delegation to types would be nice, anyway

Are there any specific roadblocks you are thinking of that you could elaborate on? The current master branch has support for specifying where clauses in addition to the existing support for generic type bounds. Ambassador in general is aimed to support generics, so if there is anything in that department that you are missing, I'd love to hear it. If you specifically meant generics in the context of type-level delegation, I'd also be interested in your thoughts there, as the interface we are designing there should also be able to accommodate generic use-cases.

Cydhra commented 4 years ago

and we also need to make sure that it won't make delegation as a whole too confusing.

Well, maybe that would be a use-case for #13. If we force the user by API to differentiate between associated functions and methods, we could cut some corner cases at the expense of a more verbose API. Just an idea.

Having said all that, I think a minimal initial feature set, like the one outlined in your example (no-field structs [...]

Is it such a huge difference to include field structs? Since the delegation ignores fields and any state anyway, there should not be much difference between the struct types, right? The same should be true for enums: While their variants may look/behave differently, their content should be irrelevant to associated functions, anyway.

Ambassador in general is aimed to support generics

My traits are generic, and there are no examples in the readme and no test cases that showcase how to do that, and I was unable to get it to work with a generic trait:

use ambassador::delegatable_trait;
use ambassador::Delegate;
use num::BigUint;

struct FastSqrt;

#[delegatable_trait]
trait DiscreteSquareRootProtocol<T> {
    fn sqrt(&self, value: T, modulus: T) -> T;
}

impl<T> DiscreteSquareRootProtocol<T> for FastSqrt {
    fn sqrt(&self, value: T, modulus: T) -> T { unimplemented!() }
}

#[derive(Delegate)]
#[delegate(DiscreteSquareRootProtocol)]
struct WrapperProtocol(FastSqrt);

Here, T is not found by the ambassador::Delegate macro, and if I add T to WrapperProtocol using a PhantomData marker, the macro expects T in the #[delegate(DiscreteSquareRootProtocol)] macro, however the macro cannot parse #[delegate(DiscreteSquareRootProtocol<T>)]. So I just assumed, that this was indeed unsupported. Am I just doing it wrong?

hobofan commented 4 years ago

Ah, I did not considered generic traits yet, as I rarely use them myself. I've opened up a new issue for that: #17

dsully commented 2 months ago

I'm finding the need for this as well.

I'd like to have an associated trait function such as:

pub trait MyTrait {
    fn parse(input: &str) -> Result<Self>;
}

However that is currently not possible with ambassador.