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

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

Receiver functions overcome it by not making you import anything, i.e., you do not introduce new identifiers into the block where you call the function that expects a receiver function (i.e. the block in which you call excelTable and htmlTable in my example), but only in the block of code that you pass to that function (in which we then call the row function).

The whole of the code would then become:

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

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

  def htmlTable(f: this Builder => Unit): HtmlTable = {
    val b = new Builder(new HtmlTable)
    f(b)
    b.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!")
    }
  }
}

(This could probably be minimized further, but it’s an extremely small edit distance)

I Don’t like kotlin syntax for that. Maybe I’m just unfamiliar with it. Maybe.

Probably I need also to change name because of conflict with scala.js :frowning:.
Any ideas?
Ps: i tried to avoid ReceiverFunction[T,R] because don’t think it shows what it does. In kotlin it is function that can be called on T.

val init: HTML.() -> Unit
HTML().init() 

but in this proposal it is ordinary function that takes something special as first argument.

1 Like

see:https://github.com/edvin/kdbc/blob/master/src/main/kotlin/kdbc/expression.kt 3

Yeah, you’re right. Kotlin makes it look like the it’s just another extension function for the receiver.

I’m not familiar with Scala Virtualized, but just from the intro, I don’t think it solves the problem at all.

When you try to create an internal DSL by working to make Scala not look like Scala, but more like SQL or human language, or something else, you are deploying a lot of elaborate machinery for little gain. It doesn’t scale. Seemingly innocuous changes require too much work. The abstraction leaks. Scala syntax is very flexible, but still not flexible enough. Scala is way too powerful, your DSL allows to express an infinite number of things it should not allow, and you cannot turn them off. And, oops, you cannot change operator precedence or associativity. You cannot change what is an assignment, or a numeric or String literal.

Either express what you want in idiomatic Scala, or if you want a DSL, write a parser.

2 Likes

I’ve found that there’s a semantic disconnect when people talk about writing “DSLs” in Scala. Frankly, I think we should stop saying that it’s a good idea – at least in those terms.

Scala makes it relatively easy to add some kinds of language enhancements, which make it easier to do some kinds of things in Scala. I think of ScalaTest as the classic example: it provides a ton of “DSL” for testing.

But the key thing is, it’s not a separate language, it’s enhancements to Plain Old Scala. Within some fairly strong limits, Scala is decently good at that. But it’s not really a separate language, and trying to limit it to being a separate language seems like a lose.

So I honestly don’t think the term “DSL” really belongs here – it leads people to believe that they can build an entirely separate language inside Scala, which isn’t really true. (Even in specialized string interpolators, I’m not sure you can really turn off the Scala-ness.) What you can do is enhance the Scala language a bit, for some specialized purposes. When that’s what you want, great, and sometimes you can do remarkable stuff with it. (Eg, Rob’s BASIC-in-Scala monstrosity.) But it’s not really a true DSL…

2 Likes

I think we will be able to achieve much more easy solution, If we allow such creation of builder.

for examle

//See java lambda expression 
@FunctionalInterface class Builder{

}
class HTML extend Builder{
    def body() { ... }
}

def html(html: HTML): HTML =  {
    html.set(...) //dependency injection
    html.apply()       //ThisFunction extends Function1 and we can call it as normal 
    html
}

html {    
    body()   // calling a method on the receiver object
}

If I understand correctly it can be just sugaring

html(
  new HTML {
      override def apply():Unit{
       ....
      }
  }
)

It also has advantage:
We can manual override HTML

html(
  new HTML {
     override def someMethod()...
     override def apply():Unit = {
       ....
      }
  }
)

I don’t see much troubles with that approach, and it is more scalable in some cases.

Ok. I need reconsider whole idea. There is few steps that needs to be done before we go further:

  • looks to dsl-paradise and see all use cases.
  • see what can and what cannot be done with ImplicitFunctions
  • find alternative syntax for ThisFunction.
  • consider AMatveev proposition (still don’t understand this example but yea)

I hope to spend some time on it but still I’m waiting for opinions, corner cases and other stuff that was not been exposed yet. I understand that this idea is big and it can be harmful for language especially when is made wrong. It has been considered by many people before (dsl-paradise is great example) and it introduces possibilities that is hard to implement using existing features.

I agree. Most of the time when I talk about dsl i mean library that ‘fits’ into scala syntax but with nice and precise dictionary. ScalaTest doesn’t fit to this definition (in my opinion of course) because it tries to mimic English language (what is hard and leeks in many ways).

good examples of dsls are slick and scalatags. They lift scala language to provide great experience. You don’t need to know how implicit conversions, extensions methods and other stuff works to use them (you need to know few rules that are defined in documentation, and know scala a bit).

I founded way how to encode similar thing to this proposition using ImplicitFunction with bit more boilerplate. There is hope that this proposal could be not necessary. I need to test it more. Will see.