Reputation: 9361
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
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 A
s 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 B
s. 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