Reputation: 113
I am wondering if there is a way of patching over an apparent inconsistency in Scala's handling of Function1 and Function2..N.
For a Function1
, say Int => String
, the parameter list (Int) is not identical to Int (even if the two are isomorphic), yet the compiler will infer the input as bare Int (see the code below).
For Function2..N
, say
val f: (String, Int) => String = ???
the compiler will not infer any type for the input parameter list. In particular, there is no ParameterList[String, Int] even though it is isomorphic to tuples of (String, Int) and any other wrapper you fancy putting around Strings and Ints.
My first question is, is there is a reason it is like this (or is this something that exists on a list of scala todos)? i.e. why can Function1 be deconstructed into input and output type but not Function2 and does anybody want to fix that?
Are there any work arounds. Specifically, in the code below, is there a way to make invoke2 work?
package net.jtownson.swakka
import org.scalatest.FlatSpec
import org.scalatest.Matchers._
import shapeless.ops.function._
import shapeless.{HList, HNil, _}
class TypeclassOfFunctionTypeSpec extends FlatSpec {
// Here, we know the return type of F is constrained to be O
// (because that's how the shapeless FnToProduct typeclass works)
def invoke1[F, I <: HList, O](f: F, i: I)
(implicit ftp: FnToProduct.Aux[F, I => O]): O = ftp(f)(i)
// So let's try to express that by extracting the input type of F as FI
def invoke2[FI, I <: HList, O](f: FI => O, i: I)
(implicit ftp: FnToProduct.Aux[FI => O, I => O]): O = ftp(f)(i)
"Invoke" should "work for a Function1" in {
// Here's our function (Int) => String
val f: (Int) => String = (i) => s"I got $i"
val l = 1 :: HNil
// this works
val r1: String = invoke1(f, l)
// So does this. (With evidence that the compiler sees the function parameter list (Int) as just Int
val r2: String = invoke2[Int, Int::HNil, String](f, l)
r1 shouldBe "I got 1"
r2 shouldBe "I got 1"
}
"Invoke" should "work for a Function2" in {
// Here's our function (String, Int) => String
val f: (String, Int) => String = (s, i) => s"I got $s and $i"
val l = "s" :: 1 :: HNil
// this works
val r1: String = invoke1(f, l)
// But this does not compile. There is no expansion for the type of FI
// (String, Int) != the function Parameter list (String, Int)
val r2: String = invoke2(f, l)
/*
Error:(...) type mismatch;
found : (String, Int) => String
required: ? => String
val r1: String = invoke1(f, l)
*/
r1 shouldBe "I got s and 1"
r2 shouldBe "I got s and 1"
}
}
Upvotes: 2
Views: 131
Reputation: 51723
Int => String
is syntax sugar for Function1[Int, String]
, (String, Int) => String
is syntax sugar for Function2[String, Int, String]
, ((String, Int)) => String
is syntax sugar for Function1[(String, Int), String]
aka Function1[Tuple2[String, Int], String]
.
You can help Shapeless to resolve FnToProduct
instances if you define implicit conversion
implicit def tupledFnToProduct[FI1, FI2, O, Out0](implicit
ftp: FnToProduct.Aux[Function2[FI1, FI2, O], Out0]
): FnToProduct.Aux[Function1[(FI1, FI2), O], Out0] =
new FnToProduct[Function1[(FI1, FI2), O]] {
override type Out = Out0
override def apply(f: Function1[(FI1, FI2), O]) = ftp((x, y) => f(x, y))
}
Then you can call invoke2
with .tupled
val f: (String, Int) => String = (s, i) => s"I got $s and $i"
val l = "s" :: 1 :: HNil
val r2: String = invoke2(f.tupled, l)
r2 == "I got s and 1" //true
Upvotes: 1