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

name of feature

this functions, dsl function, context functions, receiver function or import function don’t know which name is better. In whole document i’m using this function consequently.

specification

I propose to introduce additional trait into scala

trait ThisFunction[-T, +R] extends Function1[T, R] 
//trait ReceiverFunction[-T, +R] extends Function1[T, R]
//trait ContextFunction[-T, +R] extends Function1[T, R] 
//trait ImportFunction[-T, +R] extends Function1[T, R] 
//trait DslFunction[-T, +R] extends Function1[T, R] 
//trait ScopeFunction[-T, +R] extends Function1[T, R]
//trait Scope[-T, +R] extends Function1[T, R]  //???

it is like normal Function1 and always has only one argument.
It is specially treated when such function is defined literally:

//val fun:Scope[SomeBuilder, Any] = {
//val fun:ReceiverFunction[SomeBuilder, Any] = {
val fun:ThisFunction[SomeBuilder, Any] = {
    someFunctionOnX()
    other() 
} 

It is defined not as function but as block (as seen above).
Inside definition of this function we see all fields, methods, implicits and extension methods of SomeBuilder without any prefix.

that can be used for example like this (simplest possible use-case):

def head(f:ThisFunction[SomeBuilder, Any]) = ???

which can be replaced by simplified notation:

def head(this f:SomeBuilder => Any]) = ???
//or 
def head(f: this SomeBuilder => Any]) = ???
//if `this` keyword is unacceptable, then we can use other keywords instead 
//def head(import f:SomeBuilder => Any]) = ???
//def head(with f:SomeBuilder => Any]) = ??? 
//def head(with import f:SomeBuilder => Any]) = ??? 

it’ll allow to call it like this. It is similar to kotlin receiver function.

head { 
	someFunctionOnX() 
	other() 
}

It is ‘almost’ equivalent to such code

head { x => 
   import x._ 
	someFunctionOnX() 
	other()
}

‘almost’ means that there are some diffrences. Here are rules that are applied into call site:

  • we can use all fields/methods that are in SomeBuilder
  • all implicit fields/methods are avaliable in this scope without prefix
  • we can use extension methods of SomeBuilder in place (insted of x.extFunc() we can write extFunc())
  • we don’t do anything with this reference in block. It shows to whatever it shown previously (place where ThisFunction is bit confusing)!
  • instance of SomeBuilder is not accessible (it has no name). Below is proposition how to obtain this reference.

head is ordinary method and it should be possible to just call it like other methods. We need to remember that on call site it’ll be seen as it expect Any! Kind of head’s first argument is changed from expected (*) -> (*) to (*)

head { println("yea") }
head { 22 }

Such code should be disallowed due to wrong kind that cannot be redused properly:

def head(this f:SomeBuilder) = ??? //compilation error
def head(this f:(SomeBuilder, String) => Any]) = ???  //compilation error

example with expected function

//all definitions are exactly the same 
def head(f:ThisFunction[SomeBuilder, Function1[Key, Any]]) = ???
def head(f:ThisFunction[SomeBuilder, Key => Any]) = ???
def head(this f:SomeBuilder => Key => Any]) = ???

head { k => 
	someFunctionOnX(k) 
	other(k)
}

/** or even */
head { implicit k => 
	someFunctionOnX() 
	other()
}

Motivation

Scala has lot of features that simplify to write dsls, but still lacks of good feature that’ll introduce context precisely. We can ‘imit’ this feature using new in our dsl but there are cases where it is not desirable.

new Head { /** we have access to every function in head ;) */}

interactions with ImplicitFunction

important part is how it’ll behave with ImplicitFunctions?

def head(f:ThisFunction[SomeBuilder, ImplicitFunction1[Key, Any]]) = ??? 
//this is extreamly powerfull. Rather not happy with that!
def head(this f:SomeBuilder => implicit String => Any]) = ??? 
//same as above

//this'll allow for such constructions? 
//Much to POWERFULL!
head {
	val x = implicitly[Key] //comes from ImplicitFunction1 ???
	someFunctionOnX(x) //comes from SomeBuilder.
}

define function in other place than call site

val func: ThisFunction[SomeBuilder, Any] = {
	someFunctionOnX()
	other()
}
head(func)

using Function1 as ThisFunction

we should be able to pass normal function to head.

val func = f:SomeBuilder => ()
head(func)

but then types does not fit. Theoretically we can fix that by simple implicit conversion from Function1[SomeBuilder, Any] to ThisFunction[SomeBuilder, Any], but it can be to much fragile (TODO test how much).

Not sure if it should be done by implicitly, but we can do it explicitly using extension method on Function1

val func = f:SomeBuilder => ()
head(func.toThis)
//head(func.toThisFunction) 
//head(func.toContextFunction) 
//head(func.asThis)
//head(func.this)

use this argument explicitly

Ok, but sometimes we want to explicitly name this argument. What then?
we can use special notation (mark argument as this) and compiler should be happy:

head { this x => 
	x.someFunctionOnX() 
	x.other() 
}

val func:ThisFunction[SomeBuilder, Any] = this x => {
	x.someFunctionOnX()
	other() //still possible
}
head(func)

//TODO what with such construct?
val func:ThisFunction[SomeBuilder, Any] = implicit this x => {
	x.someFunctionOnX()
	other() //still possible
}

what heappen when we miss this keyword?

head { x => 
	x.someFunctionOnX() 
	x.other() 
}

Thankfully compiler should show us error here that he doesn’t know type of x. It expects Any and cannot infer type of argument of annynous function, byt if we’ll be bit more helpfull here:

head { x:SomeBuilder => 
	x.someFunctionOnX() 
	x.other() 
}

then this code’ll compile (and it’ll be horribly wrong!). This should never happen if we’ll accept more useful result type than Any. Still i think this is the weakest part of whole PRE_SIP

4 Likes

I agree, kotlin receiver function(kotlin documentation)
is a great thing.
With that functionality we can make more comfortable dsl.

But I am not sure I understand all your proposal correctly.

Could you give a complite example for kotlin kode:

class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // create the receiver object
    html.init()        // pass the receiver object to the lambda
    return html
}

html {       // lambda with receiver begins here
    body()   // calling a method on the receiver object
}

How 'head\html` will be implemented?

How SomeBuilder \ HTML will be implemented?

Why

head { x => 
   import x._ 
	someFunctionOnX() 
	other()
}

May be more precisely

head { x => 
   new Builder(x){
       def execute():Any = {
              someFunctionOnX() 
	      other()
       }
   }.execute()

}

?

1 Like

SomeBuilder in my examples is our DSL model class that contains avaliable functions (HTML in your example).

class HTML {
    def body() { ... }
}
//def html(init: ThisFunction[HTML, Unit]): HTML = {
def html(this init: HTML => Unit): HTML =  {
    val html = HTML()  // create the receiver object
    init(html)        //ThisFunction extends Function1 and we can call it as normal 
    html
}

html {       // lambda with receiver begins here
    body()   // calling a method on the receiver object
}

yea… but it shows that we magically create some Builder what is not always appropriate. We just create function that has prepared scope. But yes, your code better shows semantic of ThisFunction block.

A previous use case of receiver functions (builders) could be handled with implicit function types. I wonder if that generalizes - i suspect there’s a way to do it.

I love this idea. I’m a heavy user of scala for DSL purposes. I need to find time to wrap my head around this proposal and provide a more thorough feedback.

This has been discussed, and prototyped, before as

3 Likes

implicit function will never bring new names to scope, it’ll only make them available to use. This is powerful but needs very careful design of dsl, and lot of things’ll still be impossible. I see that this proposition is too powerful, but this is what i ended with when i tried to apply kotlin design here.

To be honest I’m exploring ImplicitFunctions right now. There is important change in implicit resolution that allows for such code:

case class CompCtx(path:List[String] = List.empty) {
  override def toString() = path.reverse.mkString(".")
  def fork(part:String): CompCtx = copy(path = part :: path)
}

class Component(val name:String)(implicit cc:CompCtx) {
  protected implicit val innerCC: CompCtx = cc.fork(name)
  println(name + " -> " + innerCC.path.reverse.mkString(".")) //test what innerCC we have
}

//and usage
implicit val ctx: CompCtx = CompCtx()
  
class ErrorBox(name:String, cc0:CompCtx) extends Component(name)(cc0) {
    def this()(implicit cc0:CompCtx) = this("errorBox", cc0) //we need it to not polute implicit cc0 acros whole file!
    println("angry definition from: " + implicitly[CompCtx])
  }
new Component("root") {
    val header = new Component("header") {
      val login = new Component("login") {
        println("happy definition from inline: " + implicitly[CompCtx])
      }
      val error = new ErrorBox {}
    }
    val menu = new Component("menu") {}
}

// in dotty
//root     -> root
//header   -> root.header
//login    -> root.header.login
//happy definition from inline: root.header.login
//errorBox -> root.header.errorBox
//angry definition from: root.header.errorBox
//menu     -> root.menu
//---------------------------------------
// in scala 2.12
//  error: ambiguous implicit values:
// both value ctx of type => CompCtx
// and value innerCC in class Component of type => CompCtx

I just note.
It is similar, but not the same.

Not talking about the feature itself or its merits for now.

Just pointing out that ThisFunction would be a terrible name for it because ThisFunction is a concept that already exist in Scala.js, and means something completely different. See

2 Likes

I’ve been using Scala to create internal DSLs and I have become actually quite disillusioned.

It starts out great, because it is easy to get something started. But as you are trying to continuously improve it based on user requests, it turns out that maintaining a DSL that at the same time is Scala and is trying to be something else is too constraining.

Over time, one of two things will happen:

Either your users will learn Scala and like to use Scala for the task, and then your DSL will gradually cease to be a DSL and instead turn into a regular Scala library which your users will use writing regular Scala code.

Or, your users will ask for improvements that look perfectly reasonable and straight-forward to them but require increasingly convoluted hacks to be implemented with an internal DSL, and then you clearly want an external DSL instead.

3 Likes

I would rather see def head(f: this SomeBuilder => Any) = ???, if I’m honest. It should be a modification of the type, not the parameter itself, just like => T for by-name parameters.

Also, you mentioned kotlins name for it, which is receiver function. Why not ReceiverFunction then?

I don’t really see the value of this feature in general. 99% of the time it’s perfectly fine to write { x => x.methodOnX(); otherMethod() }. And if you want to design some delicate DSL, you can do this with implicit function types with a little bit of extra boilerplate for the person who is designing the DSL, which imho is also fine.

class Builder { 
  private var foo = 0
  private var bar = 0
  def setFoo(a: Int) = foo = a 
  def setBar(a: Int) = bar = a
  def make = s"[foo:=$foo, bar:=$bar]"
}
object BuilderDSL {
  def setFoo(a: Int)(implicit b: Builder) = b.setFoo(a)
  def setBar(a: Int)(implicit b: Builder) = b.setBar(a)
  
  def build(f: implicit Builder => Unit): String = {
    val b = new Builder
    f(b)
    b.make
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    import BuilderDSL._

    val a = build {
      setFoo(4)
      setBar(2)
    }
    
    println(a)
  }
}
1 Like

That is why I will also invest my time in reactivating scala virtualized. It can smooth some of the rough edges.

Yes. But I think that “implicit” is very good for:

  • dependency injection
  • type injection

And it is very bad for dsl(builder template) because dramatically increase coupling.

Wide use of “implicit” for Dsl lead us to hell of “ambiguity” :))

It is like global variables, it is powerful. With global variables ( and go to :slight_smile: ) we can make everything. But I am personally not smart enough to do everything with global variables :slight_smile:

yea I see. This looks bit more odd to me but it is rather minor. You have right that semantically it could be after f:. I prefere to use it like other stuff that’ll go before parameter name like implicit and @someAnnotation

More important are:

  • what interactions with other scala features should be allowed
  • what could go wrong and what new corner cases’ll be introduced
  • if we really need this feature?

Note that, with the approach of implicits, you may run into the problem of ambiguity when using two grammars at once.

Tested in 2.12.6 (after changing the implicit function to the “desugaring”, of course), this does not compile:

class ExcelTable {
  private def addRow(r: ExcelRow): Unit = ???
}
case class ExcelRow(text: String)
object ExcelTable {
  def row(text: String)(implicit ev: ExcelTable) = ev.addRow(ExcelRow(text))

  def excelTable(f: implicit ExcelTable => Unit): ExcelTable = {
    val et = new ExcelTable
    f(et)
    et
  }
}
class HtmlTable {
  private def addRow(r: HtmlRow): Unit = ???
}
case class HtmlRow(text: String)
object HtmlTable {
  def row(text: String)(implicit ev: HtmlTable) = ev.addRow(HtmlRow(text))

  def htmlTable(f: implicit HtmlTable => Unit): HtmlTable = {
    val ht = new HtmlTable 
    f(ht)
    ht
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    import ExcelTable._
    val et = excelTable {
      row("I'm in Excel!")
    }

    import HtmlTable._
    val ht = htmlTable {
      row("I'm in HTML!")
    }
  }
}

And it gives the following error:

<console>:23: error: reference to row is ambiguous;
it is imported twice in the same scope by
import HtmlTable._
and import ExcelTable._
             row("I'm in HTML!")

(If anyone could check in Dotty, that’d be much appreciated)

Of course, this case is somewhat artificial, but as brought up in dsl-paradise, e.g. ScalaTags, Spray are two libraries that could benefit from this. Reducing problematic cases of identifiers overlapping could be useful in other DSLs as well, I can imagine.

in dotty 0.9.0-RC1

37 |      row("I'm in HTML!")
   |      ^^^
   |      reference to `row` is ambiguous
   |      it is both imported by import ExcelTable._
   |      and imported subsequently by import HtmlTable._
2 Likes

It very depends on business logic. I know people which can say that scala itself is too dsl oriented and they prefer to use java.

But I personally for sql library want to use kotlin and for type injection want to use scala. It is very bad choice for me.

We could also copy Kotlin there and opt for f: SomeBuilder.() => Any or similar, but I don’t think that’d go down too well for Scala aesthetics.

1 Like

I still don’t see what this example proves. How would receiver functions overcome this problem?
If you need to use 2 different row methods in the same scope you will have exactly the same issue. If you don’t you can put the imports and usages in separate scopes.

2 Likes