DelayedInit or OnCreate, any solution?

@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!

The implementation above did not allow multiple definitions with the implicit Post calling one another. At the following thread I posted a fixed implementation that still uses ambiguity but with the help of shapeless’s LowPriority and in a more generic fashion:

I don’t really see how it’s relevant to the discussion about DelayedInit, though (might be that I’m missing something here). If you have multiple methods with the same name shadowing or conflicting with each other, you will always have a problem. How would DelayedInit solve that problem?

It doesn’t, that was him explaining a use case where static scope injection is strictly more powerful than the proposed alternative of “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”.

Yes, you are right.
I can also add:
If I had something similar to kotlin receiver function
I would never use DeleayInt. I think construction like

new JFXApp.PrimaryStage {
...
}

Is not very good for that purpose.

But now I am not ready to write JFXApp dsl in scala3.
DeleayInt is absent
Receiver function is absent.

So may be better to use simple java library. It always work at least :wink:

As an intermediate checkpoint for this discussion, it sounds like there are two use cases for DelayedInit:

  • “life-cycle management”: capturing user code and allowing custom user-specified code to run before and after (e.g., in specs2 and ScalaTest)
    The ScalaTest usage of DelayedInit also nicely explains how to avoid using DelayedInit by putting the code that needs to be intercepted in a method :slight_smile:
  • “scope management”, which would likely be subsumed by implicit function types or “static scope injection”: implicit function types replace scoping by availability of implicits, which may result in name clashes

Assuming this is an exhaustive summary, I propose we move to accept dropping DelayedInit, if those in favor of dropping it can show a compelling way to implement these two use cases.

1 Like

I’m not familiar enough with the ScalaFX code base to be certain which of these applies to its usage, but I’m hoping that one of the maintainers of ScalaFX will jump in to either confirm that this works or state why it doesn’t.

Which one of this use cases cannot be implemented with kotlin receiver function?

I can imagine the only one use case where we want to define class extension and apply function in one context:

  class A extend B{
      ovveride def someFunc():Unit = ???
      sumFunc()
  }

instead of

  class A extend B{
      ovveride def someFunc():Unit = ???
      ovveride def apply():Unit = {
          sumFunc()
      }
  }

It seems it is impossible without DeleyedInit.
So it should not be depreciated :slight_smile:

Or may be we need some syntax changes, for example:

object TestApp extend Application do {
  doSomething
}

which will be sugaring to

object TestApp extend Application{
   ovveride def apply(){
      doSomething
   }
}

Random thought (perhaps too far out of the box? :-)): how about a marker trait NoInit, which will cause an error to be generated for subclasses (trait/class) that have any initialization statements outside of calling the super constructor / passing constructor params? That way you can force subclasses to put anything side-effecty in a method that you can then call when appropriate.

The nice side-effect is that this also helps with reasoning about these classes’s initialization behavior (this is how this occurred to me), and that traits with a NoInit parent have a cleaner mapping to java interfaces (and a tighter binary compatibility story).

3 Likes

It can make my life easier.

Why is it trait, why is it not an annotation?

For example I do not think it is good to generate java interfaces like:

public interface SimpleTrait extends NoInit {

}

My actual usage is in a embedded DSL (SpinalHDL), it use DelayedInit/onCreate to automaticaly build a component hierarchy from the user code execution:

//embedded DSL core
object Component{
   val componentStack = mutable.Stack[Component]()
}

class Component extends OnCreate{
  val children = ArrayBuffer[Component]()
  val parent = Component.componentStack.head()
  parent.children += this

  Component.componentStack.push(this)
  
  //Automatic callback after the whole object construction
  def onCreate() = Component.componentStack.pop() 
}

//User code
class UserSubComponent extends Component{
  //behaviour code
}

class UserComponent extends Component{
  //behaviour code
  val subA = new UserSubComponent() //subA automaticaly know his parrent component via the componentStack, and it will automaticaly add itself to the UserComponent children list.
  val subB = new UserSubComponent()
}

It is also used in some other similar cases.

I do not think it is equal replacement. IIUC An ide code assistant will work much worse on complex structures.

I am sure good code assistant is a killer feature in industrial programming.

I’m dropping the usage of DelayedInit in my project by using a compiler plugin providing a equivalent functionality.

There is the user trait :

And there is the compiler plugin implementation (the case cd: ClassDef is for another feature)

For each “new XXX()” where XXX implement the PostInitCallback trait, it transform it into :
“new XXX().postInitCallback()”

So far it seem to work fine.

3 Likes