Technically, you can do something like traits in C#. Traits is ad-hoc polymorphism. i.e. you can gift types with additional traits ad-hoc, or after-the-fact. This is useful when you're given a type (like IEnumerable<T>
from the BCL) and you want it to have additional properties, like being a monad (but you don't own the type, so you can't change its interface list).
So, for your iterator example, you could do this:
public interface Iterator<MA, A>
{
bool HasNext(MA iter);
A Next(MA iter);
}
MA
is the container type, A
is what it contains.
Next, we'll do an implementation of that for IEnumerator<A>
.
public struct IteratorEnum<A> : Iterator<IEnumerator<A>, A>
{
public bool HasNext(IEnumerator<A> iter) =>
iter.MoveNext();
public A Next(IEnumerator<A> iter) =>
iter.Current;
}
Ignore the fact that MoveNext
/ HasNext
, and Next
/ Current
don't mean the same thing, this is just for convenience.
Note how the type is a struct
. This is because structs can never be null
. And so, default(MY_STRUCT)
will always have a non-null value.
Now, we'll create a generic implementation that uses Iterator
and knows nothing about the underlying type:
public static IEnumerable<A> IterAnything<IterA, MA, A>(MA iter)
where IterA : struct, Iterator<MA, A>
{
while(default(IterA).HasNext(iter))
{
yield return default(IterA).Next(iter);
}
}
Note the use of default(IterA)
, this is because we've constrained IterA
to be struct, Iterator<MA, A>
- that means it can't be null
and must implement Iterator<MA, A>
.
Then we can invoke it with any Iterator
we like:
var items = (new[] { 1, 2, 3, 4, 5 }).AsEnumerable().GetEnumerator();
var newitems = IterAnything<IteratorEnum<int>, IEnumerator<int>, int>(items);
A simpler example of this approach is to Num<A>
and Eq<A>
. There is no base INumeric
type in C#, but we want to write our numeric processing functions once. The types are all buried in the BCL, so we can't go and change their interfaces:
public interface Num<A>
{
A Add(A lhs, A rhs);
A Subtract(A lhs, A rhs);
A Multiply(A lhs, A rhs);
A Divide(A lhs, A rhs);
A FromInt(int value);
A One { get; }
A Zero { get; }
}
public interface Eq<A>
{
bool IsEqualTo(A lhs, A rhs);
// using C#8 default interface methods
bool IsNoEqualTo(A lhs, A rhs) => !IsEqualTo(lhs, rhs);
}
Then we can implement both for int
and long
:
public struct NumInt : Num<int>, Eq<int>
{
public int Add(int lhs, int rhs) => lhs + rhs;
public int Subtract(int lhs, int rhs) => lhs - rhs;
public int Multiply(int lhs, int rhs) => lhs * rhs;
public int Divide(int lhs, int rhs) => lhs / rhs;
public int FromInt(int value) => value;
public bool IsEqualTo(int lhs, int rhs) => lhs == rhs;
public int One => 1;
public int Zero => 0;
}
public struct NumLong : Num<long>, Eq<long>
{
public long Add(long lhs, long rhs) => lhs + rhs;
public long Subtract(long lhs, long rhs) => lhs - rhs;
public long Multiply(long lhs, long rhs) => lhs * rhs;
public long Divide(long lhs, long rhs) => lhs / rhs;
public long FromInt(int value) => (long)value;
public bool IsEqualTo(long lhs, long rhs) => lhs == rhs;
public long One => 1;
public long Zero => 0;
}
Then we can create some methods for working with numbers:
public static bool IsEqualTo0<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
default(NumA).IsEqualTo(n, default(NumA).Zero);
public static bool IsEqualTo1<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
default(NumA).IsEqualTo(n, default(NumA).One);
public static A Subtract1<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
default(NumA).Subtract(n, default(NumA).One);
public static A Subtract2<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
Subtract1<NumA, A>(Subtract1<NumA, A>(n));
public static A Fibonacci<NumA, A>(A n) where NumA : struct, Num<A>, Eq<A> =>
IsEqualTo0<NumA, A>(n) || IsEqualTo1<NumA, A>(n)
? n
: Fibonacci<NumA, A>(default(NumA).Add(
Subtract1<NumA, A>(n),
Subtract2<NumA, A>(n)));
And then finally, invoke:
Fibonacci<NumInt, int>(100);
Fibonacci<NumLong, long>(100L);
And so, that shows adding traits to a type ad-hoc. It's not pretty, and the C# team are looking to address this with the Shapes/Concepts/.. proposal. But, it is possible if you need it. I use it every now and then, and have a ton of example traits and implementations in my language-ext library (called Type-classes and Class-instances).
If the .NET team had made an INumeric
interface and made all the numeric types derive from it, then usage of it would cause boxing. The approach demonstrated above doesn't cause any boxing at all. Also, all the default(NumA)
calls get optimised out in a release build, so it's just as fast as calling the functions directly.
The limitations are that C# doesn't have higher kinds, which leads to problems when trying to implement something like Monad
or Functor
, but types like Monoid
are easy