Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

scala

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

like image 939
Bruno Avatar asked Aug 27 '18 16:08

Bruno


People also ask

What types can be used for polymorphic type inference?

Any values of a, t, and x can be used here as long as A , B, C, and D have the given types. Both polymorphic type inference and pattern matching in OCaml are instances of a very general mechanism called unification . Briefly, unification is the process of finding a substitution that makes two given terms equal.

How does polymorphic type inference work in OCaml?

Both polymorphic type inference and pattern matching in OCaml are instances of a very general mechanism called unification . Briefly, unification is the process of finding a substitution that makes two given terms equal.

What is the difference between pattern matching and type inference?

Pattern matching in OCaml is done by applying unification to OCaml expressions (e.g. Some x), whereas type inference is done by applying unification to type expressions (e.g. 'a -> 'b -> 'a). It is interesting that both these procedures turn out to be applications of the same general mechanism.


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

like image 167
Mateusz Kubuszok Avatar answered Nov 15 '22 08:11

Mateusz Kubuszok