I support this proposal! And think it’s a much better solution than implicit function types, for the kinds of things implicit function types have been advertised for.
Here’s a quick list of other things that implicit function types cannot do:
-
ThisFunction
can inject common types likeString
andInt
- It is common folk wisdom that in Scala, you of course do not declare common types like Int and String to be implicit, but I think it’s worth pointing out that this is only a limitation of implicits, and not of ThisFunctions/ReceiverFunctions.
E.g.
trait Foo { def x: Int; def y: String }
val f: ThisFunction[Foo, Int] = {
x + y.length
}
- ThisFunction can inject multiple members/values of the same type, unlike implicits
E.g.
case class Foo(x: Int, y: Int, z: Int, zz: Int, zzz: Int) {
def zzzz: Int
}
val f: ThisFunction[Foo, Int] = {
x + y + z + zz + zzz + zzzz
}
- ThisFunction introduces capabilities into a scope in a DISCOVERABLE way. Consider the example typically given, with HTML builders:
class Table {
val rows = new ArrayBuffer[Row]
def add(r: Row): Unit = rows += r
override def toString = rows.mkString("Table(", ", ", ")")
}
class Row {
val cells = new ArrayBuffer[Cell]
def add(c: Cell): Unit = cells += c
override def toString = cells.mkString("Row(", ", ", ")")
}
case class Cell(elem: String)
def table(init: implicit Table => Unit) = {
implicit val t = new Table
init
t
}
def row(init: implicit Row => Unit)(implicit t: Table) = {
implicit val r = new Row
init
t.add(r)
}
def cell(str: String)(implicit r: Row) =
r.add(new Cell(str))
table {
row {
cell("top left")
cell("top right")
}
row {
cell("bottom left")
cell("bottom right")
}
}
This example is taken from the Dotty website. However even this example is much better handled by ThisFunction’s. The problem with implicits here is that we are relying on the availability/unavailablility of many implicit values to tell us what is “in scope / available to use” and what isn’t. For example:
cell("top left")
// ^-- only because implicit search fails
// do I find out this is wrong! `cell` is in scope, but
// I would have to simulate implicit search to know that it
// can't be used here
row {
}
// ^-- same thing here
table {
cell("top left")
// ^-- and here
row {
cell("top left")
cell("top right")
}
row {
cell("bottom left")
cell("bottom right")
}
}
In Kotlin, or with ThisFunction
, it is extremely obvious what is and what is not available.
- With Implicit function types, even with all this working you still need to know where is the big import you need to perform. You’ll have to search around for where are all these little DSL functions in some package object somewhere and remember to import
com.example.my.package.dsl._
whereas with ThisFunction, the exact right members are imported into scope in exactly the right places and only in the right places.
An other benefit: ThisFunction
does not imply (and therefor commit to) any kind of ordering to the members injected. Suppose I have the following equivalent code under both ThisFunction and Implicit-Function-Types styles
trait Foo { def x: String; def y: Int }
val f: ThisFunction[Foo, Unit] = {
print(x + y)
}
f(new Foo("", 1))
val g: (implicit x: String, y: Int) => Unit = {
print(x + y)
}
g("", 1) // calling g explicitly
Now imagine I change the order around…
trait Foo { def y: Int; def x: String }
val g: (implicit y: Int, x: String) => Unit = {
print(x + y)
}
^ here, the order of the parameters changing has broken the call to g
, but f
is perfectly fine.
@scalway thank you for this proposal! I really hope something like this can get into Scala.