# Problem

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.

Why not use just `return@performComplexAlgorithmAndPerformActionOnEachVisitedNode`? (or explicitely define a label Returns and jumps | Kotlin )

Because itâs currently not allowed in Kotlin. You will get this compiler error if you do that: ââreturnâ is not allowed hereâ.

See Inline functions | Kotlin (kotlinlang.org) and Inline Functions in Kotlin | Baeldung on Kotlin if you donât know about this error.

2 Likes

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.

2 Likes

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.

Iâd add that itâs a significant code smell to use an exception for non-exceptional behavior.

Why not allow a function to be in scope within the lamda, something like this:

``````fun main() {
val rootNode = GraphNode(0, (1..10000).map { GraphNode(it) })

rootNode.performComplexAlgorithmAndPerformActionOnEachVisitedNode {
print(it.value)
if (it.value > 1) haltFurtherAction()
}

println("END")
}
``````

Or possibly even better, just return something, maybe a boolean to continue processing:

``````fun main() {
val rootNode = GraphNode(0, (1..10000).map { GraphNode(it) })

rootNode.performComplexAlgorithmAndPerformActionOnEachVisitedNode {
print(it.value)
return if (it.value < 2) true else false
// TODO: how to break if it.value >= 2
}

println("END")
}
``````

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.

In the code example, you are only allowed to do anything you want inside the `main` method. Everywhere else is read-only code from a 3rd party library.

Your code doesnât work without making changes inside the read-only `performComplexAlgorithmAndPerformActionOnEachVisitedNode` method.

1 Like

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.

1 Like

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.

10 Likes

I honestly really donât understand whatâs the problem here. Itâs not limitation of the library, itâs the limitation of Kotlin.

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.

1 Like

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.

4 Likes

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) }
}
}
``````
2 Likes

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.

I ended up with this extension:

``````class CancellableException: Exception()

inline fun cancellable(cancellableAction: () -> Unit) {
try {
cancellableAction()
} catch (e: CancellableException) {
// cancel action
}
}
``````
``````cancellable {
rootNode.performComplexAlgorithmAndPerformActionOnEachVisitedNode {
print(it.value)
if (it.value > 1) throw CancellableException()
}
}
``````

Actually, the `cancellable` and `CancellableException` above are similar to coroutine `CancellationException`.

Because this is the native way to break coroutines, I think, we can also make it the native way to break lambdas in Kotlin.

2 Likes

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

1 Like

Yeah, and it would be nice if it is included natively in kotlin std lib because itâs consistent with the current language design of coroutines.

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.

1 Like

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)
}
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.

2 Likes

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.