IronPython is your friend - Part 2 - More IronPython and delegates….

Part  2 - More IronPython and delegates...



So we have briefly covererd strongly typed delegates and event handlers... But they make the assumption that we know what our arguments are at compile time, what if we dont?



Say for instance you're expression engine has a bag of contextual values being passed around in a dictionary, like this:


Dictionary context = new Dictionary();
context.Add("user", "alex");
context.Add("age", 26);


What if we want to use python to evaluate expressions against that context?... Say something like writing out "my name is alex and my age is 25" - the expression in python is easy enough, we can go:


my name is' + name + ? and my age is ? + str(age)

But how do we marry all these together... lets start exploring... ever noticed that delegates have a DynamicInvoke method with a signature like this?


object DynamicInvoke(params object[] parameters)

Perhaps we can try and use this to our advantage... lets see shall we?

First Attempt

[Test]
[ExpectedException(typeof (ArgumentException), "T must be a concrete delegate, not MulticastDelegate or Delegate")]
public void LooseDelegateAndDynamicInvoke()
{
PythonEngine engine = new PythonEngine();

List parameters = new List(new string[] {"name", "age"});

Delegate func =
engine.CreateMethod("return ?my name is' + name + ? and my age is ? + str(age)", parameters);
string result = (string) func.DynamicInvoke("alex", 26);

Assert.AreEqual("my name is alex and my age is 25", result);
}



Sadly this doesn't work so well, seems the PythonEngine is looking for a concrete delegate... we could try giving it void delegate, but it does type checking on the number of parameters and their type, so we're a little stuck.



At this point I think the best answer is to actually build some code which generates the apropriate delegate type, based on the supplied dictionary, and then passing that onto the python engine, but there is another way... it's a little less elegant, but it's at least amusing:


Round 2

First of let's build a little helper method...


private static string GenerateFunction(string functionName, string[] parameters, string statements)
{
StringBuilder builder = new StringBuilder();

builder.AppendFormat("def {0}(", functionName);

for (int i = 0; i < parameters.length;="">
{
if (i > 0) builder.AppendFormat(", ");
builder.AppendFormat(parameters[i]);
}

builder.AppendFormat("):rn ");

builder.AppendFormat(statements.Replace("rn", "n").Replace("r", "n").Replace("n", "rn "));

return builder.ToString();
}



You can no doubt see where this is going - so given a statement like this:


GenerateFunction("func", new string[] { "user", "age" }, "return user + ' is ' + str(age) + ' years old'")


We'd end up with a string like this:



    def func(user, age):

        return user + ' is ' +
str(age) + ' years old'



And, moving on from there we build a test...
[Test]
public void GeneratingPythonFunction()
{
Dictionary context = new Dictionary();
context.Add("user", "alex");
context.Add("age", 26);

List parameters = new List(context.Keys);
List values = new List(context.Values);

PythonEngine engine = new PythonEngine();

engine.Execute(
GenerateFunction("func", parameters.ToArray(), "return user + ' is ' + str(age) + ' years old'"));

PythonFunction func1 = engine.EvaluateAs("func");

engine.Execute(
GenerateFunction("func", parameters.ToArray(),
"return user + ' is ' + str(age+1) + ' years old next year'"));

PythonFunction func2 = engine.EvaluateAs("func");

object result1 = func1.Call(values.ToArray());
Assert.AreEqual("alex is 26 years old", result1);

object result2 = func2.Call(values.ToArray());
Assert.AreEqual("alex is 27 years old next year", result2);
}

Are there problems with this?... well there's a few, unsurprisingly!

  • PythonFunction's are not delegates.
  • String manipulation is a bit clunky
  • Because these are named functions, you probably want to lock against some object when generating the function, to avoid cross-threading issues...

Back to delegates

Aside from the problems it works alright, but what we really want is a delegate... those PythonFunctions don't give you the flexibility to substitute IronRuby in the future now do they?

So first off, lets declare a delegate suitable for our purposes.

[ThereBeDragons("Only use as a last resort")]
public delegate object UntypedDelegate(params object[] parameters);


Aint she a beauty ;o) - full credit for the ThereBeDragons attribute goes to Ayende, though it's probably not really warranted in this situation - now, let's rework the last test to use a delegate instead, a simple anonymous delegate will do the dirty work:


[Test]
public void UntypedDelegateForPythonFunction()
{
Dictionary context = new Dictionary();
context.Add("user", "alex");
context.Add("age", 26);

List parameters = new List(context.Keys);
List values = new List(context.Values);

PythonEngine engine = new PythonEngine();

engine.Execute(GenerateFunction("func",parameters.ToArray(),"return user + ' is ' + str(age) + ' years old'"));

PythonFunction func1 = engine.EvaluateAs("func");

UntypedDelegate func1Delegate = delegate(object[] param)
{
return func1.Call(param);
};

object result1 = func1Delegate(values.ToArray());
Assert.AreEqual("alex is 26 years old", result1);
}



It's certainly better then passing around PythonFunction instances - though you need to be a little careful... there's a little quirk here, if we were to use:


object result1 = func1Delegate.DynamicInvoke(values.ToArray());


It's going to fail because the wrong number of arguments were supplied (it expects only 1, an array), so our delegate doesn't really behave like it has multiple parameters.. so to dynamically invoke this delegate we'd need to take special care, promoting the arguments into a second array like so:


object result1 = func1Delegate.DynamicInvoke(new object[] { values.ToArray()});


Next Time

When I get bored I'll write a version which doesn't require the clunky generator and post it up...



Next time I'll talk about writing classes which are python-friendly, riveting stuff eh? As you were.
Written on September 30, 2006