7
\$\begingroup\$

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

\$\endgroup\$

3 Answers 3

3
\$\begingroup\$

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.

\$\endgroup\$
0
1
\$\begingroup\$

That's an interesting trick, which I'd not come across. I've never really looked into dynamic because I favour static type checking, even at the expense of longer code. The static way of doing this in C# would be the visitor design pattern:

interface IPrimitiveVisitor<T>
{
    T Visit(Number val);
    T Visit(Sequence val);
    T Visit(Die val);
}

abstract class Primitive
{
    public abstract Primitive OpAccess(Primitive right);
    abstract T Accept<T>(IPrimitiveVisitor<T> visitor);
}

class Number : Primitive
{
    override T Accept<T>(IPrimitiveVisitor<T> visitor) => visitor.Visit(this);

    public override Primitive OpAccess(Primitive right) =>
        right.Accept(new OpAccessImpl(this));

    private class OpAccessImpl : IPrimitiveVisitor<Primitive>
    {
        ...
    }
}

As implied by my opening paragraph, the big advantage here is static type checking. If you add a subclass of Primitive then implementing the Accept method forces you to update the IPrimitiveVisitor interface, which forces you to update all of its implementations. And the big disadvantage is that you're going to have create a bunch of implementations of the visitor interface to wrap the methods which were exposed at a "natural" level in your dynamic dispatch implementation.

There's no right answer here: it's a question of personal priorities.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ Thank you. abstract T Accept<T> cannot be private, because Primitive is an abstract class. It also cannot be protected since a derived class cannot access protected members of its base class through an instance of the base class, and right.Accept is trying to do just that. Is it public? \$\endgroup\$ Commented May 8, 2017 at 10:20
  • \$\begingroup\$ @AndrewSavinykh, it's up to you whether you want it to be public or internal. \$\endgroup\$ Commented May 8, 2017 at 10:49
  • \$\begingroup\$ @AndrewSavinykh Can you elaborate more on why abstract T Accept<T>(IPrimitiveVisitor<T> visitor); cannot be protected? \$\endgroup\$
    – Denis
    Commented May 10, 2017 at 23:22
1
\$\begingroup\$

Why Abstract Class?

Primitive is declared as an abstract class that has only one method. The method itself is abstract too. I don't see how is it better than an interface, since no base implementation is provided anyway (and I'm a big not believer of implementation inheritance as a way to create new types).

interface Primitive
{
    Primitive OpAccess(Primitive right);
}

The Evil Triplets

In my humble opinion, from the object-oriented design perspective it's strange to see these public triplets in implementation classes:

public Primitive OpAccess(Number right)
public Primitive OpAccess(Sequence right)
public Primitive OpAccess(Die right)

They are simply redundant as long as Primitive OpAccess(Primitive right) is enforced by base class/interface.

@t3chb0t's answer with switch works as long as you know define all the branches properly. Meaning, you must know all Primitive implementations ahead of time (i.e. at compile time) which is a huge design limitation (imagine you would want to have plugins that provide additional operations in run time, or combine Primitive operations into complex ones). You will also have to not forget update this dispatching method every time you add a new class that implementsPrimitive...

@Peter's implementation of IPrimitiveVisitor relies on same assumption that we know all special classes ahead of time.

Should I Be Constructive?

I think, it would be better to have each child class (Number, Die, and Sequence) define a single method that works against the Primitive parameter. There are no visible benefits in having public triplets in addition to this one. However, it's totally fine to keep the triplets private though and use them for result computation.

For the consuming code Primitive OpAccess(Primitive right) method is 100% sufficient to achieve the result. The consumer will never want to, nor should be responsible for picking up a specific implementation to make a call.

P.S. This answer is written in a review manner since we're on CodeReview SE. If the question is really about "how to make it work" rather than "how to make it object-oriented", it would make more sense to ask in StackOverflow.

\$\endgroup\$
5
  • \$\begingroup\$ I think this is the best solution and if fact I implement such things exactly the way you described it. I'm wondering why did I suggest the switch in the first place ;-) I guess I just assumed there won't be any other types and took the easy way out. \$\endgroup\$
    – t3chb0t
    Commented May 11, 2017 at 6:45
  • \$\begingroup\$ The "assumption that we know all special classes ahead of time" is unavoidable because the common interface doesn't give us anything to work with. \$\endgroup\$ Commented May 11, 2017 at 8:36
  • \$\begingroup\$ @t3chb0t no, your solution uses the switch in the abstract class. I am against this idea. a) interface would work better; b) each particular implementation of Primitive would need to know all the specific other Primitives it can work with. This logic is not shareable. \$\endgroup\$ Commented May 11, 2017 at 14:32
  • 1
    \$\begingroup\$ I know you are against the switch, after all I'm too ;-P that's why I said that your idea is better and that I use it all the time myself but I've got the impression that you might have thought I meant my idea. \$\endgroup\$
    – t3chb0t
    Commented May 11, 2017 at 14:40
  • \$\begingroup\$ @PeterTaylor each concrete Primitive will still somehow dispatch (via switch or via a Strategy map/list as in your proposal. The dispatch will pick a private implementation as opposed to a public one. Also, there's no need in exposing evil triplets as public not in declaring the triplets on abstract class level. The abstraction of Primitive is useful, but incomplete. We need a few more interfaces to have a successful family of abstractions that can work with each other. \$\endgroup\$ Commented May 11, 2017 at 15:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.