Suppose we have classes A and B, functions int fa(A&) and int fb(B&), and a class env with public instance members
struct env
{
A a;
B b;
}
Define "evaluation of fa or fb in the environment provided by env x" to mean f(x.a) when f is fa and f(x.b) when f is fb.
Problem: create a type which is able to hold references to fa or fb and, given an instance of env, is able to evaluate the held function.
#include <string>
#include <iostream>
using namespace std;
// Setup for the problem.
struct A
{
int member;
};
struct B
{
std::string member;
};
struct env
{
A a;
B b;
};
int fa(A& a)
{
cout << "fa called on an A with member = " << a.member << endl;
return a.member;
}
int fb(B& b)
{
cout << "fb called on a B with member = " << b.member << endl;
return int(b.member.length());
}
// First define a generic way to extract the member of type T from
// an instance of env. Note that this is non-intrusive: whatever
// the definition of env is, if it is possible to get an A and a B
// from it, the specialisations of tselect can do so.
template <class T>
struct tselect
{
};
template <>
struct tselect<A>
{
static A& member(env& x) { return x.a; }
};
template <>
struct tselect<B>
{
static B& member(env& x) { return x.b; }
};
// Check tselect performs as intended..
void test0(env& x)
{
cout << "a is " << tselect<A>::member(x).member << endl;
cout << "b is " << tselect<B>::member(x).member << endl;
}
// Interface for a held function.
class FuncHolder
{
public:
virtual ~FuncHolder() {}
virtual int invokeInEnv(env& x) const = 0;
};
// Generic implementation. This scales well:
// if we later want to add C,D,E and F we don't
// need to write any new code here.
template <class T>
class FuncHolderImpl : public FuncHolder
{
public:
typedef int (*funcT)(T&);
funcT func_;
FuncHolderImpl(funcT func) : func_(func) {}
virtual int invokeInEnv(env& x) const
{
return func_(tselect<T>::member(x));
}
};
// Convenience function for creating FuncHolders.
// In the event that fa or fb were overloaded we'd
// have to fall back on explicitly choosing the
// type of the implementation class to construct.
template <class T>
FuncHolder* makeNewHolder(int (*func)(T&))
{
return new FuncHolderImpl<T>(func);
}
// And here it is..
int main()
{
env e;
e.a.member = 1;
e.b.member = "hello";
test0(e);
FuncHolder* fha = makeNewHolder(&fa);
FuncHolder* fhb = makeNewHolder(&fb);
fha->invokeInEnv(e);
fhb->invokeInEnv(e);
return 0;
}
When compiled and run this produces
a is 1
b is hello
fa called on an A with member = 1
fb called on a B with member = hello
Templates keep track of all the type information. They know that fa needs an A and the tselect template knows how to go off and find one.
How about Python, a language in which templates are not needed because there are no static types to worry about? We need to give it just a tiny bit of help because fa can't tell us it needs an A: it doesn't know itself.
class A:
def __init__(self, m):
self.m = m
class B:
def __init__(self, n):
self.n = n
def fa(a):
print 'fa called on a with a.m = ', a.m
def fb(b):
print 'fb called on b with b.n = ', b.n
class env:
def __init__(self, a, b):
self.a = a
self.b = b
class FuncHolder:
def __init__(self, func, argTypeKey):
self.func = func
self.argTypeKey = argTypeKey
def invokeInEnv(self, e):
return self.func(e.__dict__[self.argTypeKey])
fha = FuncHolder(fa, 'a')
fhb = FuncHolder(fb, 'b')
e = env(A(1), B('hello'))
fha.invokeInEnv(e)
fhb.invokeInEnv(e)
No doubt there are ways to make this cleaner and slicker but even as it stands it's usable and fairly concise. We give the FuncHolder a key telling it what type the function it is holding requires as an argument.
So what goes wrong when I try to write this in Scala using type parametrization? Firstly, because of type erasure, a generic FuncHolderImpl[T] can't decide what to do on the basis of the type of T because this is not known (inside the object). The only way it can change what it does is if it has an object of type T or related to it: then it can call an overridden member of some base of T to get it to do the work that varies. So we have to give it some kind of type marker (or at least that's what seemed like a possible solution).
But then we run into the second problem. Nowhere can we write code like this:
if (...)
f(e.a)
else
f(e.b)
One branch or the other won't typecheck. Making a common base doesn't work unless, for each and every function, we write a second version which takes the base but expects the right subclass and casts to it (via pattern matching).
I've tried all sorts of other permutations but for some reason which I can't quite put my finger on, this seems to be hard to do. Suggested solutions in Scala or Java are most welcome.
No comments:
Post a Comment