3

In the Scala doc, https://docs.scala-lang.org/tour/traits.html, it gives an example of trait.

trait Iterator[A] {
  def hasNext: Boolean
  def next(): A
}

class IntIterator(to: Int) extends Iterator[Int] {
  private var current = 0
  override def hasNext: Boolean = current < to
  override def next(): Int = {
    if (hasNext) {
      val t = current
      current += 1
      t
    } else 0
  }
}


val iterator = new IntIterator(10)
iterator.next()  // returns 0
iterator.next()  // returns 1

We know C# doesn't support traits yet. However, the example above can be easily converted to C# code:

interface Iterator<A> 
{ 
    bool HasNext(); 
    A Next(); 
}

public class IntIterator : Iterator<int> 
{
    int _to;
    int _current = 0;
    public IntIterator(int to) => _to = to;
    public bool HasNext() => _current < _to;
    public int Next() => HasNext() ? _current++ : 0;
}

var itor = new IntIterator(10);
itor.Next()

And C# interface can have default method now. What's C# missing comparing with traits?

Or there should be a better Scala example to show the power of trait?

ca9163d9
  • 27,283
  • 64
  • 210
  • 413

2 Answers2

3

Comparing with Scala traits in particular (traits in different languages can be quite different), C# is missing:

  1. Initialization code: you can have

    trait A {
      println("A's constructor")
    }
    

    and this code will be executed (in proper order) in the constructor of any class inheriting from A. Or more simply

    trait A {
      val x = 10
    }
    
  2. Trait linearization (at least in the specific details)

  3. Different base (in C#)/super (in Scala) resolution, which means the Stackable Trait pattern won't work with C# interfaces.

  4. (coming in Scala 3) Constructor parameters

  5. (removed in Scala 3) Early definitions

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
1

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

louthster
  • 1,560
  • 9
  • 20