DelayedInit or OnCreate, any solution?

I take the chance to add another use case. We have test classes that implement certain life-cycle methods. In delayedInit the life-cycle methods are called in proper sequence including the actual test code:

**
  * Base trait for tests that takes care that the life-cycle methods of a test are called in proper sequence.
  *
  * Required components can be mixed into this trait. Read the documentation of the [[WithLifecycle]] trait
  * carefully in order to implement proper startup and shutdown sequences.
  */
trait InnerTest extends WithLifecycle with WithLog with DelayedInit {

  override def delayedInit(testCode: => Unit): Unit = {

    tryFinally {
      log.debug(s"before inner test - class: ${getClass.getSimpleName}")
      startup()
      log.debug(s"execute inner test - class: ${getClass.getSimpleName}")
      testCode
      log.debug(s"shutdown inner test - class: ${getClass.getSimpleName}")
      shutdown()
      log.debug(s"wait for completion of inner test - class: ${getClass.getSimpleName}")
      waitForCompletion()
      log.debug(s"inner test completed regularly - class: ${getClass.getSimpleName}")
    } {
      log.debug(s"cleanup inner test - class: ${getClass.getSimpleName}")
      cleanup()
      log.debug(s"after inner test - class: ${getClass.getSimpleName}")
    }
  }

}

We also use this type of dsl. I think with static scope injection and implicit function type (

)
It can be done better.

See also the dedicated thread that @etorreborre opened about specs2: Scala 3: DelayedInit

(but please continue the discussion here, instead)

This is now the ‘official’ thread related to removal of DelayedInit in Scala 3, as part of the first Scala 3 SIP batches. See First batch of Scala 3 SIPs

After some thoughts on this subject lately. Probably some use-cases can be satisfied with the addition of trait parameters. My use cases still require some sort of a post-constructor call. I propose the following OnCreate replacement.


trait AnyRef {
  // ....
  def OnCreate : this.type = this
}

The users can override OnCreate:

trait Foo {
  override def OnCreate : this.type = {
    //Some changes here
    this //either mutate this or return a new object
  }
}

Because the compiler identifies an overridden OnCreate then

new Foo {}

will be replaced with

new Foo {}.onCreate

Note: this is of course for cases where the end-users directly requiring interacting with new, so a companion object’s constructor is not enough to get the job done.

It is very often requiring when we use dsl.
And in such case

The above code is very bad decition.

Is there any plan to implement static scope injecitoion?

With that feature I will remove ‘new’ operator from my dsl with great pleasure.

@etorreborre Since Before/After are the real users of DelayedInit, and they’ve caused confusion before, have you considered deprecating them? Standard usage of Scope doesn’t require DelayedInit, right? It’s mostly the AOP-style before/after hooks.

Is it correct to summarize this as JFX being a shared resource that must be initialized before usage, and that should only be used through a well-defined entry point?

Sounds kind of like ExecutionContext or similar context that’s made available implicitly. Could the JFXApp supply the implicit, which is then consumed by everything needing access to it? Or just some lazy val that’s inherited from JFXApp, and some way of enforcing that all JFX access goes through it?

May be it is more closer to builder pattern.

It is not always comfortable In common case of builder pattern.
Because if grammar of builder is complex,
it is very important to have the ability to use context dependency.
For example it is very comfortable when method setImage is accessible only from ImageView.

Regarding static scope injection, I think it could be subsumed by methods defined in an outer scope that take an implicit argument to enforce that they can only be called in the scope of an implicit function.

Something like (very roughly, sorry):

trait JFXDsl {
   type JFX[T] = implicit JFXAT => T
   def stage(...): JFX[...] { /* the implicit arg of type JFXAT gives you access to the JavaFX subsystem*/ }
}

trait JFXApp extends JFXDsl {
  implicit lazy val jfx: JFXAT = initializeJFX
}

class MyJFXApp extends JFXApp {
  // stage implicitly gets the jfx context from JFXApp, 
  // can't be called without a JFXAT around
   stage(....) 
}

// alternatively, you can write code, and as long as the expected type is JFX[T], 
// it will be lifted to a closure that abstracts over the missing JFXAT argument
2 Likes

The implicit function type docs have a nice example on how to implement the Builder pattern: https://dotty.epfl.ch/docs/reference/implicit-function-types.html

If I understand it correctly it works well only in simple case.

With such approach we must:

  • make manual import of grammar (import SomeClass._)
  • cares that different inner method will not have same name

In may experence the

   new SomeClass{
       ...
   }

More comfortable and scalable now.

@adriaanm In my own DSL use-case, I use lazy values for this purpose. However, I’m missing an ability to annotate a definition/value so it can only be accessed from outside the class (meaning, post-construction). E.g.,

package MyLib
class Foo {
  @post[MyLib] lazy val dontTouchInside : Int = ???
}

Foo is library class that I’m exposing to users from which they can inherit.
The dontTouchInside lazy value must only be accessed post-construction of Foo or internally with [MyLib] privileges. In other words:

new Foo{}.dontTouchInside // should compile
new Foo { dontTouchInside} //should not compile when not in package MyLib

It can be real headache for such words like width, caption and so on. When your code works well until you do some more imports :frowning:

Could the internal and external facing members be different? The one that requires Foo be initialized, could take an implicit argument that’s made available by a factory method that creates Foo instances? It’s an interesting conundrum that this: Foo is more restricted than foo: Foo in terms of the capabilities it unlocks.

Sorry, I don’t understand. The example I mentioned does not contain any imports. One way to think of it is that you enforce scoping through the availability of implicit arguments, instead of nesting the definitions (which would indeed then require them to be imported).

Not in my use-case.

It’s an issue of access restriction. The users of the library are expected to inherit classes as part of the DSL, but they should not mess around with some capabilities that are there just for the post-construction phase of the class. In other words, even if I had an onCreate, I would have wanted to annotate it as private[MyLib].

Ok we have:
p1\Main.scala

package p1
import scala.collection.mutable.ArrayBuffer

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)


object HtmlTable{
    def htmlTable(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))
}

p3\Main.scala

package p3
import scala.collection.mutable.ArrayBuffer

class Table1 {
  val rows = new ArrayBuffer[Row1]
  def add(r: Row1): Unit = rows += r
  override def toString = rows.mkString("Table(", ", ", ")")
}

class Row1 {
  val cells = new ArrayBuffer[Cell1]
  def add(c: Cell1): Unit = cells += c
  override def toString = cells.mkString("Row(", ", ", ")")
}

case class Cell1(elem: String)


object ExcelTable{
    def excelTable(init: implicit Table1 => Unit) = {
      implicit val t = new Table1
      init
      t
    }

    def row(init: implicit Row1 => Unit)(implicit t: Table1) = {
      implicit val r = new Row1
      init
      t.add(r)
    }

    def cell(str: String)(implicit r: Row1) =
      r.add(new Cell1(str))
}

p2\main.scala

object Test{
   def main():Unit = {
       import p3.ExcelTable._ // it does not work without that 
       //import p1.Dsl.table  // it is not enough
        excelTable {
          row {
            cell("top left")
            cell("top right")
          }
          row {
            cell("bottom left")
            cell("bottom right")
          }
        }
        //It does not work in this context 
        import p1.HtmlTable._  
        htmlTable {
          row {
            cell("top left")
            cell("top right")
          }
          row {
            cell("bottom left")
            cell("bottom right")
          }
        }        
   }
}

And we have compilation error

[info] Compiling 1 Scala source to C:\buf\dotty\sample\target\scala-0.8\classes ...
[error] -- [E049] Reference Error: C:\buf\dotty\sample\src\main\scala\p2\Main.scala:19:10
[error] 19 |          row {
[error]    |          ^^^
[error]    |          reference to `row` is ambiguous
[error]    |          it is both imported by import p3.ExcelTable._
[error]    |          and imported subsequently by import p1.HtmlTable._
[error] -- [E049] Reference Error: C:\buf\dotty\sample\src\main\scala\p2\Main.scala:20:12
[error] 20 |            cell("top left")

If we try to work with these tables simultaneously

@adriaanm I found a workaround for my issue.
Library code

package MyLib
trait Post
@scala.annotation.implicitAmbiguous("This definition may be called only post-construction")
implicit object GeneralPost extends Post

class Foo {
  implicit object ForceAmbiguity extends Post
  private lazy val _postConstruction = {
    //Something to be used once at Post-Construction
    0
  }

  def postConstruction(implicit post : Post) : Int = _postConstruction
}

User code

new Foo {}.postConstruction //compiles
new Foo {postConstruction} //Error: This definition may be called only post-construction

So all public members that must be called post-construction must have a Post implicit (so the implicit also helps to document the constraint for the member).

What do you think?

1 Like

Neat!