Tuesday, May 13, 2008

Is it just me?

I can do what I want in Python and I can do it in C++ but I'm having trouble doing it in Scala. (Replace A and B with HandInfo etc. from the previous post and you get my use-case.)

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: