Jeremy Townson
Jeremy Townson

Reputation: 113

Scala's type system and the input to FunctionN

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

Answers (1)

Dmytro Mitin
Dmytro Mitin

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

Related Questions