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.