Getting familiar with Scala3, I learned about the concept of context functions. However, looking at their use-case, I got a feeling they could be replaced by conceptually much simpler feature - lambda methods. Lambda methods are anonymous extensions, with implicit this argument. They exist in Kotlin under the name function with receiver. In short:
fun foo(fn: Int.() -> Int) = 1.fn()
foo { increment() } == foo { this.increment() } == 2
Comparison of context functions to lambda methods
Builder pattern:
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)
val table =
table {
row {
cell("top left")
cell("top right")
}
row {
cell("bottom left")
cell("bottom right")
}
}
Context functions
def table(init: Table ?=> Unit) =
given t: Table = Table()
init
t
def row(init: Row ?=> Unit)(using t: Table) =
given r: Row = Row()
init
t.add(r)
def cell(str: String)(using r: Row) =
r.add(new Cell(str))
Lambda methods (Kotlin)
fun<T> T.with(fn: T.() -> Unit): T =
fn()
return this
}
fun table(init: Table.() -> Unit) = // look at the type signatures
Table().with { init() } // ignore the implementation
fun Table.row(init: Row.() -> Unit) = // it reads like an ordinary lambda
add(Row().with { init() }) // no need for `given` or `using`
fun Row.cell(str: String) =
add(new Cell(str))
Post-conditions
Context functions
object PostConditions:
opaque type WrappedResult[T] = T
def result[T](using r: WrappedResult[T]): T = r
extension [T](x: T)
def ensuring(condition: WrappedResult[T] ?=> Boolean): T =
assert(condition(using x))
x
end PostConditions
import PostConditions.{ensuring, result}
val s = List(1, 2, 3).sum.ensuring(result == 6)
Lambda methods (Kotlin)
val<T> T.result = this
fun<T> T.ensuring(condition: T.() -> Boolean): T {
assert(condition())
return this
}
val s = list(1, 2, 3).sum().ensuring{result == 6}
To me, the extension based type signatures look much more understandable and beginner friendly. It is very possible, that contexts are more powerful, but then
- The documentation should show examples where more power is needed.
- It should be considered, whether the extra power is worth the complexity.
Note: I used kotlin syntax, just because I wanted to discuss the concept itself, before arguing about the particular syntax this could have in Scala.