'Spread' parameters in Scala?
Asked Answered
S

2

10

Is there any way to call a Scala function that takes individual parameters, given an array (similar to JavaScript Spreads in ECMAScript 6)?

ys = [10.0, 2.72, -3.14]
f(x, ...ys);

The cleanest syntax would be:

f(1, ys)

but that does not appear to be possible. Even f(1, ys:_*) does not work (and neither does f(ys:_*), as the compiler reports too few parameters - only the first one is filled).

Example:

def f (a:Int, b:Float, c:Float, d:Float): String

val x  = 1
val ys = List(10.0, 2.72, -3.14)  // note: non-Objects
val result = f(x, ys)  // intuitive, but doesn't work

Use Case: injecting test data (from collections) into existing methods that accept individual parameters. As these are test cases, it's quite alright if the #params in ys doesn't match up and that gives either a runtime error or incorrect result.

The question is whether Scala allows a clean syntax for calling a function that takes individual parameters, given a collection of parameters -- not whether it is a good design (although opinions are certainly welcome).

Steward answered 2/3, 2013 at 4:18 Comment(2)
What should happen if ys has less than 3 elements or more?Rupert
Overloading the function f is what comes to mind. That would allow you to call the function in the way you've provided, but it will fail in other situationsAcetylene
P
5

Passing off a list as a tuple is not easy, because the types don't match very well (more on this later). With enough shoehorning and lubricating anything fits though:

"My hack" should {
  "allow a function to be called with Lists" in {

    def function(bar:String, baz:String)= bar+baz


    //turn the function into something that accepts a tuple
    val functionT = function _
    val functionTT = functionT.tupled

    val arguments = List("bar", "baz")

    //Give the compiler a way to try and turn a list into a tuple of the size you need
    implicit def listToTuple(args:List[String]) = args match { 
      case List(a, b) => (a, b)
      case _ => throw IllegalArgumentException("Trying to pass off %s as a pair".format(args))
    }

    //Shove it in and hope for the best at runtime
    val applied = functionTT(arguments)
    applied === "barbaz"
  }
}

You can extend this approach by adding the additional arguments to the list, or by Schönfinkeling the arguments in two different groups. I wouldn't go that way.

From my remarks you might have noticed that I don't like the design that causes this question to pop up. The code I showed is essentially code that is wrapping the function in a facade anyway. Why not do it properly?

Looking at Spray you might see that their complete method accepts a ton of different parameters implicitly. The nifty trick they've used for this they've named the Magnet Pattern. You could do the same thing and introduce implicit conversions to your magnet for different tuples you choose to accept.

Planography answered 2/3, 2013 at 8:36 Comment(5)
+1 for creativity and a fairly clean solution. But any idea how to a) call the function with more arguments before the tuple, as in the example? and b) allow it to work for any number of parameters (not just two or three or how ever many are hard-coded as cases)?Steward
If you need any number of parameters, you need code generation or reflection. With additional parameters you could manipulate the list, or handle the whole thing in the implicit conversion. I'll update the answer to give some hints, but I'll mainly add a better option.Planography
There is a way to do this with the new Macro features in 2.10. I don't have the time to code it up. Though you could do something like apply(f, args ...) and get the result you are looking for (with type safety).Garrik
@Garrik I don't think you could get it to work with type safety, but I must admitt that I have not looked enough at the Macros feature in 2.10. If you have a Sequence and you add objects to it at runtime, how could the Macro ensure that the contents of that sequence have the correct type?Viticulture
@EmilH that's an interesting question indeed... I'll put it on my homework listPlanography
V
3

I think you would have to throw type safety away and resort to using reflection.

scala> object Foo {
 | def doFoo(i:Int, s:String) = println(s * i)
 | }

defined module Foo

scala> val fooFunc = Foo.doFoo _
fooFunc: (Int, String) => Unit = <function2>

scala> val applyMeth = fooFunc.getClass.getMethods()(0)
applyMeth: java.lang.reflect.Method = public final void $anonfun$1.apply(int,java.lang.String)

scala> val i:Integer = 5

i: Integer = 5

scala> val seq = Seq[Object](i,"do the foo! ")
seq: Seq[Object] = List(5, "do the foo! ")

scala> applyMeth.invoke(fooFunc, seq :_*)
do the foo! do the foo! do the foo! do the foo! do the foo! 
res59: Object = null

However, unless you are creating some DSL and realy need this kind of feature, I'd try to find another way. Either overload the methods if it's under your control or wrapp it in some facade kind of class.

EDIT

To answere Rubistro's questions in the comments.

a) How would this technique work to call foo.doFoo? (That is, you have no object -- just an instance of a class that defines doFoo.)

val fooFunc is an instance of a Function2 it is that instance apply function that gets called when invoking applyMeth.invoke(fooFunc, seq :_*).

b) does this method allow parameters to be passed to doFoo before and after the values in seq?

Not directly. To use this you would have to build your sequence before invoking the method. However, since it's a Sequence you could easily prepend/append values to the sequence you are using before invoking the method. Wrapping it up in a builder might be usefull e.g.

class InvokationBuilder(meth:java.lang.reflect.Method, args: List[Object] = Nil) {
    def invoke(instance:Object) = meth.invoke(instance, args.toSeq :_*)
    def append(arg:Object) = new InvokationBuilder(meth, args :+ arg)
    def prepend(arg:Object)= new InvokationBuilder(meth, arg :: args)
}
Viticulture answered 2/3, 2013 at 8:37 Comment(2)
a) How would this technique work to call foo.doFoo? (That is, you have no object -- just an instance of a class that defines doFoo.) b) does this method allow parameters to be passed to doFoo before and after the values in seq?Steward
@Rubistro I updated the answere with some further explanation regarding your questions.Viticulture

© 2022 - 2024 — McMap. All rights reserved.