Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Magnet pattern with repeated parameters (varargs)

Is it possible to use the magnet pattern with varargs:

object Values {
  implicit def fromInt (x : Int ) = Values()
  implicit def fromInts(xs: Int*) = Values()
}
case class Values()

object Foo {  
  def bar(values: Values) {}
}

Foo.bar(0)
Foo.bar(1,2,3) // ! "error: too many arguments for method bar: (values: Values)Unit"

?

like image 480
0__ Avatar asked Jun 02 '13 02:06

0__


People also ask

What is Varargs method?

Variable Arguments (Varargs) in Java is a method that takes a variable number of arguments. Variable Arguments in Java simplifies the creation of methods that need to take a variable number of arguments.

What is magnet pattern?

What Is Magnet Pattern? Magnet pattern is an advanced type-based pattern to deal with problems related to method overloading in Scala. A magnet pattern is a special case of the type-class pattern in which the target methods are invoked based on the types of parameters.

How Varargs works in Java?

In Java, an argument of a method can accept arbitrary number of values. This argument that can accept variable number of values is called varargs. In order to define vararg, ... (three dots) is used in the formal parameter of a method.


1 Answers

As already mentioned by gourlaysama, turning the varargs into a single Product will do the trick, syntactically speaking:

implicit def fromInts(t: Product) = Values()

This allows the following call to compile fine:

Foo.bar(1,2,3)

This is because the compiler autmatically lifts the 3 arguments into a Tuple3[Int, Int, Int]. This will work with any number of arguments up to an arity of 22. Now the problem is how to make it type safe. As it is Product.productIterator is the only way to get back our argument list inside the method body, but it returns an Iterator[Any]. We don't have any guarantee that the method will be called only with Ints. This should come as no surprise as we actually never even mentioned in the signature that we wanted only Ints.

OK, so the key difference between an unconstrained Product and a vararg list is that in the latter case each element is of the same type. We can encode this using a type class:

abstract sealed class IsVarArgsOf[P, E]
object IsVarArgsOf {
  implicit def Tuple2[E]: IsVarArgsOf[(E, E), E] = null
  implicit def Tuple3[E]: IsVarArgsOf[(E, E, E), E] = null
  implicit def Tuple4[E]: IsVarArgsOf[(E, E, E, E), E] = null
  implicit def Tuple5[E]: IsVarArgsOf[(E, E, E, E, E), E] = null
  implicit def Tuple6[E]: IsVarArgsOf[(E, E, E, E, E), E] = null
  // ... and so on... yes this is verbose, but can be done once for all
}

implicit class RichProduct[P]( val product: P )  {
  def args[E]( implicit evidence: P IsVarArgsOf E ): Iterator[E] = {
    // NOTE: by construction, those casts are safe and cannot fail
    product.asInstanceOf[Product].productIterator.asInstanceOf[Iterator[E]]
  }
}

case class Values( xs: Seq[Int] )
object Values {
  implicit def fromInt( x : Int ) = Values( Seq( x ) )
  implicit def fromInts[P]( xs: P )( implicit evidence: P IsVarArgsOf Int ) = Values( xs.args.toSeq )
}


object Foo {  
  def bar(values: Values) {}
}

Foo.bar(0)
Foo.bar(1,2,3)

We have changed the method signature form

implicit def fromInts(t: Product)

to:

implicit def fromInts[P]( xs: P )( implicit evidence: P IsVarArgsOf Int )

Inside the method body, we use the new methodd args to get our arg list back.

Note that if we attempt to call bar with a a tuple that is not a tuple of Ints, we will get a compile error, which gets us our type safety back.


UPDATE: As pointed by 0__, my above solution does not play well with numeric widening. In other words, the following does not compile, although it would work if bar was simply taking 3 Int parameters:

Foo.bar(1:Short,2:Short,3:Short)
Foo.bar(1:Short,2:Byte,3:Int)

To fix this, all we need to do is to modify IsVarArgsOf so that all the implicits allow the tuple elemts to be convertible to a common type, rather than all be of the same type:

abstract sealed class IsVarArgsOf[P, E]
object IsVarArgsOf {
  implicit def Tuple2[E,X1<%E,X2<%E]: IsVarArgsOf[(X1, X2), E] = null
  implicit def Tuple3[E,X1<%E,X2<%E,X3<%E]: IsVarArgsOf[(X1, X2, X3), E] = null
  implicit def Tuple4[E,X1<%E,X2<%E,X3<%E,X4<%E]: IsVarArgsOf[(X1, X2, X3, X4), E] = null
  // ... and so on ...
}

OK, actually I lied, we're not done yet. Because we are now accepting different types of elements (so long as they are convertible to a common type, we cannot just cast them to the expected type (this would lead to a runtime cast error) but instead we have to apply the implicit conversions. We can rework it like this:

abstract sealed class IsVarArgsOf[P, E] {
  def args( p: P ): Iterator[E]
}; object IsVarArgsOf {
  implicit def Tuple2[E,X1<%E,X2<%E] = new IsVarArgsOf[(X1, X2), E]{
    def args( p: (X1, X2) ) = Iterator[E](p._1, p._2)
  }
  implicit def Tuple3[E,X1<%E,X2<%E,X3<%E] = new IsVarArgsOf[(X1, X2, X3), E]{
    def args( p: (X1, X2, X3) ) = Iterator[E](p._1, p._2, p._3)
  }
  implicit def Tuple4[E,X1<%E,X2<%E,X3<%E,X4<%E] = new IsVarArgsOf[(X1, X2, X3, X4), E]{
    def args( p: (X1, X2, X3, X4) ) = Iterator[E](p._1, p._2, p._3, p._4)
  }
  // ... and so on ...
}
implicit class RichProduct[P]( val product: P ) {
  def args[E]( implicit isVarArg: P IsVarArgsOf E ): Iterator[E] = {
    isVarArg.args( product )
  }
}

This fixes the problem with numeric widening, and we still get a compile when mixing unrelated types:

scala> Foo.bar(1,2,"three")
<console>:22: error: too many arguments for method bar: (values: Values)Unit
          Foo.bar(1,2,"three")
                 ^
like image 156
Régis Jean-Gilles Avatar answered Oct 02 '22 15:10

Régis Jean-Gilles