Designing classes for non-standard arithmetics This question is inspired by http://anydice.com - a dice probability calculator web application.

Anydice language has three run-time types: a number, a sequence and a die. There is also a number of unary and binary operations defined on these types. Each binary operation can take any type pair (out of the 3) as arguments, there are separate definitions of what each operation does for each possible pair of types. Some operations are not commutative.

For simplicity let's consider a single binary operation, which I'll call OpAccess, or @. My goal is to design a set of classes that represent the run-time values, so that if the access operation is executed on two of such values the correct operation implementation (depending on argument types) is called.

The main problem is that at compile time it is not known yet what run-time type a value has, yet, it's required to dispatch the correct operation implementation during the run-time.

Let's start with defining the base class for our values:

abstract class Primitive 
{ 
  public abstract Primitive OpAccess(Primitive right);
}


Our value will use itself as the left argument in the OpAccessoperation and accept the right argument as the parameter.

Having this base we can design our value classes as follows:

class Number : Primitive
{
  public override Primitive OpAccess(Primitive right)
  {
    return OpAccess((dynamic)right);
  }
  public Primitive OpAccess(Number right)
  {
    Console.WriteLine("Number @ Number");
    return null;
  }
  public Primitive OpAccess(Sequence right)
  {
    Console.WriteLine("Number @ Sequence");
    return null;
  }
  public Primitive OpAccess(Die right)
  {
    Console.WriteLine("Number @ Die");
    return null;
  }
}
class Sequence : Primitive
{
  public override Primitive OpAccess(Primitive right)
  {
    return OpAccess((dynamic)right);
  }
  public Primitive OpAccess(Number right)
  {
    Console.WriteLine("Sequence @ Number");
    return null;
  }
  public Primitive OpAccess(Sequence right)
  {
    Console.WriteLine("Sequence @ Sequence");
    return null;
  }
  public Primitive OpAccess(Die right)
  {
    Console.WriteLine("Sequence @ Die");
    return null;
  }
}
class Die : Primitive
{
  public override Primitive OpAccess(Primitive right)
  {
    return OpAccess((dynamic)right);
  }
  public Primitive OpAccess(Number right)
  {
    Console.WriteLine("Die @ Number");
    return null;
  }
  public Primitive OpAccess(Sequence right)
  {
    Console.WriteLine("Die @ Sequence");
    return null;
  }
  public Primitive OpAccess(Die right)
  {
    Console.WriteLine("Die @ Die");
    return null;
  }
}


Now if we run something like:

Primitive a = new Sequence();
Primitive b = new Die();
a.OpAccess(b);
b.OpAccess(a);


We will get:

Sequence @ Die
Die @ Sequence


Since all operation will be defined differently the number of resulting methods is not an issue, it's really this many different ways to do an operation.

What worries me more is that I had to duplicate

public override Primitive OpAccess(Primitive left)
{
  return OpAccess((dynamic)left);
}


in each class and do not have an easy way around it. Remember, it's just one operation we are considering here, there will be more than a dozen of those in reality. Also, I have no idea if the use of dynamic becomes a performance problem. (Manual dispatch with switch/case should be faster).

But maybe I'm attacking this problem from the completely wrong angle? What do you think?

Note: this is in the context of writing a parser and an interpreter for Anydice language.

Update

Additional research prompted by Peter Taylor's answer uncovered the following article, which is most illuminating: Double Dispatch is a Code Smell
You might want to define the base type like this

abstract class Primitive
{
    public Primitive OpAccess(Primitive right)
    {
        switch (right)
        {
            case Number number: return OpAccess(number);
            // ... other types
            default: throw new ArgumentOutOfRangeException();
        }
    }
    protected abstract Primitive OpAccess(Number right);
    // ... other OpAccess
}


where there is only one public method and the new C# 7 switch takes care of the dispatch and derived classes need to implement only the concrete protected overloads:

class Sequence : Primitive
{
    protected override Primitive OpAccess(Number right)
    {
        Console.WriteLine("Sequence @ Number");
        return null;
    }
    // ... other OpAccess
}


dynamic is no longer necessary.