Polymorphic type inference for by-name and by-value types

Viewed 162

I've been stuck with a type inference problem and I'm not sure if I'm doing something wrong, there is a bug in the compiler, or it's a limitation on the language

I've created a dummy example to show the problem, the use case makes no sense, but trust me I have a valid use case for this

Let's say i have this code

val function: (Int, String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, P2) => R) {
   def printArgs(p1: P1, p2: P2): Unit = println(p1, p2)
}

function.printArgs(1, "foo")

That works and prints (1,foo) Now, if I change the code to be (notice the by-name argument)

val function: (Int, => String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, P2) => R) {
   def printArgs(p1: P1, p2: P2): Unit = println(p1, p2)
}

function.printArgs(1, "foo")

It will print (1,MyTest$$Lambda$131/192881625@61d47554)

Now, at this point I could try to pattern match and/or use a TypeTag to extract the value in case is a by-name parameter, but, what I'm actually trying to achieve is to do something like this

trait Formatter[T] {
  def format: String
}

case class ReverseStringFormat(v: String) extends Formatter[String] {
  override def format: String = v.reverse
}

case class PlusFortyOneFormat(v: Int) extends Formatter[Int] {
  override def format: String = (v + 41).toString
}

implicit def noOpFormatter[T](v: T): Formatter[T] = new Formatter[T] {
  override def format: String = v.toString
}

val function: (Int, => String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, P2) => R) {
   def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println( p1.format, p2.format)
}

function.printArgs(1, ReverseStringFormat("foo"))

Notice that the main intention is that I should be able to pass either the original type of the argument or a formatter instead, that's why the signature of this extension method uses Formatter[TypeOfOriginalParam] and that's also why I have implicit def noOpFormatter[T](v: T): Formatter[T] for when I don't want any formatting

Now, here, I can't do much as the code fails to compile with this error

Error:(22, 40) type mismatch;
   found   : ReverseStringFormat
   required: Formatter[=> String]
   function.printArgs(1, ReverseStringFormat("foo"))

I can make it run if I make the second type argument of my implicit class by-name

val function: (Int, => String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, => P2) => R) {
   def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println( p1.format, p2.format)
}

function.printArgs(1, ReverseStringFormat("foo"))

this prints (1,oof)

Now, the main problem is that I want to do this for any function regardless if any of its arguments is by-value or by-name. And that's where I'm stuck, I can create different implicit classes for every possible combination of cases where there is by-name params or not but it wouldn't be practical as I need to do this for every function from Function1 to Function10, and the the amount of possible combinations between by-name and by-value arguments would be massive.

Any ideas? Should I really need to care about the lazyness of an argument if I'm only interested in the type? Am I trying to do something unsupported by design or is maybe a bug in the compiler?

BTW, this is what I'm trying to avoid

val function: (Int, => String) => String     = (_, _) => ""
val function2: (Int, String) => String       = (_, _) => ""
val function3: (=> Int, String) => String    = (_, _) => ""
val function4: (=> Int, => String) => String = (_, _) => ""

implicit class Function2Ops[P1, P2, R](f: (P1, => P2) => R) {
  def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println("f1", p1.format, p2.format)
}

implicit class Function2Opss[P1, P2, R](f: (P1, P2) => R) {
  def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println("f2", p1.format, p2.format)
}

implicit class Function2Opsss[P1, P2, R](f: (=> P1, P2) => R) {
  def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println("f3", p1.format, p2.format)
}

implicit class Function2Opssss[P1, P2, R](f: (=> P1, => P2) => R) {
  def printArgs(p1: Formatter[P1], p2: Formatter[P2]): Unit = println("f4", p1.format, p2.format)
}

function.printArgs(1, "foo")
function2.printArgs(1, ReverseStringFormat("foo"))
function3.printArgs(1, "foo")
function4.printArgs(PlusFortyOneFormat(1), "foo")

which it works (notice that I've used formatters or raw values randomly, it shouldn't matter if the original param was by-name or by-value)

(f1,1,foo)
(f2,1,oof)
(f3,1,foo)
(f4,42,foo)

but it seems super odd to have to write all of that to me

2 Answers

I suggested using type classes and here is how I would implement them.

First I would create a type class for printing a single argument.

trait PrintArg[A] { def printArg(a: A): String }

Then I would implement instances for handling by-name and by-value parameter types:

object PrintArg extends PrintArgImplicits {
  def apply[A](implicit pa: PrintArg[A]): PrintArg[A] = pa
}
trait PrintArgImplicits extends PrintArgLowLevelImplicits {
  implicit def printByName[A] = new PrintArg[=> A] { def printArg(a: => A) = a.toString }
}
trait PrintArgLowLevelImplicits {
  implicit def printByValue[A] = new PrintArg[A] { def printArg(a: A) = a.toString }
}

Except Scala forbid us from declaring by-name type in other places than function declaration syntax.

error: no by-name parameter type allowed here

Which is why we're going to work around this: we'll declare by-name type in a function declaration and lift that function to our type class:

def instance[A](fun: A => String): PrintArg[A] = new PrintArg[A] { def printArg(a: A) = fun(a) }

def printByName[A] = {
  val fun: (=> A) => String = _.toString
  PrintArg.instance(fun)
}
def printByValue[A] = {
  val fun: A => String = _.toString
  PrintArg.instance(fun)
}

Now, lets put everything together:

trait PrintArg[A] { def printArg(a: A): String }
object PrintArg extends PrintArgImplicits {
  def apply[A](implicit pa: PrintArg[A]): PrintArg[A] = pa
  def instance[A](fun: A => String): PrintArg[A] = new PrintArg[A] { def printArg(a: A) = fun(a) }
}
trait PrintArgImplicits extends PrintArgLowLevelImplicits {
  implicit def printByName[A] = {
    val fun: (=> A) => String = _.toString
    PrintArg.instance(fun)
  }
}
trait PrintArgLowLevelImplicits {
  implicit def printByValue[A] = {
    val fun: A => String = _.toString
    PrintArg.instance(fun)
  }
}

Finally, we can use the type class in printer to handle all cases at once:

implicit class Function2Ops[P1: PrintArg, P2: PrintArg, R](f: (P1, P2) => R) {
  def printArgs(p1: P1, p2: P2): Unit =
    println(PrintArg[P1].printArg(p1), PrintArg[P2].printArg(p2))
}

val function1: (Int, String) => String = (_, _) => ""
val function2: (Int, => String) => String = (_, _) => ""

function1.printArgs(1, "x")
function2.printArgs(2, "y")

would print

(1,x)
(2,y)

Bonus 1: if you have a type which doesn't print anything useful if you toString it, you can just provide another implicit def/val and extend support to all 3x3 cases.

Bonus 2: for this specific example I basically shown how to implement and use Show type class, so what you probably need to do here would be simply reuse existing implementations (and maybe provide implicit def for just by-name case). But I'll leave that as an exercise for the reader.

I'm not sure I entirely understand what your ultimate goal is, but I can explain to you why you're seeing what you're seeing.

There are two facts about the Scala language that combine to produce the problem you're in:

  1. By-name => A is not a type; and
  2. By-name has to be part of a method signature because the calling code needs to construct a lazy value.

To see that (1) is true, observe that you cannot do this:

val a: => Int = 3

In other words, although (=> Int) => Int is a type, => Int is not a type. You cannot have a value of type => Int.

To see that (2) is true, consider what happens when you call a method with a by-name parameter:

def foo(a: => Unit) {
  println("1")
  a
}

foo(println("2"))

The calling code needs to know not to pass () as the argument, but rather to pass a promise. The calling code must therefore know that foo takes a by-name parameter, and so the fact that it is a by-name parameter must be a part of the method signature. Behind the scenes, the calling code must construct a no-arg method of type () => Unit and pass that.

Now, how do these combine to produce the problem in your code?

When you write:

implicit class Function2Ops[P1, P2, R](f: (P1, P2) => R) {
   def printArgs(p1: P1, p2: P2): Unit = println(p1, p2)
}

and pass an argument of type (Int, => String) => String, fact (2) requires that that P2 cannot bind to String. If P2 bound to String, then f would have type (Int, String) => String, which contradicts the rule that by-name-ness must be reflected in the method signature. Although your printArgs doesn't call f, it could, so the signature of f must preserve the by-name-ness.

However, fact (1) requires that P2 cannot bind to => String, because => String is not a type. So, the only remaining option is for P2 to bind to () => String. This is compatible with the implementation of by-name parameters as no-arg functions, and ensures that printArgs will receive the correct types from the caller and pass the correct types to f, should it choose to call f.

As a final comment, observe that it is not possible to abstract over by-name parameters because use of a by-name parameter must generate appropriate byte-code. Observe that the following two functions must generate different byte-code:

def foo(a: => Unit) {
  println(a)
}

def foo(a: Unit) {
  println(a)
}

Because type parameters in Scala are erased at compile time, it is never possible to use type parameters in a way that would alter a function's behavior. Therefore, it is not possible to abstract over by-name arguments using type parameters alone.

This final observation shows how, ultimately, you can solve your problem: You must abandon using pure type parameters and introduce value parameters. As Mateusz Kubuszok observes, one way to introduce value parameters is to use type-classes. There are other ways, such as type tags or explicit value parameters. However, in order to alter the behavior of a function, you must rely on something other than type parameters alone.

Related