PartialMixins
PartialMixins copied to clipboard
Add support to replace parameter with multiple different types (Like Generic parameter?)
As in issue #8 sugestet, it may be nice if parameter and other Types could be replaced with different types.
From original issue
[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).
ToDo
- [ ] More specifications
I'm not sure yet for what input is needed and what output expected. If I understand it correctly PlaceholderFoo is some kind of template. Maybe an Interface or another (or the same) mixin.
I'm not sure yet for what input is needed and what output expected. If I understand it correctly PlaceholderFoo is some kind of template. Maybe an Interface or another (or the same) mixin.
Yeah, I was iterating over ideas as I was writing my post and I didn't provide more details about how struct PlaceholderFoo was meant to work.
So here's the idea for struct PlaceholderFoo:
-
The motivation is that we need parameterized mixins:
- Given that C# doesn't support multiple-inheritance for
classtypes - but does support multipleinterfaceimplementation and reimplementation - compile-time mixins that interact with their new containing type can only work if the name of the new containing type can be used inside the mixin, which necessitates the need for mixin template type-name placeholders.- (A type-name placeholder being a specific type of template parameter - as mixin templates could have other parameters for things like names, literals, other identifiers, etc)
- Non-parameterized mixin templates otherwise behave the same as copying+pasting the body of one
classinto another, which restricts many useful applications of mixins because of C#'s various rules; a good example is implementing operator overloading.
- Given that C# doesn't support multiple-inheritance for
-
So given that we need parameterised mixins, why not just use a C# generic parameter as a placeholder?
- Because C# generics are very limited: a C# generic class cannot derive-from nor implement a generic type argument, nor can a type parameter be used to represent
this(this worsens C#'s problem of not supporting covariant return types... for now). You also cannot specify default types for generic parameters either. - But the main reason to not use generics is that it then restricts you from making a mixin template that actually is generic (because then it's unclear which generic parameters should be considered as template parameters as opposed to generic type parameters that should be preserved in the rendered mixin).
- Because C# generics are very limited: a C# generic class cannot derive-from nor implement a generic type argument, nor can a type parameter be used to represent
-
Instead, we could represent placeholders in the mixin template body with actual (non-generic) types - but using dummy types (typically this is an empty
structwith no members).- Using dummy-types to exert more control over C# generic monomorphism is a pretty powerful technique; for example, you can use this approach to supply additional non-type-name parameters to a C# generic type, but that's another topic).
- Using actual types means we can specify things like required or expected interfaces, members, even their inheritance tree - just like with real C# generics and still get our mixin template code to compile.
So the canonical example is implementing operator overloading - I'll demonstrate the approach using a dummy placeholder type (the struct PlaceholderFoo type) with the canonical tedious example of implementing IComparable<T> and overloading comparison operators:
partial class ComparableMixin : IComparable<ComparableMixin>, IEquatable<ComparableMixin>
{
public static Boolean operator==(ComparableMixin left, ComparableMixin right)
{
if( left is null && right is null ) return true;
if( left is null || right is null ) return false;
return left.Equals( other: right ); // Invoke IComparable<T>.Equals, not Object.Equals, btw
}
public static Boolean operator!=(ComparableMixin left, ComparableMixin right)
{
if( left is null && right is null ) return false;
if( left is null || right is null ) return true;
return !left.Equals( other: right );
}
public static Boolean operator<(ComparableMixin left, ComparableMixin right)
{
if( left is null && right is null ) return false;
if( left is null ) return true;
if( right is null ) return false;
return left.CompareTo(right) < 0;
}
public static Boolean operator<=(ComparableMixin left, ComparableMixin right)
{
if( left is null && right is null ) return true;
if( left is null ) return true;
return left.CompareTo(right) <= 0;
}
public static Boolean operator>(ComparableMixin left, ComparableMixin right)
{
if( left is null && right is null ) return false;
if( left is null ) return false;
if( right is null ) return true;
return left.CompareTo(right) > 0;
}
public static Boolean operator>=(ComparableMixin left, ComparableMixin right)
{
if( left is null && right is null ) return true;
if( left is null ) return false;
if( right is null ) return true;
return left.CompareTo(right) >= 0;
}
public Boolean Equals( ComparableMixin? other ) // IEquatable<ComparableMixin>.Equals
{
if( other is null ) return false;
Boolean isEqual = default;
this.EqualsImpl( other, isEqual: ref isEqual );
return isEqual;
}
public Int32 CompareTo( ComparableMixin? other ) // IComparable<ComparableMixin>.CompareTo
{
if( other is null ) return 1;
Int32 cmp = default;
this.CompareImpl( other, cmp: ref cmp );
return cmp;
}
partial void EqualsImpl( ComparableMixin other, ref Boolean isEqual );
partial void CompareImpl( ComparableMixin other, ref Int32 cmp );
public override Boolean Equals(Object? obj) // C# complains if we implement IEquatable without overriding Object.Equals
{
if( obj is ComparableMixin other )
{
return this.Equals( other: other );
}
else
{
return false;
}
}
public override Int32 GetHashCode() // C# complains if we override Object.Equals without overriding Object.GetHashCode
{
Int32 hash = default;
this.GetHashCodeImpl( hash: ref hash );
return hash;
}
partial void GetHashCodeImpl( ref Int32 hash );
}
- Note that I assume that the mixin system would not only copy the class's body to the destination type, but also copies over the supertype and declared implements-interfaces parts.
Some problems with the above example:
- First-and-foremost: all occurrences of "
ComparableMixin" inside the mixin template always needs to be replaced with the destination consuming type (as an invariant type) - however users might want some occurrences to be replaced with - To avoid compile-time errors for not implementing
IEquatable<T>orIComparable<T>the mixin must fully implement those interfaces, but must still hand-off control to the destination type for the real implementation - which means needing to usepartial void EqualsImplto stub it out - which then means the destination mixin consumer needs to also implement methods with the exact same signature but without the benefit of an interface or realpartialmethod to ensure the signature matches.
(TODO: Finish writing this post...)
I'm not sure what this paragraph means:
But the main reason to not use generics is that it then restricts you from making a mixin template that actually is generic (because then it's unclear which generic parameters should be considered as template parameters as opposed to generic type parameters that should be preserved in the rendered mixin).
In the End every parameter must be replace sooner or later and it is the desision of the user of the type to decide when.
This is not different from other generics. E.g.
public class MyDirectory<T> : Dictionary<T,string> { }
To avoid compile-time errors for not implementing IEquatable<T> or IComparable<T> the mixin must fully implement those interfaces, but must still hand-off control to the destination type for the real implementation - which means needing to use partial void EqualsImpl to stub it out - which then means the destination mixin consumer needs to also implement methods with the exact same signature but without the benefit of an interface or real partial method to ensure the signature matches.
I'm not sure which specific version of c# you need for that feature and if it also need a specific dot net verions, but if you add an modifier to the partial method, the compiler ensures that the method is implemented. You can also use return types in that case and don't need a ref parameter
public partial int GetHashCodeImpl();

The only problem with that usage is that the Mixin class itself needs an implementation. For that reason I added a feature that will replace abstract methods with partial ones.
So with the current implementation of Mixins your sample would look like that:
[Mixin.Mixin(typeof(ComparableMixin))]
public partial class Test
{
public partial bool EqualsImpl(Test other) => false;
public partial int CompareImpl(Test other) => -1;
public partial int GetHashCodeImpl() => 1;
}
abstract class ComparableMixin : System.IComparable<ComparableMixin>, System.IEquatable<ComparableMixin>
{
public static Boolean operator ==([Mixin.Substitute] ComparableMixin left, [Mixin.Substitute] ComparableMixin right)
{
if (left is null && right is null) return true;
if (left is null || right is null) return false;
return left.Equals(other: right); // Invoke IComparable<T>.Equals, not Object.Equals, btw
}
public static Boolean operator !=([Mixin.Substitute] ComparableMixin left, [Mixin.Substitute] ComparableMixin right)
{
if (left is null && right is null) return false;
if (left is null || right is null) return true;
return !left.Equals(other: right);
}
public static Boolean operator <([Mixin.Substitute] ComparableMixin left, [Mixin.Substitute] ComparableMixin right)
{
if (left is null && right is null) return false;
if (left is null) return true;
if (right is null) return false;
return left.CompareTo(right) < 0;
}
public static Boolean operator <=([Mixin.Substitute] ComparableMixin left, [Mixin.Substitute] ComparableMixin right)
{
if (left is null && right is null) return true;
if (left is null) return true;
return left.CompareTo(right) <= 0;
}
public static Boolean operator >([Mixin.Substitute] ComparableMixin left, [Mixin.Substitute] ComparableMixin right)
{
if (left is null && right is null) return false;
if (left is null) return false;
if (right is null) return true;
return left.CompareTo(right) > 0;
}
public static Boolean operator >=([Mixin.Substitute] ComparableMixin left, [Mixin.Substitute] ComparableMixin right)
{
if (left is null && right is null) return true;
if (left is null) return false;
if (right is null) return true;
return left.CompareTo(right) >= 0;
}
public Boolean Equals([Mixin.Substitute] ComparableMixin? other) // IEquatable<ComparableMixin>.Equals
{
if (other is null) return false;
return this.EqualsImpl(other);
}
public Int32 CompareTo([Mixin.Substitute] ComparableMixin? other) // IComparable<ComparableMixin>.CompareTo
{
if (other is null) return 1;
return this.CompareImpl(other);
}
public abstract bool EqualsImpl([Mixin.Substitute] ComparableMixin other);
public abstract int CompareImpl([Mixin.Substitute] ComparableMixin other);
public override Boolean Equals(Object? obj) // C# complains if we implement IEquatable without overriding Object.Equals
{
if (obj is ComparableMixin other)
{
return this.Equals(other: other);
}
else
{
return false;
}
}
public override Int32 GetHashCode() // C# complains if we override Object.Equals without overriding Object.GetHashCode
{
var hash = this.GetHashCodeImpl();
return hash;
}
public abstract int GetHashCodeImpl();
}
There are two problems with the current implementation and this sample. First the interfaces will be copied exactly as written withoud fully quallifieing it. So in the Sample above I added the System namespace to mitigate this.
The seccond is that the SubstitueAttribute does not support Generic Parameters. So the type there will also not be replaced.
Which raises the question, does it need the Attribute at all? Or do we always want to replace the own Identifier with the concrete type?
I don't think an empty struct as an placehoder will work. You can't do anything with it in the mixin. Since an empty struct does not have any members, you can't call any method on it. I think the only valid Approach for replacement types are other mixins. (You could use refleation, but I don't like that aproach, and you would still be able to use it, when you use Generic arguments.)
For that the mixins themselves should be marked with an attribute. In that case you would not need to define the different replace parameters on the mixin, but every type that is used and is decorated with the mixin attribute needs to be replaced. And we can analize if some replacment paramters are missing.
On the consuming side you can then make an mapping of mixin to actual type that should be used.
I noticed some problems when I tested some implementations.
- You can't have attributes when referencing classes with generic parameters where you fill them out
// Valid Invalid
// | |
// V V
class ComparableMixin<[Mixin.Substitute] T> : IComparable<ComparableMixin<[Mixin.Substitue]T>>
- An Attribute Argument cannot use type parameters
// INVALID
// |
// V
[Mixin.Mixin(typeof(ComparableMixin<T>))]
public partial class Test<T>
The first problem can be removed if we no longer use the SubstituedAtribute but replace every Mixin automaticly. For the second problem we could use some replacement structs. Something like struct TypeParameter1. That is replaced with the first type parameter. We can have some predefined structs, but the user needs to define its own, since it must satisfy the constrains of the generic type parameter.
@Jehoel I fixed some bugs so your sample should work with the current implementation. (Version 1.0.52)
Sample Code
Mixin:
abstract class ComparableMixin : IComparable<ComparableMixin>, IEquatable<ComparableMixin>
{
public static bool operator ==(ComparableMixin left, ComparableMixin right)
{
if (left is null && right is null) return true;
if (left is null || right is null) return false;
return left.Equals(other: right); // Invoke IComparable<T>.Equals, not Object.Equals, btw
}
public static bool operator !=(ComparableMixin left, ComparableMixin right)
{
if (left is null && right is null) return false;
if (left is null || right is null) return true;
return !left.Equals(other: right);
}
public static bool operator <(ComparableMixin left, ComparableMixin right)
{
if (left is null && right is null) return false;
if (left is null) return true;
if (right is null) return false;
return left.CompareTo(right) < 0;
}
public static bool operator <=(ComparableMixin left, ComparableMixin right)
{
if (left is null && right is null) return true;
if (left is null) return true;
return left.CompareTo(right) <= 0;
}
public static bool operator >(ComparableMixin left, ComparableMixin right)
{
if (left is null && right is null) return false;
if (left is null) return false;
if (right is null) return true;
return left.CompareTo(right) > 0;
}
public static bool operator >=(ComparableMixin left, ComparableMixin right)
{
if (left is null && right is null) return true;
if (left is null) return false;
if (right is null) return true;
return left.CompareTo(right) >= 0;
}
public bool Equals(ComparableMixin? other) // IEquatable<ComparableMixin>.Equals
{
if (other is null) return false;
return this.EqualsImpl(other);
}
public int CompareTo(ComparableMixin? other) // IComparable<ComparableMixin>.CompareTo
{
if (other is null) return 1;
return this.CompareImpl(other);
}
public abstract bool EqualsImpl(ComparableMixin other);
public abstract int CompareImpl(ComparableMixin other);
public override bool Equals(object? obj) // C# complains if we implement IEquatable without overriding Object.Equals
{
if (obj is ComparableMixin other)
{
return this.Equals(other: other);
}
else
{
return false;
}
}
public override int GetHashCode() // C# complains if we override Object.Equals without overriding Object.GetHashCode
{
var hash = this.GetHashCodeImpl();
return hash;
}
public abstract int GetHashCodeImpl();
}
Consuming Type:
[Mixin.Mixin(typeof(ComparableMixin))]
public partial class Test
{
public partial bool EqualsImpl(Test other) => false;
public partial int CompareImpl(Test other) => -1;
public partial int GetHashCodeImpl() => 1;
}
Generated code:
namespace ConsoleApp11
{
public partial class Test : global::System.IComparable<ConsoleApp11.Test>, global::System.IEquatable<ConsoleApp11.Test>
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public static bool operator ==(ConsoleApp11.Test left, ConsoleApp11.Test right)
{
if (left is null && right is null)
return true;
if (left is null || right is null)
return false;
return left.Equals(other: right); // Invoke IComparable<T>.Equals, not Object.Equals, btw
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public static bool operator !=(ConsoleApp11.Test left, ConsoleApp11.Test right)
{
if (left is null && right is null)
return false;
if (left is null || right is null)
return true;
return !left.Equals(other: right);
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public static bool operator <(ConsoleApp11.Test left, ConsoleApp11.Test right)
{
if (left is null && right is null)
return false;
if (left is null)
return true;
if (right is null)
return false;
return left.CompareTo(right) < 0;
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public static bool operator <=(ConsoleApp11.Test left, ConsoleApp11.Test right)
{
if (left is null && right is null)
return true;
if (left is null)
return true;
return left.CompareTo(right) <= 0;
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public static bool operator>(ConsoleApp11.Test left, ConsoleApp11.Test right)
{
if (left is null && right is null)
return false;
if (left is null)
return false;
if (right is null)
return true;
return left.CompareTo(right) > 0;
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public static bool operator >=(ConsoleApp11.Test left, ConsoleApp11.Test right)
{
if (left is null && right is null)
return true;
if (left is null)
return false;
if (right is null)
return true;
return left.CompareTo(right) >= 0;
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public bool Equals(ConsoleApp11.Test? other) // IEquatable<ComparableMixin>.Equals
{
if (other is null)
return false;
return this.EqualsImpl(other);
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public int CompareTo(ConsoleApp11.Test? other) // IComparable<ComparableMixin>.CompareTo
{
if (other is null)
return 1;
return this.CompareImpl(other);
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public partial bool EqualsImpl(ConsoleApp11.Test other);
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public partial int CompareImpl(ConsoleApp11.Test other);
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public override bool Equals(object? obj) // C# complains if we implement IEquatable without overriding Object.Equals
{
if (obj is ConsoleApp11.Test other)
{
return this.Equals(other: other);
}
else
{
return false;
}
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public override int GetHashCode() // C# complains if we override Object.Equals without overriding Object.GetHashCode
{
var hash = this.GetHashCodeImpl();
return hash;
}
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Mixin Task", "1.0.52.0")]
public partial int GetHashCodeImpl();
}
}
I propose the following (see Code Sample) approach for replacement of other mixins.
I would restrict replacement to other mixinx because I currently do not see a benefit to replace other types. If non Mixin replacement is required, then I assume Generic parameters would be enough.
- If you use the
ImplementMixinAttributethe referenced type must be annotated with theMixinAttribute - If you use a mixin somewhere in another mixin it must be replaced, otherwise an error is generated describing the problem
- The replacement must "implement" the corresponding mixin
- A Mixin should (warning?) be Abstract, so it can't be instantiated. (I would like to also make it seald, but c# dosen't like that)
I see following drawbacks in this approach
- Only Mixins can be replaced. So types that are from some librarys that propably do not use mixins can't be replaced. The most prominent example of a type that is not a mixin that we want to repalce are numerics.
- Every mixin can only be replaced with one implementation. I can't have two parameters of the same mixin type
that should be replaced with different consuming types.
- This could be mitigated by defining a new type that derive from the desiered mixin and does not have any implementation of its own
This will also not handle other kinds of replacements like constant replacement (string, int, etc...) this could be done by a different mechanic.
Interesting would be which use cases will work and wich will not.
Code sample
//---------------------------------------
// Attribute definition
//---------------------------------------
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct, Inherited = false, AllowMultiple = true)]
sealed class ImplementMixinAttribute : Attribute
{
// This is a positional argument
public ImplementMixinAttribute(Type mixin)
{
}
}
[System.AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = true)]
sealed class MixinParameterAttribute : Attribute
{
// This is a positional argument
public MixinParameterAttribute(Type mixin, Type replacement)
{
}
public Type TargetMixin { get; set; }
}
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct, Inherited = false, AllowMultiple = true)]
sealed class MixinAttribute : Attribute
{
// This is a positional argument
public MixinAttribute()
{
}
}
//---------------------------------------
// Mixins definition
//---------------------------------------
[Mixin]
public abstract class FooMixin
{
private readonly BarMixin bar;
public FooMixin(BarMixin bar)
{
this.bar = bar;
}
public override string ToString()
{
return $"Bar is {bar.Name}";
}
}
[Mixin]
public abstract class BarMixin
{
public string Name { get; }
public BarMixin(string name)
{
this.Name = name;
}
}
//---------------------------------------
// ussage
//---------------------------------------
[ImplementMixin(typeof(FooMixin))]
[MixinParameter(typeof(BarMixin), typeof(BarConsumer))]
// ^^^ If this is missing, a compiletime error is generated 'cause we know that BarMixin is a Mixin
// ^^^ and we do not know what to do with it
public partial class FooConsumer
{
}
[ImplementMixin(typeof(BarMixin))]
public partial class BarConsumer
{
}
public void Test()
{
var foo = new FooConsumer(new BarConsumer("Hello World"));
Console.WriteLine(foo);
// Prints:
// Bar is Hello World
}
//---------------------------------------
// generated Code
//---------------------------------------
public partial class BarConsumer
{
public string Name { get; }
public BarConsumer(string name)
{
this.Name = name;
}
}
public partial class FooConsumer
{
private readonly BarConsumer bar;
public FooConsumer(BarConsumer bar)
{
this.bar = bar;
}
public override string ToString()
{
return $"Bar is {bar.Name}";
}
}