views:

155

answers:

2

Hi!

I hope the title doesn't sound too subjective; I absolutely do not mean to start a debate on OO in general. I'd merely like to discuss the basic pros and cons for different ways of solving the following sort of problem.

Let's take this minimal example: you want to express an abstract datatype T with functions that may take T as input, output, or both:

  • f1 : Takes a T, returns an int
  • f2 : Takes a string, returns a T
  • f3 : Takes a T and a double, returns another T

I'd like to avoid downcasting and any other dynamic typing. I'd also like to avoid mutation whenever possible.

1: Abstract-class-based attempt

abstract class T {
  abstract int f1();
  // We can't have abstract constructors, so the best we can do, as I see it, is:
  abstract void f2(string s);
  // The convention would be that you'd replace calls to the original f2 by invocation of the nullary constructor of the implementing type, followed by invocation of f2. f2 would need to have side-effects to be of any use.

  // f3 is a problem too:
  abstract T f3(double d);
  // This doesn't express that the return value is of the *same* type as the object whose method is invoked; it just expresses that the return value is *some* T.
}

2: Parametric polymorphism and an auxilliary class

(all implementing classes of TImpl will be singleton classes):

abstract class TImpl<T> {

  abstract int f1(T t);

  abstract T f2(string s);

  abstract T f3(T t, double d);

}

We no longer express that some concrete type actually implements our original spec -- an implementation is simply a type Foo for which we happen to have an instance of TImpl. This doesn't seem to be a problem: If you want a function that works on arbitrary implementations, you just do something like:

// Say we want to return a Bar given an arbitrary implementation of our abstract type
Bar bar<T>(TImpl<T> ti, T t);

At this point, one might as well skip inheritance and singletons altogether and use a

3 First-class function table

class /* or struct, even */ TDict<T> {
  readonly Func<T,int> f1;
  readonly Func<string,T> f2;
  readonly Func<T,double,T> f3;

  TDict( ... ) {
    this.f1 = f1;
    this.f2 = f2;
    this.f3 = f3;
  }
}

Bar bar<T>(TDict<T> td; T t);

Though I don't see much practical difference between #2 and #3.

Example Implementation

class MyT { 
  /* raw data structure goes here; this class needn't have any methods */
}

// It doesn't matter where we put the following; could be a static method of MyT, or some static class collecting dictionaries
static readonly TDict<MyT> MyTDict 
  = new TDict<MyT>(
      (t) => /* body of f1 goes here */ ,
      // f2
      (s) => /* body of f2 goes here */,
      // f3
      (t,d) => /* body of f3 goes here */
    );

Thoughts? #3 is unidiomatic, but it seems rather safe and clean. One question is whether there are any performance concerns with it. I don't usually need dynamic dispatch, and I'd prefer if these function bodies get statically inlined in places where the concrete implementing type is known statically. Is #2 better in that regard?

+9  A: 

Hurm. So, let me see if I understand this: In an OOP language with implicit, initial-parameter ad-hoc polymorphism, you'd like to use parametric polymorphism in order to roll your own quasi-OO system with explicit ad-hoc polymorphism via reified method dictionaries, all for the sake of allowing the type being dispatched on to appear elsewhere in the function's signature than the implicit this parameter. And you want to know if this is a good idea.

From your user name I'm pretty sure you know perfectly well that what you really want is type classes a la Wadler et al. Just because Microsoft signs SPJ's paychecks doesn't mean writing Haskell in C# is a good idea.

Your code is crystal clear to someone who understands the idioms being expressed, but it's sufficiently outside the mainstream of OOP style that you'll want to make sure that the gains in concision and correctness are worth the downsides of using foreign idioms, e.g. confusing other C# programmers. I'd also recommend that performance concerns be handled by profiling some proof-of-concept code, because that style is far from what you'll find in most C# programs.

On the other hand, finding clean ways to express foreign idioms isn't inherently evil--compare uses of algebraic data types to the visitor pattern, for instance--so if you need code with those properties, and that's the best way to express it, leave some notes on the intent behind the code and go for it.

In short: Make sure it's solving your problem, test and profile to make sure it isn't causing other problems, then document and explain the solution for the sake of other programmers.

camccann
Quick answer re the second paragraph: You're making it sound as if I was trying to hide some sinister ulterior motive =) I just didn't want to appear smug by needlessly dropping concepts; I thought my problem makes sense to people who haven't run across typeclasses. This isn't typeclass propaganda; the reason for the posting was that I'm honestly missing the ability to (in Haskell terms) have methods with other signatures than _a -> SomeConstant_ (with _a_ being the parameter of the typeclass).
FunctorSalad
@FunctorSalad: Yes, my apologies if that came off as rude--a good-natured complaint was the intent. But at any rate, borrowing concepts that are common in Haskell is the hot new thing in C# these days, you know, what with generics, LINQ, nullable value types, lambdas, and all that. Surely there's no shame in wanting to borrow a few more! Type classes are so bloody *useful* I wouldn't think they'd *need* propagandizing anyway...
camccann
+1  A: 

You'll never get C# to work like Haskell since it strongly leans away from non-method functions, but here's how I'd try to translate your intent into idiomatic C#. f1 and f3 are trivially handled using regular ad-hoc polymorphism. For example, consider:

interface INumber
{
    int Sign { get; }
    INumber Scale(double amount);
}

We now have an abstract datatype with two of the features you want: Sign is f1 and Scale() is f3. f2, an abstract constructor, is trickier. The typical OOP solution is to use a factory, an abstract class that encapsulates a constructor:

interface INumberFactory
{
    INumber Parse(string text);
}

Parse() is, of course, f2. With these two interfaces together, I think we've got all of your functions covered without resorting to any concrete types. Is that what you were looking for?

Edit: Responding to FunctorSalad's comment:

If you want to have more precise control over the type returned by your functions, the typical (but not that common) OOP solution is the curiously recurring template pattern. Instead of the above, you can define the interfaces like this:

interface INumber<T> where T : INumber<T>
{
    int Sign { get; }
    T Scale(double amount);
}

interface INumberFactory<T> where T : INumber<T>
{
    T Parse(string text);
}

It looks a little strange, but it lets you instantiate types that guarantee that they return their own type, and not necessarily the base type. For example:

class Rational : INumber<Rational>
{
    public int Sign { get { /* ... */ } }
    public Rational Scale(double amount) { /* ... */ }
}

class RationalFactory : INumberFactory<Rational>
{
    Rational Parse(string text);
}

The downside to this, of course, is that your abstract type now requires a type argument, so you can no longer pass around a "raw" INumber or INumberFactory. If you really want to go down that route, C# helps you out a bit with explicit interface implementation. With that, you could define a Rational class that implemented both INumber<Rational> for places where you want the strongly-typed return from Scale() and INumber where you're OK with Scale() returning an INumber. This isn't commonly used, but it's in there. C#'s type system is a lot more interesting than many people realize.

munificent
Funny that you found something (numbers) that matches my method signatures; I had chosen them arbitrarily :) . The problem with doing *Scale* this way is that you're expressing "scaling a number will yield another number", not "scaling a number of type *T* will yield another number of type *T* ". Example consequence: if we have _Rational_ _:_ _INumber_ with a property *MinimalDenominator*, we can't first scale a rational and then take the *MinimalDenominator* of the result (without downcasting).
FunctorSalad
Check the edit. :)
munificent