PRE SIP: ThisFunction | scope injection (similar to kotlin receiver function)


#42

It looks more like an anti-pattern to me. Introducing a bunch of unchecked mutating stuff, only for making the call-site look slightly more interesting.
You can do virtually the same thing, but safer, with named and default arguments.


#43

Sure, assign a variable directly is a bad idea.
However, assign in Kotlin will call setter function instead of assign directly.


#44

Builder in Dotty


#45

So does Scala if you define a var_= method on a class. This could in theory also work with the automatically generated _= method for vars.
With ThisFunctions, your ajax example could work as follows:

class Ajax() {
  def url_=(url: String): Unit = { /* */ }
  def method_=(url: String): Unit = { /* */ }
  def success(f: String => Unit): Unit = { /* */ }
  def error(f: Exception => Unit): Unit = { /* */ }
}
def ajax(f: this Ajax => Unit): Unit = {
  val aj = new Ajax
  
  f(aj)

  // actually do the ajax evaluation
}

def testHttpOnError(): Unit = {
  val testUrl = "https://www2.baidu.com"

  ajax {
    url = testUrl
    method = "get"
    success { s =>
      println(string)
    }
    error { e =>
      println(e.message)
      Assert.assertTrue("connect timed out" == e.message)
    }
  }
}

with a translation of:

def testHttpOnError(): Unit = {
  val testUrl = "https://www2.baidu.com"

  ajax { _this: Ajax =>
    _this.url_=(testUrl)
    _this.method_=("get")
    _this.success { s =>
      println(string)
    }
    _this.error { e =>
      println(e.message)
      Assert.assertTrue("connect timed out" == e.message)
    }
  }
}

#46

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 like String and Int
    • 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. :tada:


#47

After recently merged dotty extension methods pull request It become available to “mimic” something similar to what is expected by the means of specially arranged extension methods.
Actually I’ve already mentioned about this possibility in some previous post but that example is little bit obsolete (uses previous syntax), and probably not enough expressive.

So here is rewritten version of that crafts, which actually does something similar to
builder.scala
but using extension methods for some sort of scope semi-injection (here I call it “semi-injection” because “injected” names becomes available not “as is” but with some %-prefix)

// this is currently compilable/working code - works under current state of dotty master branch

object TestMain {

  def main(args: Array[String]): Unit = { Test.testDsl() }


  object Test {
    import builders.table
    import dslActivators.{dsl => %}

    val data =
      %table {
        %row {
          %cell("A1")
          %cell("B1")
        }
        %row {
          %cell("A2")
          %cell("B2")
        }
      }

    def testDsl(): Unit = {
      println(data)
      assert(s"$data" == "Table(Row(Cell(A1), Cell(B1)), Row(Cell(A2), Cell(B2)))")
    }
  }


  import scala.collection.mutable.ArrayBuffer

  object dslActivators {

    // dsl activating marker
    trait DslActivator

    // any "dummy" name that should serve as extension target (part of scope semi-injection)
    object dsl extends DslActivator

  }

  object builders {
    import models._
    import dslActivators._

    def (erased dslActivator: DslActivator) table (tableInit: implicit TableBuilder => Unit) : Table = {
      val tableBuilder = new TableBuilder
      tableInit(tableBuilder)
      tableBuilder.build
    }

    class TableBuilder {
      val target = new Table()
      def build: Table = target

      def (erased dslActivator: DslActivator) row (rowInit: implicit RowBuilder => Unit) : this.type = {
        val rowBuilder = new RowBuilder
        rowInit(rowBuilder)
        target.add(rowBuilder.build)
        this
      }
    }

    class RowBuilder {
      val target = new Row()
      def build: Row = target

      def (erased dslActivator: DslActivator) cell (payload: String) : this.type = {
        target.add(new Cell(payload))
        this
      }
    }
  }


  object models {
    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(", ", ", ")")
    }

    class Cell(elem: String) {
      override def toString = s"Cell($elem)"
    }
  }

}

Here one can see that

    val data =
      %table {
        %row {
          %cell("A1")
          %cell("B1")
        }
        %row {
          %cell("A2")
          %cell("B2")
        }
      }

Is in fact

    val data =
      %.table {
        %.row {
          %.cell("A1")
          %.cell("B1")
        }
        %.row {
          %.cell("A2")
          %.cell("B2")
        }
      }

In “global” scope imported only builders.table (which is extension method applicable to %), other names from this snippet (%.row , %.cell) are not globally imported, instead they are “inject” into scope in some “fine way” by implicit functions.

So that this approach could be formally treated as some sort of workaround for issue highlighted by @AMatveev in his post related to DelayedInit vs implicit functions types

Continuing writing some other variation of aforementioned snippet one can also write something like

// this is currently compilable/working code - works under current state of dotty master branch

object TestMain1 {

  def main(args: Array[String]): Unit = {
    Test.testDsl()
    Test2.testDsl()
    Test3.testDsl()
  }


  object Test {
    import builders.table
    import dslActivators.dsl

    val data =
      %table {
        %row {
          %cell("A1")
          %cell("B1")
        }
        %row {
          %cell("A2")
          %cell("B2")
        }
      }

    def testDsl(): Unit = {
      println(data)
      assert(s"$data" == "Table(Row(Cell(A1), Cell(B1)), Row(Cell(A2), Cell(B2)))")
    }
  }

  object Test2 {
    import builders.table
    import dslActivators.dsl

    val data =
      dsl.table {
        dsl.row {
          dsl.cell("A1")
          dsl.cell("B1")
        }
        dsl.row {
          dsl.cell("A2")
          dsl.cell("B2")
        }
      }

    def testDsl(): Unit = {
      println(data)
      assert(s"$data" == "Table(Row(Cell(A1), Cell(B1)), Row(Cell(A2), Cell(B2)))")
    }
  }

  object Test3 extends dslActivators.DslActivator {
    import builders.table

    def testDsl(): Unit = {
      val data =
        Test3.this.table {
          Test3.this.row {
            Test3.this.cell("A1")
            Test3.this.cell("B1")
          }
          Test3.this.row {
            Test3.this.cell("A2")
            Test3.this.cell("B2")
          }
        }
      println(data)
      assert(s"$data" == "Table(Row(Cell(A1), Cell(B1)), Row(Cell(A2), Cell(B2)))")
    }
  }


  import scala.collection.mutable.ArrayBuffer

  object dslActivators {

    // dsl activating marker
    trait DslActivator

    // any "dummy" name that should serve as extension target (part of scope semi-injection)
    object dsl extends DslActivator

  }

  object builders {
    import models._
    import dslActivators._

    def (erased dslActivator: DslActivator) table (tableInit: implicit TableBuilder => Unit) : Table = {
      val tableBuilder = new TableBuilder
      tableInit(tableBuilder)
      tableBuilder.build
    }

    class TableBuilder {
      val target = new Table()
      def build: Table = target

      def (erased dslActivator: DslActivator) row (rowInit: implicit RowBuilder => Unit) : this.type = {
        val rowBuilder = new RowBuilder
        rowInit(rowBuilder)
        target.add(rowBuilder.build)
        this
      }
    }

    class RowBuilder {
      val target = new Row()
      def build: Row = target

      def (erased dslActivator: DslActivator) cell (payload: String) : this.type = {
        target.add(new Cell(payload))
        this
      }
    }
  }


  object models {
    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(", ", ", ")")
    }

    class Cell(elem: String) {
      override def toString = s"Cell($elem)"
    }
  }

}

Here most remarkable looks that snippet

    def testDsl(): Unit = {
      val data =
        Test3.this.table {
          Test3.this.row {
            Test3.this.cell("A1")
            Test3.this.cell("B1")
          }
          Test3.this.row {
            Test3.this.cell("A2")
            Test3.this.cell("B2")
          }
        }
      println(data)
      assert(s"$data" == "Table(Row(Cell(A1), Cell(B1)), Row(Cell(A2), Cell(B2)))")
    }

If that issue related to this.extMethod() <==> extMethod() natural behaviour
would be fixed, then that snippet would become look like “true scope injection”

// this is not valid code in current state of dotty master branch since `this.extMethods()` != `extMethods()` for now

  object Test3 extends dslActivators.DslActivator {
    import builders.table

    def testDsl(): Unit = {
      val data =
        table {
          row {
            cell("A1")
            cell("B1")
          }
          row {
            cell("A2")
            cell("B2")
          }
        }
      println(data)
      assert(s"$data" == "Table(Row(Cell(A1), Cell(B1)), Row(Cell(A2), Cell(B2)))")
    }
  }

But any way it could not be treated as final solution, rather as some tricky workaround.
Something similar to Kotlin’s extension methods / extension functions / extension lambadas
may fit for this problem much better.


#48

The link to

is this Extension Method


#49

Thanks, yes, exactly that pull request (link fixed)


#50

It is a pitty, but I think it will not help me much :frowning:

  • I would prefer to write html.table instead of %table because in such cases an operator has a magic sense. In our company it is bad style when you need look up headers to understand code. Such contraction can be used only in local code block.
  • The second greatest disadvantage of “implicit” in comparison to “receiver function” is absence of context dependent grammar . So I would afraid to use such approach in complex library like scalaFx I would prefer to use DelayedInit.