Yevgeniy Brikman
Yevgeniy Brikman

Reputation: 9361

Is there any way in Scala to have a dynamic number of named/typed parameters?

I'm working on some Scala code where the user will have a variable number of named variables, all of the same type A:

val a1: A = getA1()
val a2: A = getA2()
val a3: A = getA3()
// ...

I would like to define a class called Processor that takes these arguments as parameters, transforms each parameter into a new type B, and exposes a method called process that gives the user access to these new transformed variables. The best API I've come up with so far is:

class Processor(a1: A, a2: A, a3: A) {
  private val b1 = toB(a1)
  private val b2 = toB(a2)
  private val b3 = toB(a3)

  def process(f: (B, B, B) => C): C = {
    f(b1, b2, b3)
  }
}

// Usage:

val a1: A = getA1()
val a2: A = getA2()
val a3: A = getA3()

val p = new Processor(a1, a2, a3)
p.process((b1, b2, b3) => doSomething(b1, b2, b3))

The problem with this API is that it only works for three parameters, whereas the user could have anywhere from 1 to 20 parameters. I could define Processor1, Processor2, Processor3, etc classes that take each take a different number of parameters, but this would be a repetitive and ugly API. Another option is to take A* as a parameter in both the Processor constructor and process method:

class Processor(a: A*) {
  private val b = a.map(toB)

  def process(f: (Seq[B]) => C): C = {
    f(b)
  }
} 

// Usage:

val a1: A = getA1()
val a2: A = getA2()
val a3: A = getA3()

val p = new Processor(Vector(a1, a2, a3))
p.process((b) => doSomething(b(0), b(1), b(2)))  

This API works for any number of parameters, but it no longer has type safety or named parameters, so the code could blow up at runtime if the user accidentally looked up an index that did not exist (e.g. b(3)). In other words, the compiler no longer verifies that the number of parameters passed to the Processor constructor matches the number used in the process method.

Is there any way in Scala to have a dynamic number of named/typed parameters? Perhaps using a macro?

Upvotes: 2

Views: 341

Answers (1)

Alex Archambault
Alex Archambault

Reputation: 985

With the help of shapeless, you can define Processor like:

class Processor[T](t: T)
 (implicit
   mapper: ops.tuple.Mapper.Aux[T, ToB.type]
 ) {
  private val bs = mapper(t)

  def process[C](f: mapper.Out => C): C = {
    f(bs)
  }
}

(shapeless._ was previously imported.)

It can be used like

val p4 = new Processor(a1, a2, a3, a4)
p4.process{case (b1, b2, b3, b4) => ??? }

Here, when Processor is instantiated with a tuple of As with type T, it requires an implicit shapeless.ops.tuple.Mapper, that allows to map toB (wrapped in ToB, see below) on this tuple. The type member Out of this implicit is the "output" tuple, made of Bs. It is used in the signature of process.

ToB is defined like

object ToB extends Poly1 {
  implicit def apply = at[A](a => toB(a))
}

and allows to map toB on a tuple with ops.tuple.Mapper.

Upvotes: 3

Related Questions