I was trying to do some stuff last night around accepting and calling a generic function (i.e. the type is known at the call site, but potentially varies across call sites, so the definition should be generic across arities).
For example, suppose I have a function f: (A, B, C, ...) => Z
. (There are actually many such f
s, which I do not know in advance, and so I cannot fix the types nor count of A, B, C, ..., Z
.)
I'm trying to achieve the following.
How do I call f
generically with an instance of (A, B, C, ...)
? If the signature of f
were known in advance, then I could do something involving Function.tupled f
or equivalent.
How do I define another function or method (for example, some object
's apply
method) with the same signature as f
? That is to say, how do I define a g
for which g(a, b, c, ...)
type checks if and only if f(a, b, c, ...)
type checks? I was looking into Shapeless's HList
for this. From what I can tell so far, HList
at least solves the "representing an arbitrary arity args list" issue, and also, Shapeless would solve the conversion to and from tuple issue. However, I'm still not sure I understand how this would fit in with a function of generic arity, if at all.
How do I define another function or method with a related type signature to f
? The biggest example that comes to mind now is some h: (A, B, C, ...) => SomeErrorThing[Z] \/ Z
.
I remember watching a conference presentation on Shapeless some time ago. While the presenter did not explicitly demonstrate these things, what they did demonstrate (various techniques around abstracting/genericizing tuples vs HList
s) would lead me to believe that similar things as the above are possible with the same tools.
Thanks in advance!
Yes, Shapeless can absolutely help you here. Suppose for example that we want to take a function of arbitrary arity and turn it into a function of the same arity but with the return type wrapped in Option
(I think this will hit all three points of your question).
To keep things simple I'll just say the Option
is always Some
. This takes a pretty dense four lines:
import shapeless._, ops.function._
def wrap[F, I <: HList, O](f: F)(implicit
ftp: FnToProduct.Aux[F, I => O],
ffp: FnFromProduct[I => Option[O]]
): ffp.Out = ffp(i => Some(ftp(f)(i)))
We can show that it works:
scala> wrap((i: Int) => i + 1)
res0: Int => Option[Int] = <function1>
scala> wrap((i: Int, s: String, t: String) => (s * i) + t)
res1: (Int, String, String) => Option[String] = <function3>
scala> res1(3, "foo", "bar")
res2: Option[String] = Some(foofoofoobar)
Note the appropriate static return types. Now for how it works:
The FnToProduct
type class provides evidence that some type F
is a FunctionN
(for some N
) that can be converted into a function from some HList
to the original output type. The HList
function (a Function1
, to be precise) is the Out
type member of the instance, or the second type parameter of the FnToProduct.Aux
helper.
FnFromProduct
does the reverse—it's evidence that some F
is a Function1
from an HList
to some output type that can be converted into a function of some arity to that output type.
In our wrap
method, we use FnToProduct.Aux
to constrain the Out
of the FnToProduct
instance for F
in such a way that we can refer to the HList
parameter list and the O
result type in the type of our FnFromProduct
instance. The implementation is then pretty straightforward—we just apply the instances in the appropriate places.
This may all seem very complicated, but once you've worked with this kind of generic programming in Scala for a while it becomes more or less intuitive, and we'd of course be happy to answer more specific questions about your use case.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With