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

Feature request: Simplify generic notation #7763

Closed
ChristianRygg opened this issue Jan 4, 2016 · 8 comments
Closed

Feature request: Simplify generic notation #7763

ChristianRygg opened this issue Jan 4, 2016 · 8 comments

Comments

@ChristianRygg
Copy link

I love using generics, but tend to end up in the same mess everytime I get carried away with them: At some point interfaces get a long list of generic types, most of which theoretically can be inferred from the rest.

Take this simplified example (with real examples having longer lists of types):

    // Interfaces
    public interface IMessage
    {
    }

    public interface IWrappedMessage<TMessage> where TMessage : IMessage
    {
        TMessage Message { get; set;  }
    }

    public interface IHandler<in TWrappedMessage, TMessage> 
        where TWrappedMessage : IWrappedMessage<TMessage>
        where TMessage : IMessage
    {
        bool HandleMessage(TWrappedMessage msg);
    }

    // Implementations
    public class TextMessage : IMessage
    {
        public string Text { get; set; }
    }

    public class EmailTextWrapper : IWrappedMessage<TextMessage>
    {
        public EmailTextWrapper(TextMessage message)
        {
            Message = message;
            SendToEmailAddress = "some.email@address.nul";
        }

        public TextMessage Message { get; set; }
        public string SendToEmailAddress { get; set; }
    }

    public class EmailTextMessageSender : IHandler<EmailTextWrapper, TextMessage>
    {
        public bool HandleMessage(EmailTextWrapper msg)
        {
            // TODO: Send e-mail to specified address
            return true;
        }
    }

In this case, EmailTextMessageSender is a handler for EmailTextWrapper and the TextMessage type is given from EmailTextWrapper.

Would it be possible to simplify the notation to not have to specify the second type somehow? I see that it might be needed if the type should be used directly in the interface, but perhaps it is possible to only specify it at the highest level (in this case the IHandler interface) and not in the implementation, something like this:

    public interface IHandler<in TWrappedMessage, /* some magic keyword here */ TMessage> 
        where TWrappedMessage : IWrappedMessage<TMessage>
        where TMessage : IMessage

    /* snip */

    public class EmailTextMessageSender : IHandler<EmailTextWrapper>

    /* snip */

Perhaps it's just my coding style, but this would be a great feature so both simplify coding and making the resulting code more readable.

@HaloFour
Copy link

HaloFour commented Jan 4, 2016

I've sometimes avoided this kind of generic knots through helper interfaces. I know that your code sample is only supposed to be illustrative and simplified but I have to ask about why IHandler cares about both the wrapper type and the wrapped type? In those kinds of situations the wrapper type would fix the wrapped type anyway so it doesn't really make sense for the handler to have to work with it generically.

@ChristianRygg
Copy link
Author

Yes, in my case it does care. My example doesn't perfectly describe my scenario: IHandlerResolver would be a better name than IHandler.

Imagine a component (IHandlerResolver) which takes an IWrappedMessage and uses parameters on the wrapped message to do stuff like authentication/authorization/logging. It then locates an appropriate IHandler (IOC container?) and calls this handler with the wrapped message.

I could take a non-generic version of the interface in and get the inner type through reflection, but that seems like a step back rather than forwards...

@iskiselev
Copy link

I thought about same today. If C# would be able to infer generic parameters and their constraints on their declaration., it should cover original problem and lot's of similar problems.
Consider next example:

    public interface IA<TA, TB, TC> 
        where TA : IA<TA, TB, TC>
        where TB : IB<TA, TB, TC>
        where TC : IC<TA, TB, TC>
    {
        TB B { get; }
        TC C { get; }
    }

    public interface IB<TA, TB, TC>
        where TA : IA<TA, TB, TC>
        where TB : IB<TA, TB, TC>
        where TC : IC<TA, TB, TC>
    {
        TA A { get; }
        TC C { get; }
    }

    public interface IC<TA, TB, TC>
        where TA : IA<TA, TB, TC>
        where TB : IB<TA, TB, TC>
        where TC : IC<TA, TB, TC>
    {
        TA A { get; }
        TB B { get; }
    }

    public class A : IA<A,B,C>
    {
        public B B { get; set; }
        public C C { get; }
    }

    public class B : IB<A,B,C>
    {
        public A A { get; set; }
        public C C { get; set; }
    }

    public class C : IC<A, B, C>
    {
        public A A { get; set; }
        public B B { get; set; }
    }

    public class A2 : IA<A2,B2,C2>
    {
        public B2 B { get; set; }
        public C C { get; }
    }

    public class B : IB<A,B,C>
    {
        public A A { get; set; }
        public C C { get; set; }
    }

    public class C : IC<A, B, C>
    {
        public A A { get; set; }
        public B B { get; set; }
    }

    public static class Helpers
    {
        public static TB GetB<TA, TB, TC>(this IA<TA, TB, TC> a)
            where TA : IA<TA, TB, TC>
            where TB : IB<TA, TB, TC>
            where TC : IC<TA, TB, TC>
        {
            return a.B;
        }
    }

Here created interfaces IA, IB, IC that reference each other and require their implementation by same concrete class group. It allow implement common logic for all implementation of this interfaces that would not obscure concrete class type, so that we still can do new A().GetB().C = new C(), despite we have no setter in IC<,,> interface (see Helpers.GetB).

Main problem of such solution currently is requirements to write long generic arguments declarations and constraint even when they could be inferred.

Let's suppose next rules: if class/function declaration starts with ?, it's generic parameters and their constraints would be calculated automatically based on class/function other signature elements. Order of generic parameters that are inferred would be based on their order in other parts of signature (and will be after parameters declared inside generic brackets) and their constraints will be sum of constraints based on their usage in signature. My example in this pseudo-syntax could be rewritten in:

    // Will be inferred in <TA,TB,TC> based on usage order
    // If we ommit TA in and write just IA<?> it would be inferred in IA<TB, TA, TC>,
    // based on their in second line
    // TA will additionally have constraint TA : IA<TB, TA, TC>
    public interface IA<?TA> 
        where TB : IB<TA, TB, TC>
        where TC : IC<TA, TB, TC>
    {
        TB B { get; }
        TC C { get; }
    }

    // Will be inferred in <TA,TB,TC> based on usage order
    // TB will additionally have constraint TA : IA<TA, TB, TC>
    public interface IB<?>
        where TA : IA<TA, TB, TC>
        where TC : IC<TA, TB, TC>
    {
        TA A { get; }
        TC C { get; }
    }

    // Will be inferred in <TA,TB,TC> based on usage order
    // TC will additionally have constraint TC : IC<TA, TB, TC>
    public interface IC<?>
        where TA : IA<TA, TB, TC>
        where TB : IB<TA, TB, TC>
    {
        TA A { get; }
        TB B { get; }
    }

    public class A : IA<A,B,C>
    {
        public B B { get; set; }
        public C C { get; }
    }

    public class B : IB<A,B,C>
    {
        public A A { get; set; }
        public C C { get; set; }
    }

    public class C : IC<A, B, C>
    {
        public A A { get; set; }
        public B B { get; set; }
    }

    public static class Helpers
    {
        //Parameters and constraints are inferred
        public static TB GetB<?>(this IA<TA, TB, TC> a)
        {
            return a.B;
        }
    }

But it can be improve even more, if we allow same mechanics in generic argument usage.
Let's transform sample from first message in topic:

    // Interfaces
    public interface IMessage
    {
    }

    public interface IWrappedMessage<TMessage>
        where TMessage : IMessage
    {
        TMessage Message { get; set; }
    }

    // Will be IHandler<?in TWrappedMessage, TMessage>
    // and TMessage : IMessage based on IWrappedMessage<> defenition
    public interface IHandler<?in TWrappedMessage>
        where TWrappedMessage : IWrappedMessage<TMessage>
    {
        bool HandleMessage(TWrappedMessage msg);
    }

    // Implementations
    public class TextMessage : IMessage
    {
        public string Text { get; set; }
    }

    public class EmailTextWrapper : IWrappedMessage<TextMessage>
    {
        public EmailTextWrapper(TextMessage message)
        {
            Message = message;
            SendToEmailAddress = "some.email@address.nul";
        }

        public TextMessage Message { get; set; }
        public string SendToEmailAddress { get; set; }
    }

    //Second argument would be inferred automagically based on
    //EmailTextWrapper : IWrappedMessage<TextMessage> and IWrappedMessage<TMessage> where TWrappedMessage : IWrappedMessage<TMessage>
    //I can't say now if it is possible matematically, but let's discuss it.
    public class EmailTextMessageSender : IHandler<?EmailTextWrapper>
    {
        public bool HandleMessage(EmailTextWrapper msg)
        {
            // TODO: Send e-mail to specified address
            return true;
        }
    }

Feels that last one is very raw, but we can discuss it. It also should relate to calling method with inferring only part of generic arguments - not sure if there is already issue with such discussion.

@khellang
Copy link
Member

Sounds a bit like #7850 😄

@khellang
Copy link
Member

Related issue: #5023

@alrz
Copy link
Member

alrz commented Jan 23, 2016

@ChristianRygg why not

    public interface IWrappedMessage<out TMessage> where TMessage : IMessage
    {
        TMessage Message { get; set;  }
    }
    public interface IHandler<in TWrappedMessage> 
        where TWrappedMessage : IWrappedMessage<IMessage>       
    {
        bool HandleMessage(TWrappedMessage msg);
    }

Though, the whole design can be reduced with higher kinded generics (#2212).

@ChristianRygg
Copy link
Author

@alrz I realize that my example doesn't really express the problem clearly enought, but I really would prefer getting the actuall class rather than the interface when handling the message. I could of course do my own casting and type checking, but in my mind this is even more complex than the situation I was hoping to simplify...

@gafter
Copy link
Member

gafter commented Mar 20, 2017

We are now taking language feature discussion on https://github.com/dotnet/csharplang for C# specific issues, https://github.com/dotnet/vblang for VB-specific features, and https://github.com/dotnet/csharplang for features that affect both languages.

@gafter gafter closed this as completed Mar 20, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants