Assume the code below is provided by a 3rd party library:
class GraphNode(val value: Int, val neighbors: List<GraphNode> = emptyList()) {
fun performComplexAlgorithmAndPerformActionOnEachVisitedNode(action: (visitedNode: GraphNode) -> Unit) {
// Assume the logic is very complex with a lot of code here
action(this)
neighbors.forEach { node -> node.performComplexAlgorithmAndPerformActionOnEachVisitedNode(action) }
}
}
Because the code is from a 3rd party library,
we cannot change it to inline function.
we cannot change the return type of action lambda and the logic to cancel the computation.
Even if we are the owner of the lib, the function is too large with a lot of code, we donât want to use inline function.
Assume we have a complex graph with thousands or even more nodes, so we want to break the computation immediately when a condition is matched.
fun main() {
val rootNode = GraphNode(0, (1..10000).map { GraphNode(it) })
rootNode.performComplexAlgorithmAndPerformActionOnEachVisitedNode {
print(it.value)
// TODO: how to break if it.value >= 2
}
println("END")
}
Expected output:
012
END
My ugly workaround 1
fun main() {
val rootNode = GraphNode(0, (1..10000).map { GraphNode(it) })
runBlocking {
launch {
rootNode.performComplexAlgorithmAndPerformActionOnEachVisitedNode {
print(it.value)
if (it.value > 1) throw CancellationException()
}
}
}
println("END")
}
The workaround is ugly, because
it forces to add kotlinx.coroutines dependency to the current code. If we are writing a library, we donât want to add overhead if we donât need to.
itâs over complex for a common and simple problem
code is added is unrelated to the original problem. Why on earth, do we need concurrency (runBlocking and launch) just to break a normal method call?
it doesnât work well in case the method performComplexAlgorithmAndPerformActionOnEachVisitedNode want to return a value. Of course, we can have a more complex logic to handle the return value, but as I mentioned, itâs over complex to do that with this workaround.
My ugly workaround 2
Introduce a my own new exception and use try-catch for this exception.
Conclusion
This is a very common problem, Kotlin should provide an elegant way to return and break the computation for this case.
I know about that error, didnât notice it applies to this situation, sorry .That makes sense though, allowing a non-local return is ugly itself. I donât think âIntroduce a my own new exception and use try-catch for this exception.â is an ugly workaround, just a solution to that problem. But maybe thatâs just me.
I think we should somehow learn the way we cancel a coroutine job for this case. I expect this problem should be natively supported with a standard approach in Kotlin, easy and simple for developers without inventing their own extra stuffs. It should be simple and ready to use like when we want to break a loop or cancel a coroutine job.
Ah! Thanks, I did miss that somehow Iâll think about it more.
One quick idea is to wrap the function and create your own logic for skipping further processing. That would allow you to change the return type or add lambda member functions and skip the meat of the logic on further nodes.
On mobile now, otherwise Iâd type an example.
EDIT:
Even if you couldnât use extension functions to make the API appear clean for some reason (for example of youâre trying to solve this in java instead of Kotlin), I think there will be solutions within the current language feature set. If a language change is needed, weâll at least need to gather the current solutions to compare alternatives.
If I understood your case correctly, then what you try to do here is to force 3rd party code to do what it was not designed to do. Your case is not possible to do without some weird hacks, because performComplexAlgorithmAndPerformActionOnEachVisitedNode just doesnât support finishing early. Kotlin canât and shouldnât really fix limitations in APIs of various libraries.
I think @broot hit the bullâs eye here. Even though youâre working with code defined externally, most of the time we customize it by wrapping it with our tooling. This is especially true when using a Java library from Kotlinâjust like how youâll usually define a handful of functions and wrappers to adapt the API into something easier to work with in Kotlin, we can do the same here.
Basically, since weâre not limited to the library API, pick a new API we prefer and adapt it.
Hereâs a quick runnable example of returning a boolean to continue processing nodes.
//sampleStart
fun main() {
val rootNode = GraphNode(0, (1..10).map { GraphNode(it) })
rootNode.visitNodes {
print(it.value)
return@visitNodes (it.value <= 2)
}
println("END")
}
//sampleEnd
fun GraphNode.visitNodes(action: (visitedNode: GraphNode) -> Boolean) {
var continueProcessing = true
this.performComplexAlgorithmAndPerformActionOnEachVisitedNode {
if (continueProcessing) {
continueProcessing = action(it)
}
}
}
// ---
// Assume this is an external library and we CANNOT change it.
class GraphNode(val value: Int, val neighbors: List<GraphNode> = emptyList()) {
fun performComplexAlgorithmAndPerformActionOnEachVisitedNode(action: (visitedNode: GraphNode) -> Unit) {
// Assume the logic is very complex with a lot of code here
action(this)
neighbors.forEach { node -> node.performComplexAlgorithmAndPerformActionOnEachVisitedNode(action) }
}
}
I might even say you could use the custom exception trick to jump out of processing further nodes in some cases. Of course, youâll want to make sure the library behaves doing that.
Your case here is really: foo() invokes bar() which invokes baz(). Now you want to somehow jump out of baz() straight to foo(), with skipping whatever code was in bar(). I donât know even a single language that allows this outside of exceptions.
Only because the code of the lambda is âvisually closeâ to the code inside main() in the source file, doesnât mean these two code blocks are close to each other at runtime. In practice, they could run at totally different execution contexts, in different threads or even at a different time.
This request would likely cause lots of issues and be a code smell to use. The reason the exception handling to break out of some code is hacky is that weâre using it to perform a jump. Doing a jump out of a libraryâs code should look uglyâitâs a good thing that it stands out.
At least with exceptions, the library has the option to catch it before you and perform handling on itâa pure jump out would skip this option and the library wouldnât have an option to wrap up its control flow as it does with exceptions.
For this reason, I think your best option is to perform the jump is to use a custom exception (for the clarity that you intend to use it as an exit condition), wrap the ugliness in a function so you donât see it, and create a bunch of tests to confirm the library is able to handle early exiting well.
EDIT:
Hereâs the runnable version using an exception. If you do something like this Iâd recommend locking down the visibility of this code and heavily documenting itâs intended use, maybe change the name of the custom visitNodes function to mention its exceptional behavior.
//sampleStart
fun main() {
val rootNode = GraphNode(0, (1..10).map { GraphNode(it) })
rootNode.visitNodes {
print(it.value)
if (it.value > 1) haltProcessing()
}
println("END")
}
//sampleEnd
fun GraphNode.visitNodes(action: GraphNodeAction.(visitedNode: GraphNode) -> Unit) {
try {
this.performComplexAlgorithmAndPerformActionOnEachVisitedNode {
GraphNodeAction().action(it)
}
} catch (e: HaltProcessingException) {
// Ignored since we use this exception to exit.
}
}
class GraphNodeAction {
fun haltProcessing(): Nothing = throw HaltProcessingException()
}
// Private to help contain the usage of this type.
private class HaltProcessingException : Exception()
// ---
// Assume this is an external library and we CANNOT change it.
class GraphNode(val value: Int, val neighbors: List<GraphNode> = emptyList()) {
fun performComplexAlgorithmAndPerformActionOnEachVisitedNode(action: (visitedNode: GraphNode) -> Unit) {
// Assume the logic is very complex with a lot of code here
action(this)
neighbors.forEach { node -> node.performComplexAlgorithmAndPerformActionOnEachVisitedNode(action) }
}
}
I donât know even a single language that allows this outside of exceptions.
Agree that we should use exceptions for this case. Using exceptions will tell developers that your action could be dangerous and you should know the consequence of what you are doing.
Actually, coroutine CancellationException is exactly the solution to break coroutines. So if we donât have problem with this solution in coroutine, I donât see the reason to not doing the same with lambda.
Nice! Even better than my quick runnable example since this is generic. And of course all without having to introduce a new form of jumps/returns to the language
Gotcha. Iâm happy that itâs turned into a stdlib requestâmuch easier to swallow than a language design change. Iâm not sure about the process for proposing stdlib changes (probably just a KEEP). At least in this case itâs an easy thing to add and use by itself in order to collect use cases in the real world.
No, this is not at all consistent with the rest of the language. Exceptions are for exceptional states, not for returning the data. They could be used for different kinds of hacks and workarounds, but this is not what they are for.
Moreover, what you ask for is just not possible to do, both for conceptual and technical reasons. It could work in specific cases only, but not in general. How do you expect below code to work?
fun foo() {
println(1)
runAfterAnHour {
println(3)
// return to foo() or throw Exception()
}
println(2)
}
How do you want to return from the lambda to the foo() if foo() finished executing an hour before we even get to return? Throwing an exception there will only crash some other, remote component in your application, but it wonât get you to foo().
Also, Iâm not sure why do you want to use CancellationException from coroutines. CancellationException is not in any way magic. You can throw just any exception with exactly the same effect.
You need to wrap your block inside cancellable, similar to CancellationException, which should be wrapped inside a coroutine.
No, please read the way CancellationException works. Itâs handled different than other exceptions by default inside coroutine. And note that, my code uses a similar exception CancellableException for demonstration. You donât need to catch CancellationException if they are inside a coroutine scope. Similar, if you use my proposed cancellable and CancellableException, you donât need to catch it too. The code becomes clean and reasonable. Of course, the code I proposed is just a naive version to explain the same logic of how to apply the same way of coroutines to normal lambda.
If we want something in lib, I would expect something better like this.
If you see that itâs not good, then I understand that you are somehow meaning that current CancellationException in coroutine is not good too.
As I said, it will work in specific cases only. This is impossible to do in generic case and Kotlin canât anyhow provide such functionality.
It is handled in a special way by coroutines framework only. From the rest of the world it is just a regular exception. You can rewrite your cancellable with any other random exception and it will work exactly the same.