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?
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
thisparameter. 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.