I got a question regarding the current Kotlin 2.3....
# compiler
o
I got a question regarding the current Kotlin 2.3.20-Beta2 build: it seems to be more picky about overload resolutions than any previous version, which is currently breaking my code and my ideas completely. First example: I have a method in two variants, one "older API" using
Result<T>
as the result, the other using Arrow's Raise context using context parameters. It looks like this:
Copy code
context(_: Raise<NotFound>)
fun extractFrom(data: Any?): Any?

fun extractFrom(data: Any?): Result<Any?> =
    recover({
        Result.success(extractFrom(data))
    }) {
        Result.failure(NotFoundException(it.message))
    }
When switching from Kotlin 2.3.10 to 2.3.20-Beta2, I get this error:
Copy code
e: JsonPointer.kt:86:24 Overload resolution ambiguity between candidates:
context(_: Raise<NotFound>) fun extractFrom(data: Any?): Any?
fun extractFrom(data: Any?): Result<Any?>
This one, I could work around by changing one function's name. Not nice, but okay, I could. But WHY is the newer compiler not able to distinguish the two functions which do have different signatures? Plus: it all works in IntelliJ, it is just when compiling using gradle build that this fails! The next problem I get when working around this is even more tricky and gives me real headaches. I am deliberately overloading well-known Java functions with my own stubs to give users proper error messages. As an example, here I am "overwriting" `System.currentTimeMillis()`:
Copy code
object System {
    @Deprecated(ERROR_NOW_OUTSIDE_CONTEXT, level = ERROR)
    fun currentTimeMillis(): Long = error(ERROR_NOW_OUTSIDE_CONTEXT)

    /**
     * Returns the current time in milliseconds since the epoch or a fixed time in test environment.
     */
    context(_: TransformationTimeAccess)
    fun currentTimeMillis(): Long = instantNow.toEpochMilli()
}
Basically, in my environment users are NOT allowed to use import statements, so if somebody uses
System.currentTimeMillis()
outside of a context that is required, it deliberately fails with a compiler error, but when calling IN the context it's allowed, it will work. With Kotlin up to 2.3.10 this works just fine, with Kotlin 2.3.20-Beta2 I get this error:
Copy code
e: Overload resolution ambiguity between candidates:
fun currentTimeMillis(): Long
context(_: TransformationTimeAccess) fun currentTimeMillis(): Long
If this gets released like this, I would really be in deep trouble. WHY does the new compiler fail on this when the old ones worked? Thanks!
d
cc @kirillrakhman
k
I think it's https://youtrack.jetbrains.com/issue/KT-82579. cc @Alejandro Serrano.Mena
o
Should I raise an issue on this? This would really be breaking change for me...
d
It's normal to have breaking changes in experimental features.
o
Well, yes, but what is my workaround? What is the correct way to address this? This is what I mean.
Of course context parameters are experimental, but I cannot see any reason for this being problematic now. The situation is not ambiguous at all: if I am in a context, the more specific version should just be selected.
Copy code
fun foo() {}

context(_: Bar)
fun foo() {}

object Bar

fun main() {
    foo() // should select the first foo()

    context(Bar) {
        foo() // should select the second foo with context
    }
}
y
I believe this is a recent change from the explicit context argument KEEP. The context param keep now says:
§7.8 _(most specific candidate)_: When choosing the most specific candidate we follow the Kotlin specification. Context parameters play no role in this choice, unless they are explicitly given (see KEEP-448 for details).
While before it used to say:
* Candidates with context parameters are considered more specific than those without them.
* But there is no other prioritization coming from the length of the context parameter list or their types.
Changed in this commit
o
This is really bad
Why is this change necessary and how would I need to work around this? Why is a context parameter argument not considered?
So, in my case: I need two functions to have the same name. And for the user if they call a function with that name, the one WITH context must be chosen in the context, else the other.
k
In the general case, I would say drop the deprecated overload. Users will see the appropriate error when trying to call the contextual overload without the context. I'm not sure if that helps in your special case.
o
You're just showing me that you changed it, but I believe this is not good.
I cannot drop it. I want both functions to exist, but the first one explicitly throw an error on compilation.
This is in a DSL where the users actually do not know anything about Kotlin or context parameters....
y
FWIW, this works:
Copy code
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
fun foo() { println("no context") }

context(_: Bar)
fun foo() { println("context") }

object Bar

fun main() {
    foo() // no context

    context(Bar) {
        foo() // context
    }
}
Edit: even better:
Copy code
fun foo(unit: Unit = Unit) { println("no context") }

context(_: Bar)
fun foo() { println("context") }

object Bar

fun main() {
    foo() // no context

    context(Bar) {
        foo() // context
    }
}
o
Uff. Even more unsupported stuff solves a problem I did not have before... Why did you need to change this? For what reason?
ah, ok tricky thing with the Unit... 😉
I don't like to work around Kotlin, but at least I do not need suppressions...
What was the problem that you originally wanted to solve with the idea to NOT make context parameters act as more specific?
Because to me, they ARE indeed more specific.
y
FIrstly, I'm not affiliated with JetBrains, so I'm just giving references to what I remember for the sake of info. Secondly, IIRC, part of the reasoning is that such a rule was rather adhoc, and with explicit context arguments, you can explicitly choose the contextual overload. Ultimately, Kotlin has many ways to deprioritize a function over another, so I don't think we need contexts to add an extra deprioritization mechanism. You're writing a DSL (IMO), so you should be comfortable with using random langauge features to achieve what you want.
❤️ 1
k
There are multiple reasons for the change. 1. The rule only applied to 0 vs some but not to 1 vs 2 or the types of the context arguments. But the algorithm to make it work would be quadraticly scaling with the number of context arguments. 2. Overload selection works by the principle that the user can do something like an upcast, provide an explicit generic argument, etc to force the compiler to select a less specific overload. You can't do that with context arguments because you can't remove arguments from the scope.
y
I don't want to drown out the conversation, but I think it's important to not fall into an XY problem here The
unit: Unit
trick already fixes your solution, but I think some alternative solutions can be helpful, too: For your first problem, it sounds like you're transitioning from using
Result
to using Arrow. Hence, you can write a little wrapper that encapsulates your
recover
logic, and use that whenever you're in code that still relies on
Result
. You thus won't have 2 different methods anymore
OptIn
annotations can let you mark where things like
currentTimeMillis
are used, so you can easily find them You can remove the
@Deprecated
overload, and instead rely on the fact that the compiler will already report a
NO_CONTEXT_ARGUMENT
error for
TransformationTimeAccess
. I understand your users may be non technical, but teaching them that
No context argument for TransformationTimeAccess found
means "you can't use time-based APIs without having TransformationTimeAccess in scope" sounds pretty doable to me. If you explain the concept of a "context" to them, and explain what
TransformationTimeAccess
lets you do, then the error message is very clear. Maybe the real feature request here is to allow for a custom message to be reported when a context is not found in scope (similar to what OptIn allows you to do). Scala has something similar with their
implicitNotFound
annotation
o
@Youssef Shoaib [MOD] thank you so much for all your answers! Helping a lot and much appreciated! Yeah, you're completely right, maybe I just need a custom error message. My users don't really know what a context is, they are supposed to learn just my DSL, but as it allows custom code blocks in pure Kotlin, in the past they've done quite some scary Java/Kotlin programming especially regarding time handling that I now pull these tricks to give them error messages that even include links to our documentation, like in this case, don't use currentTimeMillis(). So l will probably pull the trick with with the unit argument after all. I was just really scared when I saw my project does not compile anymore with 2.3.20... 🙈
y
I completely sympathize as a DSL lover myself. I think it's reasonable to want to provide a custom error message, but thus it's important to understand that you're pushing the language, so you should be fine with using strange tricks to achieve your design. There are other ways, btw, to deprioritize one member against another (e.g. type parameters). Knowledge of such tricks is crucial to get the "perfect DSL", and it's great that Kotlin allows for such tricks in the first place (I think they're even documented, so they won't break, but don't take my word for it). Finally, I think the hammer of the internal annotations (like
LowPriorityInOverloadResolution
) is worth considering sometimes. I wish these annotations were available with an
OptIn
or something. Even with the ugly hacky `Suppression`s, I think they're worth it as a last resort.
o
Yeah, I already have same "strange" function signatures and also still some open issues that I couldn't figure out on how to make it work yet. Your "hammer" internal annotation, how did you learn about it?
y
Initially, I've seen chatter about it, especially since the stdlib uses some of those internal annotations (e.g
@OnlyInputTypes
to have a reasonable
assertEquals
). Then, I look often at the file they're defined in when I'm at my wits end with a DSL design problem. I've also been messing around with compiler plugins, and while debugging, I end up seeing references to annotations like
@Exact
and
@HidesMembers
, with interesting code paths in compiler code. Mostly, though, it's just trial and error. The annotations aren't super self-explanatory, so I have some approximate ideas on when they might be helpful, but I just have to try them and see if they work
a
I think that Kirill's message describes quite good the reasoning behind the change. The general rule is that we don't want to make a choice of overload without no recourse to choosing the other one (which exists for almost every other choice -- receiver, types, suspend...). Let me just add that 2.4 will bring explicit context arguments, allowing to explicitly chose overloads by stating the name of the context argument.
p
Is that rule not already broken by extension receivers?
Copy code
fun String.greet() = println("Hello, $this!")
fun greet() = println("Hello there!")
There's no way to call
greet()
once you have a
String
in scope as a receiver. I can't help but feel that this will limit the usability of context parameters (by requiring the crutch of explicit context arguments at the callsite to disambiguate) or result in overly verbose function naming to avoid conflicts. Would a general anti-scope function be more useful - some way to enforce explicit references/context from the outer scope:
Copy code
with("Kotlin") {
    greet() // no way to call (1)

    explicitScope {
        greet() // calls greet()
        this@with.greet() // explicitly call String.greet()
        context(contextOf<String>()) { greet() } // explicitly (re)declare context
    }
}
d
There's no way to call greet() once you have a String in scope as a receiver.
Copy code
my.package.name.greet()
1
👍 1
y
Note that fully-qualifying with the package name won't work if your function is defined in the top-level package. Fully qualifying wouldn't work in the context and no-context overload situation (if they're in the same package)
p
@dmitriy.novozhilov Thank you, I stand corrected 🙂 Although it's not exactly ergonomic for readability. Wouldn't an import alias work for disambiguating context and no-context (same for extension I assume). I just realised the alias would apply to both overloads 🤦‍♂️
y
I think import alias and FQName share exactly the same disambiguation power. If we have context and no-context in the same package with the same name, they thus have the same FQName, so the alias would just alias both of them. One thing you can do, which throws a wrench in all of this, is write your own "alias"es". Imagine we have some
foo
with a context and non-context overload. If we're inside the context, not all hope is lost for calling non-context `foo`: you can call some
foo2
that itself calls non-context foo. So, ultimately, you're not 100% prevented from calling something simply because of the scope you're in, since you can always "recreate" your scope but with something (receiver, context) removed. However, it's a lot of boilerplate, so I understand why it's a good rule of thumb to either error with ambiguity or have some way to choose a lower-priority overload
p
We have a few utilities where we have overloads for with or without a
Raise
context, i.e.
Copy code
context(_: Raise<MalformedId>)
fun String.parseId(): Id = ensureNotNull(toIntOrNull()) { MalformedId(this) }

fun String.parseId(): Either<MalformedId, Id> = either { parseId() }
Which this change is going to impact. In this case it's very unlikely to want to use the no-context overload when the context is available. We also had similar overloads with with or without
context(json: Json)
with similar intent (although these also had other context parameters and stopped working recently due to context shadowing restrictions.
👍 1
o
@phldavies same here in my code. Some overloads are there with and without Raise context just to allow both...
y
The
unit: Unit
trick, mentioned earlier in the thread, would serve you well, then. Personally, I think you should get rid of the
Either
overload. Instead, wrap your calls with
either { }
when you need to. It then makes it easier to find places where you should refactor your code to use
Raise
more extensively. It's a lot easier for me, personally, to reason about code that looks like
either { foo.parseId() }.flatMap { either { it.toString().parseId() } }
and understand that it's equivalent to
either { foo.parseId().toString().parseId() }
than code like
foo.parseId().flatMap { it.toString().parseId() }
because the latter goes through
Either
in a way that's hidden away from me. Such transformations can absolutely be automated (e.g. using structural find and replace), so it can make refactoring even easier (good luck doing that with the multiple-overloads version without accidentally breaking code)
Copy code
context(_: Raise<MalformedId>)
fun String.parseId(): Id = ensureNotNull(toIntOrNull()) { MalformedId(this) }

fun String.parseId(unit: Unit = Unit): Either<MalformedId, Id> = either { parseId() }
The no-context overload has lower priority as a result
o
Now that Kotlin 2.3.20 has been released and I tried to use it in more projects, this change in selection of overload methods regarding the context is really breaking a LOT of code. Mainly in my Arrow usage where standard imports often include
import arrow.core.*
and recently
import arrow.core.raise.context.*
for using
Raise
with contexts. The problem now is that Arrow provides most of its methods twice: one with using extension style (in arrow.core) and another one with using
context(r: Raise<..>)
in arrow.core.raise.context.. It requires a lot of tweaking now to convince the compiler to chose the correct one. Even worse: all of these errors are not shown in IntelliJ (yet?). Everything looks fine, but when doing a gradle build, it fails. @Alejandro Serrano.Mena should know a lot about this problem... 😉
p
I think the expectation is to use one set of APIs or the other, however it's not always possible, especially when integrating with existing code that uses receiver-raise
o
Yeah. sure. But that is not possible the way the packages are structured! Importing the "base" elements using
import arrow.core.*
made a lot of sense and mostly this was done automatically as of the star import rule. Adding the newer style context lambdas in its own package now leads to this problem.
As an example: mapping and accumulating errors over an iterable: if you have an
import arrow.core.*
it will always select
Copy code
public inline fun <Error, A, B> Iterable<A>.mapOrAccumulate(
  @BuilderInference transform: RaiseAccumulate<Error>.(A) -> B,
): Either<NonEmptyList<Error>, List<B>> = either {
  mapOrAccumulate(this@mapOrAccumulate, transform)
}
in
Iterable.kt
. But this automatically creates an
Either
- which is what I do not want any more. I want to "stay in Raise-land" and use the newer
Copy code
context(raise: Raise<NonEmptyList<Error>>)
@RaiseDSL public inline fun <Error, A, B> Iterable<A>.mapOrAccumulate(
  @BuilderInference transform: context(RaiseAccumulate<Error>) (A) -> B
): List<B> = raise.mapOrAccumulateExt(this, transform)
defined in
RaiseAccumulateNelContext.kt
in package
arrow.core.raise.context
. This is awkward now and yes, @phldavies, I would like to do one OR the other, but since many functions are defined at top level, there is not a good way to chose one over the other.
For anything up to Kotlin 2.3.10 this worked fine, even though I had the imports, because of that said change. Now it all breaks.
k
Does using an explicit import help here?
o
You cannot explicitly "unimport", @kirillrakhman. So once code clean up actions do the star import, it's all lost! The only way to work around this is to be VERY cautious about any imports and yes, manually import dozens of single elements from
arrow.core
if you need them.
In any bigger, real world project that uses Arrow with star imports, you have to fine tune all of the imports now AND this is VERY tedious and currently, due to the lack of IDE support, you don't even see the errors in IntelliJ.... 😞
k
Let's try to distinguish between compiler problems and IDE problems. If I understand correctly, the compiler problem can be solved with an explicit import. But it seams to lead to an IDE problem with import optimizer.
You said that you don't see the error in the IDE, this means that the analyzer version in the IDE is older than the compiler you're using. Can you share the project or a reproducer, so I can test on a Nightly verison of the IDE?
o
Yes, correct. The change in overload resolution, while meant to be a good idea, makes it hard in real world projects with many star imports that used to work fine prior to 2.3.20.
I am using the most current version of everything. 😉
k
It's expected that IDE support for experimental features is a bit slow to catch up.
o
I think @Alejandro Serrano.Mena knows exactly what I am talking about as he knows the Arrow lib very well. The compiler errors can be worked around by only doing single imports, I guess. There might also be problems with member functions of types like
Either
that come with extension style. If you now want to use contexts, I don't know whether importing the new extension function with context style really works.
@kirillrakhman I know. I don't expect this to work immediately. That's not the point. I just wanted to point out the issues that this change now shows.
k
Does this snippet somewhat represent your scenario?
Copy code
import arrow.core.*
import arrow.core.raise.context.Raise
import arrow.core.raise.context.mapOrAccumulate

context(_: Raise<NonEmptyList<Throwable>>)
fun testContextual(x: Iterable<String>) {
    x.mapOrAccumulate { it }
}

fun testWithoutContext(x: Iterable<String>) {
    x.mapOrAccumulate {
        if ("false".toBoolean()) raise(Exception())
        it
    }
}
On my Nightly version of IJ IDEA, import optimizer doesn't remove the explicit import here.
o
Here's a real world example: coming from these imports:
Copy code
import arrow.core.*
import arrow.core.raise.context.*
import arrow.core.raise.recover
everything used to work with Kotlin 2.3.10 Switching to Kotlin 2.3.20 this leads to compile errors (not shown in IDE):
Copy code
e: file:///Calculator.kt:238:50 Overload resolution ambiguity between candidates:
fun <Error, A, B> Iterable<A>.mapOrAccumulate(transform: RaiseAccumulate<Error>.(A) -> B): Either<NonEmptyList<Error>, List<B>>
context(raise: Raise<NonEmptyList<Error>>)
fun <Error, A, B> Iterable<A>.mapOrAccumulate(transform: context(RaiseAccumulate<Error>) (A) -> B): List<B>
The fix for this is to rework the imports to those:
Copy code
import arrow.core.*
import arrow.core.raise.Raise
import arrow.core.raise.context.bind
import arrow.core.raise.context.either
import arrow.core.raise.context.ensure
import arrow.core.raise.context.mapOrAccumulate
import arrow.core.raise.context.withError
import arrow.core.raise.recover
But when I do my clean up (Option-Cmd-L), IntelliJ does this automatically:
Copy code
import arrow.core.*
import arrow.core.raise.Raise
import arrow.core.raise.context.*
import arrow.core.raise.recover
and this breaks it again. 😞
I simply get very annoyed if any IDE action that is supposed to help (clean up) breaks code. This is the only thing that any refactoring tool must never do.
k
Understandable. Still, you're dealing with a version mismatch between IDE analyzer and compiler, so it's expected to see some problems, especially with experimental features.
o
By the way, doing these imports:
Copy code
import arrow.core.*
import arrow.core.raise.Raise
import arrow.core.raise.context.*
import arrow.core.raise.context.mapOrAccumulate
Also fix the issue for the compiler - but cleaning up imports thinks "oh, there's a star import on context, let's remove the explicit one for the function". This seems to be a problem. The IDE tools do not know that this explicit import does something "special". They look at this as I also would have done: "it's all imported, so let's remove the unnecessary ones".
@kirillrakhman I am not so sure. Check if your version of the IDE leaves the extra import on top of my star import!
If it does not... that's broken as well. Because for the compiler doing a star import PLUS an extra fun import does mean something special now.
k
It leaves the import, yes. And it's not broken. This is intended behavior, see the spec: > Star-imports import all named entities inside the corresponding scope, but have lesser priority during overload resolution of functions and properties. https://kotlinlang.org/spec/packages-and-imports.html
👀 1
o
If this is the spec, why does my latest IntelliJ version remove my explicit import? Or has this spec been changed for Kotlin 2.3.20 but IntelliJ has not adopted this yet?
k
Because your IDE version doesn't know about the new rule introduced in 2.3.20.
👍 1
o
Ok. Then at least it's fair to say the timing is suboptimal. 😉 When will IntelliJ support 2.3.20? I was under the impression that the language support can be switched as soon as a newer version of Kotlin is available through plugins...
k
It should be in 2026.1 which is already in EAP.
❤️ 1
o
@kirillrakhman Good morning, Kirill! This morning, I wanted to try out 2026.1, but here's another catch: 2026.1 marks my code invalid even though my project still uses Kotlin 2.3.10 where the overload resolution is still taking context receviers into account. This is kind of a very bad mix right now: users will always have a mix of projects and having to chose the right IDE for the right project to open it is really something I have not seen before. Do I see this correctly: 2026.1 uses the new rules, independent of the project's Kotlin version?
Even though I could technically work my way through this mix by opening specific projects in their "dedicated" IDE, in a team setup this becomes messy quite quickly. I could clean up my imports now for the "new situation", but as soon as a colleague who does not use EAP releases of IntelliJ touches the file and does an auto import fix before committing, the imports will be broken again.
k
Unfortunately, I don't know of a good workaround except deciding on a fixed combination of compiler + IDE version inside the team. With experimental features, we don't provide the same compatibility guarantees as with stable ones. But context parameters are planned to become stable in 2.4.
o
Yeah, I know. Technically all of this is about Context Receivers, I just felt that if Kotlin version A has a different way of treating imports (just in general, not looking at context receivers), the changes should take into account how IntelliJ can evolve around this smoothly. I just thought that generally, the IDE "knows" more about the version of Kotlin used in each project and acts accordingly...
k
In general, it does. When we change something in the language, it's always done under a flag that's controlled by the language version. This way, you can use a new IDE to open an older project and see the same errors in the IDE as an old compiler produces. The only exception is experimental features kodee sad
m
I am nearing completion of a IEEE754-2019 decimal floating point implementation for KMP. Context parameters provided a seemingly-ideal solution ... up thru 2.3.10 Without a context parameter the arithmetic operators used a Thread-global context to provide parameters/preferences/flags
Copy code
val a = b + c * d
Advanced users who need rounding control (for example) could perform arithmetic operations within a context parameter.
Copy code
with(mySpecializedContext) {
  val a = b + c * d
}
.OR.
mySpecializedContext.context {
  val t = (w * x) / (y + z)
}
where:
fun <T> DecContext.context(block: context(DecContext) () -> T): T = block(this)
As of 2.3.20 this does not work because of 'overload resolution ambiguity' Where is the ambiguity? Is the ability to have a context-parameter-implementation and a bare-implementation of operator funs really going away?
y
Copy code
fun main() {
    MyFloat() + MyFloat() // called without context
    with(MyContext) { MyFloat() + MyFloat() } // called with context
}
object MyContext

class MyFloat

operator fun <T: MyFloat> T.plus(other: T) = println("called without context")

context(_: MyContext)
operator fun MyFloat.plus(other: MyFloat) = println("called with context")
the type parameter makes the context-less overload have a lower priority in overload resolution. Thus, the compiler tries the context overload first, and if it fails, tries the context-less one, which is exactly what you want. There's a different trick for non-operator functions, namely adding dummy default parameters. You can do some magic to get it working for operators, though:
Copy code
fun main() {
    MyFloat() + MyFloat() // called without context
    with(MyContext) { MyFloat() + MyFloat() } // called with context
}
object MyContext

class MyFloat

@Suppress("INAPPLICABLE_OPERATOR_MODIFIER")
operator fun MyFloat.plus(other: MyFloat, unit: Unit = Unit) = println("called without context")

context(_: MyContext)
operator fun MyFloat.plus(other: MyFloat) = println("called with context")
There are other tricks you can use here. These tricks have been known about and used way before contexts were ever introduced. As a result, I personally don't mind this new ambiguity change, since I have tricks that can get me the old behavior back when I want it
p
I can't help but feel this ambiguity change is detrimental to Kotlin's design goal of being pragmatic and not getting in the way of developers. Maybe it's just my mental model but I find the ambiguity confusing as there is evidently a more specific applicable function that should be resolved in these cases (where context is available). The proposed solution to this ambiguity is explicitly passing context as named arguments at every callsite (not yet available) which ultimately makes the contextual overload worth less (not worthless) as it should just be declared with regular parameters (which wouldn't work for operators). This doesn't however provide a mechanism for calling the non-contextual overload without requiring a bridging function. It seems like the suggestion is to just avoid having both contextual and non-contextual overloads in your API, which feels restrictive. I'd love to be proven otherwise, but I foresee the majority of cases intending to call the more-specific contextual function over the non-contextual. Being unable to invoke the non-contextual function when the context is available can likely be worked around with local bridging functions to cover those edge cases, or providing the ability to explicitly define or restrict receiver/context scope for a block. API designers feeling the need to rely on tricks with unused params/type-params or internal annotations to coerce resolution to match the arguably clearer intent feels inherently broken.
👍 1
o
And I feel that the newest change to how imports work is also just a patch on top of a real problem. I don't like controlling which method is chosen by adding imports as this also is not very specific, but a "global patch" on file level. I already feel the pain with this with my code base being incompatible with the latest IntelliJ IDE due to how it does auto imports... 😞
k
And I feel that the newest change to how imports work
This rule is not new
o
Uff. I thought it was because if it's not new, then all IntelliJ auto import action are misbehaving as they always throw out my deliberate imports..
p
I think the import rule is just more apparent now with the recent ambiguity change of contextual functions, no?
o
I don't know. If it has always had this meaning, why does IntelliJ kill import statements that have an effect?
p
I can't speak to the auto-import functionality as I've been blocking star imports for years now 😉
🤣 1
a
I can't help but feel this ambiguity change is detrimental to Kotlin's design goal of being pragmatic and not getting in the way of developers.
Actually, it's the opposite. With the new rule, you don't need to keep tracking in your head what it's and what it's not in scope, and that it's a huge win for those using those libraries, at the expense of the API author having to come up with different names for contextual vs. non-contextual operations.
p
I understand where you're coming from there. The counter-argument would be you'd likely need to track what is and isn't in scope in order to aptly decide which operation to call instead, and hoping you've not accidentally called the non-contextual operation when context was available and should have been used. I'm also not certain requiring naming to disambiguate won't lead to some sort of
blahContextually
suffix a la
coEvery
or
everySuspend
which becomes general noise. It also wouldn't work as a solution for operators (as with the float library discussed previously in this thread).
o
My biggest problem is as this is all in DSL code, I don't want to introduce different names because users of the DSL should not bother at all. I used contextual overloads a lot to direct to the correct function...
k
IntelliJ auto import action are misbehaving
This is exclusively due to a version mismatch between the compiler and the IDE. With experimental features, you can only expect reasonable IDE behavior by using a compatible version of the IDE and as I wrote earlier, you need to use 2026.1 with 2.3.20.
o
We are now talking about the imports and you said the rule is not new...
k
Import optimization is a complex process that takes into account the resolution rules. Therefore, changes to the resolution rules can lead to changes in import optimization.
o
I am sorry but I really get confused about what causes what. 🙂 It's a very complex situation. I feel I understand much better now how difficult it is to evolve a language and not cause friction.. JetBrains way to do this is already really, really good. But sometimes it's hard.
As to the overload resolution not taking contextual parameters into account, I think that one might be REALLY challenging to do right, just as Phil pointed out. I also feel this is a step back to what I have done before. And, for DSLs, it's not as easy - you cannot write differently named functions instead of overloads.
y
Here's my 2 cents on this situation: • The import issue is simply because an older IDE version doesn't understand the new ambiguity rules introduced in a newer Kotlin version. For stable features, I believe the team gets around this by having an early preview of such new rules under a compiler flag (which gets enabled by default in some new Kotlin version), and so an old IDE picks up on the fact that you're (implicitly) using that compiler flag, and thus has an okay mental model of ambiguity rules. This wasn't done here since contexts are still unstable. • It sounds to me that the main gripe with the ambiguity change is for DSL usage (since in non-DSL code, you can just disambiguate by name). I have a firm belief that, if you're writing a DSL, you need to get comfortable with tricks (because a DSL is at the edge of what the language supports). The options I've found so far (dummy params, type params) seem sufficient for a lot of use cases. I think the friction here is due to the transition into a new ambiguity rule. If contexts behaved like this from the start, I doubt there would've been many complaints.
o
Plus my (very special) case that I try to write a DSL that interferes a lot with Java defined methods that I want to overload in my contexts to make it look like Java method but in reality use my overload function. Thus my options to invent dummy params and type params are limited even more.
y
In your case, you don't want the Java methods to ever be called directly though, right? If so, I'd have some linter to prohibit importing them, and then there'll never be a situation where ambiguity arises
o
OH yes. My end goal would be to be able to control exactly what my script is able to call. But up to today, I don't see an easy way to do that. I need a very specific way for Kotlin Scripting to disallow things that should not be used.
I tried a lot, even with parsing the script text and disallowing import statements, but this is hell. There are so many different ways to trick my very rudimentary regex to detect the wrong function call. That's where the limit is quickly reached.
I think I would need to inspect the byte code and find out if someone tries to bypass my security checks.
y
Based on the example you initially gave, though, I don't see why this wouldn't work:
Copy code
object System {
    @Deprecated(ERROR_NOW_OUTSIDE_CONTEXT, level = ERROR)
    fun currentTimeMillis(unit: Unit = Unit): Long = error(ERROR_NOW_OUTSIDE_CONTEXT)

    context(_: TransformationTimeAccess)
    fun currentTimeMillis(): Long = instantNow.toEpochMilli()
}
Your initial strategy didn't give errors when using
java.lang.System
anyway, so this isn't much different. The exact signature of Java methods thus seems irrelevant, since the whole point is that you're shadowing them in Kotlin and using error deprecation. Perhaps I am mistaken, though!
o
No, you're right, in my current situation I can still work around this as before. I just had to learn why it didn't work. My problems with this new resolution strategy basically then surfaced on a completely different area with Arrow and its Raise context parameters vs Extension lambdas. It just all felt wrong from 2.3.10 to 2.3.20... plus: I am always behind because of CodeQL not being able to use the latest Kotlin version, plus now bugs in 2.3.20 in scripting that make me wait for 2.3.21... and now with IntelliJ wanting to update I cannot really find a great way to escape all of these at the same time... 😄 I know: first world problems!