is it possible to create a scope, where inside the...
# getting-started
y
is it possible to create a scope, where inside the scope, existing functions are overridden? for example, a scope where inside the scope,
Any?.toString()
is overriden by my own custom function.
e
doesn't that already work? https://pl.kotl.in/QZMtItb7C
y
that's interesting! how do I get something like this to work, though? https://pl.kotl.in/WuuJ2HtQ6 (it prints "fizzbuzz" and not "hello fizzbuzz")
e
ah that doesn't work because
plus
isn't an extension, it's a member function, and member functions can't be shadowed by extensions
y
I see. and what is it that prevents overriding the
toString()
implementation used by String templates? because it's a compiler builtin?
y
as always, thanks a lot
y
Interestingly there's an internal compiler annotation
@HidesMembers
which is meant to do exactly this, but the issue is that it's limited to only
forEach
and
Throwable.addSuppressed
. I've experimented a little bit with making a compiler plugin that allows it for every function, but it seems that the compiler is a little bit resistant to that.
d
@HidesMembers
is a dirty hack which was introduced specially for
forEach
function The problem with that function is performance: ususually resolution stops when first appropriate function was found (and in most cases it happens very early in local or member scope). But if there might be a declaration with
@HidesMembers
then compiler should always to lookup all scopes for each function call/property access, because it's impossible to say if there is a really some extensions with
@HidesMembers
looking to only member declaration So to support
forEach
we have a simple hacky code like that in resolution algorithm:
if (foundCandidate && name != "forEach") stopResolution()
y
Could there potentially be a case for plugins to add custom names that should be considered for
@HidesMembers
then? Note that that is currently possible right now because the "constant" hides members name list is just a Java Set, which can be added to. It's also good to note that, from some experimentation,
@HidesMembers
deals just fine with context receivers (and so e.g. you can define an extension function with context receivers that only hides members if those context receivers are satisfied) which allows for replacing implementations of methods in very specific contexts (inside of a specific class declaration, or within a DSL). It feels like there should be some (opt-in, of course) mechanism to make a context receiver take priority in determining an overload, and therefore making an extension win against a member. This mechanism can be approximated through hacking
HidesMembers
right now, but could it be supported through more official means? I know that resolution rules are nearly sacred for Kotlin, and for very good reasons, but the case of contextual extension vs non-contextual member seems to have some room for interpretation, especially when you consider that a contextual extension always wins over a non-contextual extension. It seems as if the line between member and extension is quite blurry, at least from the eyes of someone consuming a library.
d
No, it couldn't
@HidesMembers
is a hack we are forced to support, not a feature we want to propagate And it's unwanted to spread its usage
Also I want to highlight that possibility of presence declarations with
@HidesMembers
breaks the general intuition how is resolution algorithm works
y
sorry to change to subject, but I just realized @ephemient's solution above (https://pl.kotl.in/QZMtItb7C) seems to work only with
null
. how come?
d
null
has type
Nothing?
, and
Nothing
is a special type with more-or-less empty scope
y
That's because the "real"
toString()
is defined inside
Any
, and so it doesn't apply for nulls. Kotlin also has an extension
Any?.toString()
which hence gets resolved for null calls, but because it is an extension, it is beaten by the member-extension inside Foo
y
okay. but then, why can I not beat the "real"
toString()
with a more specific one inside
Foo
, like
Int.toString()
?
y
Because the real
toString()
is a member
d
Member scope of explicit receiver has the top priority See relevant chapter in specification
y
oh, right! @Youssef Shoaib [MOD]
y
This is part of the issue though. The behavior can be unintuitive sometimes, even accidentally. Take this rather contrived example (Playground):
Copy code
object Foo {
    fun Any?.toString(): String = "Foo.$this"
}

class Test(val value: String? = "hello")

fun main() {
    println("Local Module:")
    printTest(Test())
    println("\nDifferent Module:")
    printPairFirst("hello" to Unit)
}

// Test is defined in the same module, so smart casting its `value` is safe
fun printTest(test: Test) = with(Foo) {
    if(test.value != null) {
        val canBeSmartCasted: String = test.value // Uncomment to see that smartcast is safe
        println(test.value.toString()) // Prints without Foo.
    }
    println(test.value.toString()) // Prints with Foo.
}

// Pair isn't defined in the local module, so it can't be smart casted
fun printPairFirst(pair: Pair<String?, Unit>) = with(Foo) {
    if(pair.first != null) {
        // Error: Smart cast to 'String' is impossible, because 'pair.first' is a public API property declared in different module
        //val cannotBeSmartCasted: String = pair.first // Uncomment to see that smartcast isn't safe
        println(pair.first.toString()) // Prints with Foo.
    }
    println(pair.first.toString()) // Prints with Foo.
}
By moving
Test
outside of the module, the behavior of the code silently changes. If this code used a real-life function other than
toString
, where
Test.value
has type
T
, and perhaps the author behind the type
T
decides to add a member function
foo
, while an extension function
T?.foo
existed in some other code the consumer of the library had. The consumer of the library was well aware of the addition of member
foo
, and they verified that the behavior of their
T?.foo
makes sense. Now, if the consumer of the library decides to move
Test
out of their local module, all of a sudden their extension
T?.foo
is always called, instead of the member
foo
. I admit it is quite a contrived example, but it shows how seemingly trivial changes (like extracting declarations into a new module) can have silent behavior changes. Using the member as the top priority is objectively the right choice in most cases, but perhaps some user-override can help remedy cases were the behavior is well-defined but unintuitive. Alternatively, perhaps some warnings are needed here to warn about weak smart casts, extensions on supertypes where subtype members could shadow, etc.