Tuesday, February 10, 2009

Named Parameters and Builders in Scala

Tonight was the monthly BASE meeting. Jorge did a great talk on Scala actors. Before the talk, Dick was talking about looking for a good builder implementation in Scala. This seemed to be an area where Scala did not offer much over Java. Even using some of Scala's more sophisticated syntactic sugar, the resulting builder is not satisfactory. I asked Dick that if Scala had named parameter, would that be good enough?

So I did some playing around with simulating named parameters in Scala. Let's say we have a class like this

class Beast (val x:Double, val y:Double, val z:Double){
// other stuff in here
}

Now suppose that x and y are required, but z can have a default value of 0. My attempt at simulating named parameters involved creating some classes corresponding to the variables.

class X(val x:Double)
class Y(val y:Double)
class Z(val z:Double)
object X{
def ->(d:Double)= new X(d)
}
object Y{
def ->(d:Double)= new Y(d)
}
object Z{
def ->(d:Double) = new Z(d)
}

Do you see where this is going? Next we need a companion object for Beast:

object Beast{
def apply(xyz:Tuple3[X,Y,Z]) = new Beast(xyz._1.x, xyz._2.y, xyz._3.z)
}

Now we can do something like this:

val c = Beast(X->3, Y->4, Z->5)

So X->3 calls the -> method on the X object. This returns a new instance of the X class with value 3. The same thing happens for Y->4 and Z->5. Putting all thee inside the parentheses gives us a Tuple3. This is passed in to the apply method on the Beast object which in turn creates a new instance of Beast with the given values. So far so good?

Now we just need a way to make z optional and give it a default value if it is not supplied. To do this, we need some Evil.

object Evil{
implicit def missingZ(xy:Tuple2[X,Y]):Tuple3[X,Y,Z]=(xy._1,xy._2, new Z(0))
}

Now it is possible to get the optional value behavior:

object BeastMaster{
import Evil._
def main(args:Array[String]){
val b = Beast(X->1, Y->2)
println(b)
val c = Beast(X->3, Y->4, Z->5)
println(c)
}
}
The implicit def missingZ is used to "invisibly" convert a Tuple2[X,Y] into a Tuple3[X,Y,Z].

Unfortunately this is where the coolness ends. You can't switch around the order of the variables, i.e. Beast(Y->2, X->1) or even Beast(Z->5, X->3, Y->4). You can't just add more implicit defs either. Like if you try:

object Evil{
implicit def missingZ(xy:Tuple2[X,Y]):Tuple3[X,Y,Z]=(xy._1,xy._2, new Z(0))
implicit def missingZ2(yx:Tuple2[Y,X]):Tuple3[X,Y,Z] = (yx._2, yx._1, new Z(0))
}

This will cause Beast(X->1,Y->2) to fail to compile. You will get the following error:

error: wrong number of arguments for method apply: ((builder.X, builder.Y, builder.Z))builder.Beast in object Beast
val b = Beast(X->3, Y->5)

This is not the most obvious error. The problem (I think) is that the compiler can't determine which implicit def to use. The culprit is type erasure. There is no way to tell the difference between a Tuple2[X,Y] and Tuple2[Y,X] at runtime. At compile there is, so you would think that it would be possible to figure out which implicit to use... Or perhaps it is possible to merge the two implicit together by using an implicit manifest?

6 comments:

Jorge Ortiz said...

BTW, it looks likely that Scala will get named and optional parameters for 2.8.0. See: http://lampsvn.epfl.ch/svn-repos/scala/lamp-sip/named-args/sip.xhtml

Ricky Clarkson said...

It's planned that Scala will have named parameters in 2.8. You can find a sketchy implementation in one of the branches in svn, I believe.

Meanwhile:

trait BeastBits {
. . val x, y, z: Int
}

case class Beast(x: Int, y: Int, z: Int)

object Beast { def apply(bits: BeastBits): Beast = Beast(bits.x, bits.y, bits.z) }

object Main { def main(args: Array[String]) {
. . println(Beast(new BeastBits { val x = 5; val y = 6; val z = 7 })

prints: Beast(5,6,7)

Ricky Clarkson said...

Or a little better for this specific case:

scala> trait Beast { val x, y, z: Int; override def toString="I am a beast measuring "+x+" by "+y+" by "+z+", so watch it." }
defined trait Beast

scala> new Beast { val x = 4; val y = 5; val z = 6 }
res0: java.lang.Object with Beast = I am a beast measuring 4 by 5 by 6, so watch it.

James Iry said...

I think what you are seeing is a bug. Whatever the problem is, it's not type erasure. Implicits are dispatched statically before type erasure occurs

scala> implicit val x = List(1,2,3)
x: List[Int] = List(1, 2, 3)

scala> implicit val y = List('a', 'b', 'c')
y: List[Char] = List(a, b, c)

scala> def foo(implicit z : List[Int]) = z
foo: (implicit List[Int])List[Int]

scala> foo
res0: List[Int] = List(1, 2, 3)

If type erasure were the problem then foo wouldn't work.

Daniel Green said...

Ricky Clarkson: You mentioned there was a branch that supported named parameters? Which one are you referring to?

Ricky Clarkson said...

Daniel,

http://lampsvn.epfl.ch/trac/scala/browser/scala/branches/named-args