Magnet pattern with repeated parameters (varargs)

2019-04-07 03:31发布

问题:

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"

?

回答1:

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")
                 ^


回答2:

Edit:

The var-args implicit will never be picked because repeated parameters are not really first class citizens when it comes to types... they are only there when checking for applicability of a method to arguments.

So basically, when you call Foo.bar(1,2,3) it checks if bar is defined with variable arguments, and since it isn't, it isn't applicable to the arguments. And it can't go any further:

If you had called it with a single argument, it would have looked for an implicit conversion from the argument type to the expected type, but since you called with several arguments, there is an arity problem, there is no way it can convert multiple arguments to a single one with an implicit type conversion.


But: there is a solution using auto-tupling.

Foo.bar(1,2,3)

can be understood by the compiler as

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

which means an implicit like this one would work:

implicit def fromInts[T <: Product](t: T) = Values()
// or simply
implicit def fromInts(t: Product) = Values()

The problem with this is that the only way to get the arguments is via t.productIterator, which returns a Iterator[Any] and needs to be cast.

So you would lose type safety; this would compile (and fail at runtime when using it):

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

We can make this fully type-safe using Scala 2.10's implicit macros. The macro would just check that the argument is indeed a TupleX[Int, Int, ...] and only make itself available as an implicit conversion if it passes that check.

To make the example more useful, I changed Values to keep the Int arguments around:

case class Values(xs: Seq[Int])

object Values {
  implicit def fromInt (x : Int ) = Values(Seq(x))
  implicit def fromInts[T<: Product](t: T): Values = macro Macro.fromInts_impl[T]
}

With the macro implementation:

import scala.language.experimental.macros
import scala.reflect.macros.Context
object Macro {
  def fromInts_impl[T <: Product: c.WeakTypeTag](c: Context)(t: c.Expr[T]) = {
    import c.universe._

    val tpe = weakTypeOf[T];

    // abort if not a tuple
    if (!tpe.typeSymbol.fullName.startsWith("scala.Tuple"))
      c.abort(c.enclosingPosition, "Not a tuple!")

    // extract type parameters
    val TypeRef(_,_, tps) = tpe

    // abort it not a tuple of ints
    if (tps.exists(t => !(t =:= typeOf[Int])))
      c.abort(c.enclosingPosition, "Only accept tuples of Int!")

    // now, let's convert that tuple to a List[Any] and add a cast, with splice
    val param = reify(t.splice.productIterator.toList.asInstanceOf[List[Int]])

    // and return Values(param)
    c.Expr(Apply(Select(Ident(newTermName("Values")), newTermName("apply")),
      List(param.tree)))
  }
}

And finally, defining Foo like this:

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

You get type-safe invocation with syntax exactly like repeated parameters:

scala> Foo.bar(1,2,3)
Values(List(1, 2, 3))

scala> Foo.bar("1","2","3")
<console>:13: error: too many arguments for method bar: (values: Values)Unit
              Foo.bar("1","2","3")
                     ^

scala> Foo.bar(1)
Values(List(1))


回答3:

The spec only specifies the type of repeated parameters (varargs) from inside of a function:

The type of such a repeated parameter inside the method is then the sequence type scala.Seq[T ].

It does not cover the type anywhere else.

So I assume that the compiler internally - in a certain phase - cannot match the types.

From this observation (this does not compile => "double definition"):

object Values {
  implicit def fromInt(x: Int) = Values()
  implicit def fromInts(xs: Int*) = Values()
  implicit def fromInts(xs: Seq[Int]) = Values()
}

it seems to be Seq[]. So the next try is to make it different:

object Values {
  implicit def fromInt(x: Int) = Values()
  implicit def fromInts(xs: Int*) = Values()
  implicit def fromInts(xs: Seq[Int])(implicit d: DummyImplicit) = Values()
}

this compiles, but this does not solve the real problem.

The only workaround I found is to convert the varargs into a sequence explicitly:

def varargs(xs: Int*) = xs // return type is Seq[Int]

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

but this of course is not what we want.

Possibly related: An implicit conversion function has only one parameter. But from a logical (or the compiler's temporary) point of view, in case of varargs, it could be multiple as well.

As for the types, this might be of interest



回答4:

Here is a solution which does use overloading (which I would prefer not to)

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

object Foo {  
  def bar(values: Values) { println("ok") }
  def bar[A](values: A*)(implicit asSeq: Seq[A] => Values) { bar(values: Values) }
}

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