really wish :kotlin-flag: would have some kind of ...
# announcements
d
really wish K would have some kind of
postInit { }
or
ready { }
block that is called in the parent after all subclasses are fully constructed. Right now I'm implementing an abstract fun() that the child must override (to ensure that all subclasses always call the parent initializer method) (at the bottom of the child class) to call an initalize() fun in the parent. I'm wondering if there is a better way? Because the code that I would normally put in the
init { }
block needs to reference an abstract val that (implemented in the child) references a constructor property of that child
c
I would rethink the design at this point. From what I understand without seeing code, you have parent depending on a child implementation detail - never a good idea.
👍🏼 1
👍 6
d
the parent needs to start up some listeners with the abstract val being the id of what to watch out for.. but only the child can supply this id - I put an abstract fun in the parent to force implementations to remember to call the parent's code that starts the listeners (after the id is now available in the child) - what kinda bugs me is that I have to remember to override this fun in the child below the val overrides ( assuming my understanding of order of initialization is correct) - but I documented it, and can't think of a better solution
k
Why not just pass the id to the parent constructor?
👍 2
m
I had the same idea, actually also calling it
postInit
in my mind. Is there anything in Kotlin by now that gets executed in a superclass once the subclass is fully initialized? That would be really useful. In my case I create anonymous objects that define data with DSL (
val foo by dsl { … }
).
dsl
is function of the superclass. After all data is defined the superclass must further combine/process it. It can only do so once it knows that all data is defined. At the moment I’d basically have to add the following at the bottom of all 100+ subclasses:
Copy code
init { process() }
👍 2
e
this proposal doesn't seem to be implementable, at least on the JVM if you allow for Java classes to extend Kotlin classes
well, you may be able to enforce a specific pattern within your own projects with a linter, and post-hoc tricks like AOP could work too. but neither of those would be suitable for Kotlin itself
m
Valid point 🤔
@KotlinOnly
? I don’t need JVM compatibility anyway.
y
@Marc Knaup
@JvmSynthetic
is basically
@KotlinOnly
but only on the JVM
m
@Youssef Shoaib [MOD] unfortunately that doesn’t work on entire classes.
d
Would also be nice in Native for parents to
freeze()
the class after subclasses are done initialising fields.
👍 3
y
Hmmm, I'mma test real quick if it can work on the constructor because if it can then it effectively disallows subclasses EDIT: Nope, it doesn't work for constructors either, the only way I've found so far to prevent java implementers is to use an illegal name (e.g. with backticks and a space)
d
Regardless though, I agree that you need a redesign. Maybe use composition over inheritance.
m
Regardless though, I agree that you need a redesign. Maybe use composition over inheritance.
Agree with who? And I don’t think you have enough context for that. I haven’t found any way to change the design without increasing complexity and decreasing usability. Kotlin isn’t very good for static type-safe DSL yet. That requires anonymous objects. The only other way is what I have as a temporary work: All use-sites (anonymous objects) must manually complete the initialization. That effectively moves an implementation detail down in hierarchy.
e
I agree as well. if that's your issue, then I think your DSL is too tightly tied to the implementation. if the DSL just works on some configuration objects, and then your actual implementation is configured off of that, I think you'll avoid any issues as well as decouple the structure of your implementation from the DSL.
m
The DSL defines static data through anonymous objects. Let’s take CSS as a more concrete example for such static data:
Copy code
val styles = object : StyleSheet {
   val button by style { … } /* : String */
   val input by style { … } /* : String */
}

val globalStyles = object : StyleSheet {
   val primaryAccess by style { … } /* : String */
}
Consumers of data that was defined by such static DSL can simply use the properties:
Copy code
button(styles.button, globalStyles.primaryAccent) { … }
For that to work the
StyleSheet
base class will need to register all styles it defines, merged into a single block of CSS text. With regular DSL that’s easy, because the DSL controls entry and exit points:
Copy code
fun styleSheet(init: StyleSheet.() -> Unit) {
   // before init
   init()
   // after init
}

styleSheet {
   val button by style { … }
}
So when
styleSheet()
is done with
init()
it can complete the initialization. That is not currently possible if you need the variables accessible outside of the DSL block as properties. So the initial solution above remains. Any other solution would make the DSL significantly more verbose and repetitive. The only difference between the two is that the former approach (with anon object) cannot do anything at the exit point of the initialization.
e
so why not use
styleSheet()
?
d
That is not currently possible if you need the variables accessible outside of the DSL block as properties.
e
still other options available, such as
Copy code
val (styles, button, input) = styleSheet {
    exportStyle { /* button */ }
    exportStyle { /* input */ }
}
m
Btw, that static DSL is actually taken from a JetBrains library:
kotlin-styled
I’m just writing my own library on the same basis because it makes most sense.
@ephemient that’s not scalable and very error prone in mixing up definitions. Also you cannot simply jump to a definition. You have to actually count what definition is where.
Style sheets can easily contain 30+ styles.
Also the variables wouldn’t be namespaced but just float around unqualified.
e
well if that's the case it runs into other limitations first (IIRC components are limited to somewhere around 22 or 23)
I have no idea what the intended use for that DSL is, just pointing out what looked like an option given the example.
there are certainly possibilities such as
styles["button"]
as well, which of course has different tradeoffs
m
Type-safe CSS, just as
kotlin-styled
and
kotlin-css
provide. Both libraries have some issues though so I’m building my own.
kotlin-styled
DSL didn’t solve my particular issue and they have to work around that too in quite inefficient ways.
d
A compiler plugin seems appropriate for something like this tbh.
1
m
I thought of that as well. But it does seem overkill and adds unnecessary complexity & dependency for something relatively simple (in theory). Even worse, the code would seem to work on regular Kotlin but work differently with the plugin because the code is exactly the same. Just the behavior changes.
Unless I invent new syntax, which ain’t good either.
I actually have that problem with getting values out of a DSL block fairly often. In my GraphQL library for example, where types are defined within the DSL block but type-safe type refs need to be accessible outside.
n
I'm sure I'm misunderstandinging something, but why isn't it possible to combine the lambda and inheritance ideas?
This example given was like this
Copy code
val styles = object : StyleSheet {
   val button by style { … } /* : String */
   val input by style { … } /* : String */
}
Users could instead do:
Copy code
val styles = makeStyle {
    object : StyleSheet {
        ...
    }
}
and now the author of the DSL is free to have
makeStyle
execute some code after initializing the object
y
you could maybe even have it so that 
makeStyle
 provides you with a 
StyleScope
 that you're forced to pass to the constructor like this:
Copy code
abstract class StyleSheet(protected val createdInScope: StyleScope) {
    init {
        createdInScope.registerForPostInit(this)
    }
    // other code
}
class StyleScope internal constructor() {
    internal val sheets = mutableListOf<StyleSheet>()
    fun registerForPostInit(sheet: StyleSheet) { 
        if(isClosed) throw IllegalStateException()
        sheets.add(sheet)
    }
    // Safety feature just to ensure that no one will ever use this scope out-of-context
    internal var isClosed = false
}
inline fun makeStyle(block: StyleScope.() -> Unit) {
    val scope = StyleScope()
    scope.block()
    scope.isClosed = true
    for(sheet in scope.sheets) {
        // run post-init here
    }
}
// Then in user code

val styles = makeStyle {
    object : StyleSheet(this@makeStyle) {
        
    }
}
This basically turns your normal subclassing relationship into a DSL-controlled one so that at the end of the day the user is forced to run the post-init
n
I was just picturing that you could make it so that
StyleSheet
is private to the object passed as
this
inside
makeStyle
that way there would be no other way to actually declare a child except through
makeStyle
, and therefore no way to avoid the post-init
but yeah, seems fairly similar to what you're suggesting (though I'd need another minute to grok it fully)
y
I don't think that making a class private would allow an outside user to subclass it lol at least AFAIK
Maybe it could become an inner class then which would have a similar effect tbh
n
hmm but an inner class would still be accessible
yeah you're probably right, I didn't think it through very carefully
but I'm confident there's some way to restrict things so that
object : StyleSheet
will ultimately only work inside the lambda passed to
makeStyle
I'm more interested though to understand why this doesn't work; having spoken to Marc here before I'm pretty sure he would have considered this and there must be a reason he doesn't like it
m
Hey guys. Thanks for the discussion and feedback. I haven’t thought about using the anonymous object as a generic return type. I didn’t even know that’s possible. It’s an interesting approach that’s worth exploring in more depth. That would indeed allow moving all DSL logic away from the
StyleSheet
class. That class can then either be removed or serve as a marker (e.g. an
external interface
in KJS).
One downside is that it adds another layer of nesting/indentation but I think that’s okay for the benefit.
n
yeah, I thought about the extra indentation. Not ideal, but not terrible I think, probably still better than using a post-derived-class-init hook in some ways
One thing about Kotlin is that even though it doesn't give a keyword to support arbitrary return type deduction (a la
auto
in C++), it effectively supports it because any function can be implemented using
= run { .... }
which does return type deduction
m
Yeah, I’ve just assumed (for whatever reason) that anonymous objects get inferred to their supertype when used in a generic context. E.g.
object : StyleSheet
to
StyleSheet
. The UI even indicates it like that. But because it maintains the anonymous type that’s quite useful.
👍 1
n
I'll add this to my list of Kotlin DSL tricks
hadn't messed around much with anonymous objects yet