```class ExtScope { fun Int.f() {} } fun extS...
# getting-started
m
Copy code
class ExtScope {
    fun Int.f() {}
}

fun extScope(block: (ExtScope) -> Unit) {/* ... */}
extScope { scope ->
    // How to reference `Int::f` here?
}
How do I reference the
f
extension method on
Int
in the
block
argument without using
with(scope)
and the like?
Copy code
override fun ExtensionRegistrarContext.configurePlugin() {
    (::MyFirAdditionalCheckersExtension).unaryPlus()
    (::MyFirDeclarationGenerationExtension).unaryPlus()
    this.registerDiagnosticContainers(MyFirAdditionalCheckersExtension.MyDiagnostics)
}
I want to replace this with the following
Copy code
override fun ExtensionRegistrarContext.configurePlugin() {
    val self = this@MyFirExtensionRegistrar
    val context = this@configurePlugin
    self.configurePluginImpl(context)
}

private fun configurePluginImpl(context: ExtensionRegistrarContext) {
    context.unaryPlus(::MyFirAdditionalCheckersExtension)
    context.unaryPlus(::MyFirDeclarationGenerationExtension)
    context.registerDiagnosticContainers(MyFirAdditionalCheckersExtension.MyDiagnostics)
}
y
If you mean reference them as in function references (i.e. using
::
), it's currently impossible sadly. Relevant issue Context parameters were a blocker for this, so now that they're here, it's at least possible design-wise
If you just mean calling them, then for your situation: 1. I think you're "going against the grain of Kotlin" to some extent. These DSLs are designed a certain way to result in nice syntax. Trying to add verbosity to them won't serve you well, and it's just an extra burden for someone (or you 5 years from now) reading your code. 2. You can still do this. Every DSL can be reshaped to your liking, and if not, then it can always be wrapped. Consider:
Copy code
fun ExtensionRegistrarContext.register(factory: (FirSession) -> FirAdditionalCheckersExtension) { +factory }
fun ExtensionRegistrarContext.register(factory: (FirSession) -> FirDeclarationGenerationExtension) { +factory }

// which allows you to write
private fun configurePluginImpl(context: ExtensionRegistrarContext) {
    context.register(::MyFirAdditionalCheckersExtension)
    context.register(::MyFirDeclarationGenerationExtension)
    context.registerDiagnosticContainers(MyFirAdditionalCheckersExtension.MyDiagnostics)
}
but you quickly run into another hurdle: you now need to duplicate all the couple dozen
unaryPlus
methods for each extension type because the original DSL intentionally has overloads for each of them. It's not the end of the world, but maybe a sign that you should try and embrace the implicitness of Kotlin and see where it leads you!
m
I’m sure there were specific problems that the design of the API had to solve and that shaped the DSL as a result. I wonder what those problems were, if you happen to be aware? If
FirExtensionRegistrar
declared and let subclasses inherit the following final method:
Copy code
fun register(extension: (FirSession) -> FirDeclarationGenerationExtension) {
    this.registerExtension(
        FirDeclarationGenerationExtension::class,
        FirDeclarationGenerationExtension.Factory(extension),
    )
}
My
override fun configurePlugin
could have used it like so:
Copy code
class MyFirExtensionRegistrar: FirExtensionRegistrar() {
    override fun configurePlugin() {
        this.register(::MyFirDeclarationGenerationExtension)
    }
}
Which I personally find has a cleaner signature, is better at explaining the semantics than
+::MyFirDeclarationGenerationExtension
, and involves fewer layers of indirection and more straightforward name resolution.
y
There are 2 interesting design choices here: 1- Extracting the registration methods to
ExtensionRegistrarContext
is there to limit any such calls to being in
configurePlugin
only. This limits the mutable state to the context as well, meaning that
configurePlugin
can thus be reused (e.g. in IDE to recreate your extensions when user changes happen). In reality, it actually gets cached as a lazy value. Regardless, the point is to limit these calls to that one method. It also means that if another plugin somehow gets its hands on your
FirExtensionRegistrar
, it can't somehow attribute its additions to you, and similarly it means that you can't add things to your registrar after the
configurePlugin
call happens. 2- Choosing
unaryPlus
instead of
register
or some other name: It's a matter of personal taste, but the point is to reduce visual noise so that your extensions are the most visible thing in the code Using
unaryPlus
for registration or "adding things to a container" is a Kotlin idiom, and has been for a while.
m
The first objective seems to remain unachieved to me, and I personally prefer methods to operators (eg
xs.sum() / xs.size
to
{(+⌿⍵)÷≢⍵}
) but the choices are interesting.
j
You mean you prefer
xs.sum().div(xs.size)
then? (I'm not weighing in on the general topic here, but I just found it funny that in your methods example, you actually use an operator)
m
1. I often call
operator
functions by name, especially to handle nullability (eg
x?.plus(y)
). 2. It’s ignorant at best and disingenuous at worst to equate a. The use of
/
for division, b. And the use of
+
on a
(FirSession) -> FirDeclarationGenerationExtension
which is enabled because the block receiver is an instance of
inner class ExtensionRegistrarContext
that eventually registers the factory with the registrar, which is not as standard as
6 / 2 == 3
and
+x == x
. I do not appreciate that you chimed in just to be snide and not add anything else to the general topic.
j
Oh sorry, it wasn't my intention! I forgot that text doesn't convey tone at all. It was really just meant as a light joke. The reason why I said I wasn't weighing in was exactly because my joke wasn't meant to state my opinion on the topic, since I mostly agree with you here. But if we're being serious about it, I don't think you should say you prefer methods to operators as a blanket statement, then, since you seem to define classes of cases where you find operators better (as far as I can tell). My personal position on this is that overloading operators should be done only when their meaning is unambiguous. Often, that means staying close to the original mathematical meaning, but not always. For instance,
Path.div
is usually clear to most people, and I think it's a gain to use
myDir / "foo.txt"
over
myDir.resolve("foo.txt")
.
m
No offence taken! You’re good.
But if we're being serious about it, I don't think you should say you prefer methods to operators as a blanket statement, then, since you seem to define classes of cases where you find operators better (as far as I can tell).
Why continue this semantic debate though? Do you also think that in order for somebody to prefer tea to coffee, it’s not enough to drink 10 times or even 100 times as much tea, because they should simply never have any coffee at all? The line I draw is BODMAS (PEMDAS) and some de facto standard notation such as collection access (
map[key]
) versus everything else. I reckon that’s enough for me to say I prefer methods to operators!
I think it's a gain to use
myDir / "foo.txt"
over
myDir.resolve("foo.txt")
.
I personally find
Path#dir
not worth bothering about. It requires parentheses in call chains (
(x.f().g() / y).h()
) which I find unergonomic. In places where that’s not an issue, it still doesn’t enable anything
#resolve
doesn’t, and only opens the door for unproductive pointless arguments about Clean Code such as this one. For what it’s worth, I’m of the opinion that there’s a clear difference between generalising division to your own types for example, and coopting the
/
symbol in disjoint semantic contexts.