15

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 fs, 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.

  1. 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.

  2. 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.

  3. 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 HLists) would lead me to believe that similar things as the above are possible with the same tools.

Thanks in advance!

Ming
  • 1,613
  • 12
  • 27
  • I'm convinced Shapeless will help you in doing that. I unfortunately don't have the time right now to fiddle around with this. – gzm0 Jul 17 '15 at 20:24
  • My impression is also that along the lines of, this resembles what I've seen Shapeless do, but I'm not quite sure how. – Ming Jul 17 '15 at 20:28

1 Answers1

13

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.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Thanks for clarifying the individual pieces that go into creating the end product! – Ming Jul 18 '15 at 00:44
  • A bit of a follow-up, out of curiosity: What constrains the relationship between `F` and `I`? In other words, what prevents the use of `wrap` with incompatible types, say for example, `(A, B)` and `(A, B, C) => D`? Is it the fact that I can not get instances of `FnToProduct` / `FnFromProduct` for types which don't line up? What prevents me from creating such a type, accidentally or otherwise? In other words, what differentiates "correct" (or conventionally acceptable) instances of `FnToProduct` / `FnFromProduct`? – Ming Jul 18 '15 at 00:51
  • 1
    Shapeless will only provide valid instances of these type classes. I suppose you could provide your own invalid ones, but that's the case with any (non-sealed) type class. Why these type classes aren't sealed is a good question—it may just be because that would be inconvenient given the way Shapeless's code generation works. In any case you typically don't worry about accidentally creating invalid instances of type classes like this (and shouldn't be defining your own instances at all). – Travis Brown Jul 18 '15 at 01:35
  • For many shapeless typeclasses it's convenient to be able to define one's own instances, e.g. if one had one's own "function-like" class that one wanted to be able to use with this method. – lmm Jul 20 '15 at 09:12
  • @lmm I'd probably define my own type class in that situation—you'd have to duplicate a lot of Shapeless's arity-level boilerplate anyway. – Travis Brown Jul 20 '15 at 12:49