LokiMidgard / PartialMixins

Extends C# with Mixins.
MIT License
24 stars 2 forks source link

Allow for type-name substitution (perhaps using parameter attributes) #8

Closed daiplusplus closed 3 years ago

daiplusplus commented 3 years ago

Background problem

A major use-case for mixins is for adding operator overloads to types - however C# operator overloads are static and the compiler enforces special-rules that the declaring type be one or both of the parameter types. Currently the PartialMixins tooling doesn't allow us to add implicit operator overloads, for example.

Currently, this doesn't work:

    [Mixin(typeof(Shared))]
    public partial class Test
    {
    }

    public class Shared
    {
        public static implicit operator Int32( Shared self )
        {
            return 1;
        }

        public static operator Shared+( Shared left, Shared right ) => new Shared();
    }

The generated mixin class Test is this:

    public partial class Test
    {
        public static implicit operator Int32(Shared self)
        {
            return 1;
        }

        public static Shared operator+( Shared left, Shared right ) => new ...;
    }

...which fails at compilation because the Shared self parameter should be Test self, and the Shared+ operator should similarly use Test instead of Shared for the types:

CS0556: User-defined conversion must convert to or from the enclosing type: PartialMixins.g.cs

Proposed solution

I'd like to be able to denote type substitutions for the mixin, which could be done with parameter attributes, like so (using a hypothetical attribute class UseSubjectAttribute : Attribute).

    [Mixin(typeof(Shared))]
    public partial class Test
    {
    }

    public class Shared
    {
        public static implicit operator Int32( [UseSubject] Shared self )
        {
            return 1;
        }

        [return: UseSubject]
        public static Shared operator+( [UseSubject] Shared left, [UseSubject] Shared right ) => ...;
    }

Alternative solution 1

Another possibility is to allow for trivial textual transformations to be specified directly in the Mixin attribute, like so:

    [Mixin(typeof(Shared), nameof(Test), nameof(Test) )] // `class MixinAttribute( Type mixin, params String[] placeholderValues )`
    public partial class Test
    {
    }

    public class Shared
    {
        public static implicit operator Int32( [Placeholder(0)] Shared self )
        {
            return 1;
        }

        struct PlaceholderFoo {
            Shared GetShared() { throw new NotImplementedException(); } // or use an interface?
        }

                [TextPlaceholder(1, typeof(PlaceholderFoo))]
        [return: Placeholder(0)]
        public static Shared operator+( [Placeholder(0)]  Shared left, [Placeholder(0)] Shared right )
        {
            PlaceholderFoo foo = new PlaceholderFoo();
                        return foo.GetShared();
        }
    }

When the mixin is copied into the receiver class, all parameters (and return-types) tagged with PlaceholderAttribute(1) are lexically replaced with "Test" (as it's specified in [Mixin(typeof(Shared), nameof(Test) )]) while type-names (or any text in general) inside a member function can be specified using the hypothetical attribute [TextPlaceholder] while the method uses real types (in this case struct PlaceholderFoo) which gets replaced with the second value from the receivers params String[] placeholderValues, so the above is compiled to this:

    public partial class Test
    {
        public static implicit operator Int32( Test self )
        {
            return 1;
        }

    public static  Test operator+( Test left, Test right )
    {
        Test foo = new Test();
        return foo.GetShared();
    }
    }

This approach has the benefit of allowing the "template" source to still compile as valid C# (so we can avoid using T4) while allowing more powerful functionality closer to C++ templates - without introducing the limitations of C#'s generics as a means of parameterisation (as mentioned below).

Alternative solution 2

I recognize that this could also be done with generics: so the mixin becomes a generic type and the mixin receiver (the subject) becomes a type-parameter, however I don't recommend this approach because that then makes it more difficult to have actual generic mixins - as well as how this mixins tooling operators at the lexical layer (like C++ templates) which affords us far more flexibility than C#'s more limited generics-constraints.

    [Mixin(typeof(Shared<Test>))]
    public partial class Test
    {
    }

    public class Shared<T>
    {
        public static implicit operator Int32( T self ) // <-- Won't compile because `Shared<T>` is not `T`.
        {
            return 1;
        }

        public static T operator+( T left, T right )
                {
                    // etc
                }
    }
LokiMidgard commented 3 years ago

I like your first solution. I will probably implement that at first.

The second is more flexible, and I think it could be implemented additionally later.

LokiMidgard commented 3 years ago

@Jehoel I added support for to replace the type for parameters and return types. I named the attribute Substitute. I also added support for abstract methods. They will be implemented using partial methods. Otherwise I found it problematic to come up with a sample, since everything used by the Mixin must be defined in the mixin.

Version 1.0.46 should soon be available on nuget.

I haven't added support for multiple replacements like in solution 2. I will make an new issue for that. But I'm not sure if I'll go to implement it soon.