Tuesday, October 18, 2011

Calling a set of n-ary functions ... in a weird way

Here's another brain teaser. It just happened that I needed a weird construct for my tiny graph-rewriting framework.

What I'm looking for is something similar to this:
val r1 = Rule(
   left = Graph(Node("a")),
   right = Graph(Node("a") ~> Node("b")),
   bind = ( (Node("a"), Node("b")),
      { (a:Node, b:Node) => b.foo = a.foo } ) )
To put it in words, the left-hand side of rule r1 looks for an arbitrary node (here "a" is just an symbolic id within the scope of the rule). If the graph contains such a node (or: it is not empty), the right-hand side extends the graph by a new node "b". Finally, the binding step assigns free variables of "b". The tricky part here is the signature of the "bind" argument. The first argument is a n-ary tuple identifying a set of nodes which are in turn passed to the second argument (a function).

Now lets put on the glasses of "abstraction and genius" and have a second look. Actually what we really want is to call a n-ary function given a n-ary tuple of arguments. Of course these arguments can be transformed prior to passing them to the function, but our glasses hide these details from us ...

Okay then lets get into the specifics here. This is what our magical method which calls a set of n-ary functions should look like:
val f = { (a:Int, b:Int) => println(a+b) }
val e = { (a:Int, b:Int, c:Int) => println(a+b+c) }

call( ( f, (1, 2)), (e, (1, 2, 3) ) ) // prints 3, 6
The function 'call' simply takes an arbitraty set of 2-tuples (function, args). But how the heck can we express this in Scala?

Well, it turns out Scala provides the means implement this without much hassle:
  • Tuples: Yes! Tuples as first-class citizen in Scala e.g. ("a", "b", "c")
  • Kleene closure T*: Not so sure the term really fits, basically it's the n-ary cartesian product or var-args of type T
  • Implicit conversions: If a type conversion A -> B is required to e.g. match a function signature, Scala tries to call a user-defined f : A -> B (known from C++)
It still requires some more backgrounds to put this together. Tuples are represented by TupleN types where N denotes the number of arguments the tuple holds. Product is the base trait of TupleN.
Accordingly functions are represented by FunctionsN types. Note that the function tupled() turns Function2[A,A,R] into Function1[Tuple2[A,A],R] where Tuple2 <: Product.

What we end up with is:
def call(p: Tuple2[Function1[Product,Unit], Product]*) = 
{ p.foreach(f => f._1(f._2)) }

implicit def func2tupled2[T](f:(T,T) => Unit) = 
{ f.tupled.asInstanceOf[Function1[Product,Unit]] }
implicit def func2tupled3[T](f:(T,T,T) => Unit) =
{ f.tupled.asInstanceOf[Function1[Product,Unit]] }
      
val f = { (a:Int, b:Int) => println(a+b) }
val e = { (a:Int, b:Int, c:Int) => println(a+b+c) }

call( ( f, (1, 2) ), (e, (1, 2, 3)) ) // prints 3, 6
I hope your brain screams "WOOT!". Read it from the bottom up. The function 'call' takes e,f which signatures are converted from Function2[Int,Int,Unit] and Function3[Int,Int,Int,Unit] to Function1[Product,Unit] by func2tupledN. At last, 'call' invokes each function f and passes a tuple holding the arguments.

No comments:

Post a Comment