There are quite a few solutions to your problem. I will start with the simple ones and then move on to the more complex.
When without argument
Kotlin documentation says:
when
can also be used as a replacement for an if
-else if
chain. If no
argument is supplied, the branch conditions are simply boolean
expressions, and a branch is executed when its condition is true
Use case:
when {
startsWithHelp(givenString) -> printHelp()
startsWithFn(givenString) -> println("Hello, ${givenString.substring(3)}!")
}
Pros:
Doesn't require any additional code
Easy to understand
Cons:
- Boilerplate
(givenString)
Argument wrapper which overrides equals
The required wrapper is similar to RegexWhenArgument
, but instead of checking Regex
it invokes (String) -> Boolean
. There is nothing special about String
, so I will use <T>
.
Wrapper function:
fun <T> whenArg(arg: T) = object {
override fun equals(other: Any?) =
if (other is Function1<*, *>)
(other as Function1<T, Boolean>)(arg)
else arg == other
}
Use case:
when (whenArg(givenString)) {
::startsWithHelp -> printHelp()
startsWithFn -> println("Hello, ${givenString.substring(3)}!")
}
Pros:
- Functions can be used in
when
branches
- Very easy to understand
- Requires only one simple wrapper function
Cons:
- Easy to forget to wrap an argument
- The function of the wrong type can be accidentally used in a branch condition
Note: this solution and the next one allows using functions combination, which can be created by these extensions:
infix fun <T> ((T) -> Boolean).and(other: ((T) -> Boolean)) = { it: T -> invoke(it) && other(it) }
infix fun <T> ((T) -> Boolean).or(other: ((T) -> Boolean)) = { it: T -> invoke(it) || other(it) }
operator fun <T> ((T) -> Boolean).not() = { it: T -> !invoke(it) }
DSL-based when
analog
In Kotlin DSL can be used not only for building objects but for creating custom program flow structures as well.
Source code:
@DslMarker
annotation class WhichDsl
// This object is used for preventing client code from creating nested
// branches. You can omit it if you need them, but I highly recommend
// not to do this because nested branches may be confusing.
@WhichDsl
object WhichCase
// R type parameter represents a type of expression result
@WhichDsl
class Which<T, R>(val arg: T) {
// R? is not used here because R can be nullable itself
var result: Holder<R>? = null
inline operator fun ((T) -> Boolean).invoke(code: WhichCase.() -> R) {
if (result == null && invoke(arg)) result = Holder(code(WhichCase))
}
// else analog
inline fun other(code: WhichCase.() -> R) = result?.element ?: code(WhichCase)
}
data class Holder<out T>(val element: T)
inline fun <T, R> which(arg: T, @BuilderInference code: Which<T, R>.() -> R) =
Which<T, R>(arg).code()
Statement use case:
which(givenString) {
::startsWithHelp { printHelp() }
startsWithFn { println("Hello, ${givenString.substring(3)}!") }
}
Expression use case:
val int = which(givenString) {
::startsWithHelp { 0 }
startsWithFn { 1 }
other { error("Unknown command: $givenString") }
}
Pros:
- Customizable syntax
- New features can be added
- Functions which works only with some argument types can be added
- Nested branches can be enabled
- No boilerplate
- Function types are checked at compile-time
- Easy to read
Cons:
- Requires a few helper classes and functions
- Usage of experimental
@BuilderInference
(it can be replaced with explicit type declaration for expressions and separate method for statements)
- New syntax learning may take some time
Extensions examples:
inline fun <R> Which<*, R>.orThrow(message: () -> String) =
other { throw NoWhenBranchMatchedException(message()) }
val <R> Which<*, R>.orThrow get() = orThrow { "No branch matches to $arg" }
inline fun <T, R> Which<T, R>.branch(condition: (T) -> Boolean, code: WhichCase.() -> R) =
condition(code)
inline fun <T, R> Which<T, R>.case(value: T, code: WhichCase.() -> R) =
branch({ it == value }, code)
fun <T : CharSequence, R> Which<T, R>.regex(regex: Regex, code: WhichCase.() -> R) =
branch({ regex.matches(it) }, code)
fun <T : Comparable<T>, R> Which<T, R>.range(range: ClosedRange<T>, code: WhichCase.() -> R) =
branch({ it in range }, code)
inline fun <T> Which<T, *>.sideBranch(condition: (T) -> Boolean, code: WhichCase.() -> Unit) {
if (condition(arg)) code(WhichCase)
}
fun <T> Which<T, *>.sideCase(value: T, code: WhichCase.() -> Unit) =
sideBranch({ it == value }, code)
inline fun <R> Which<*, R>.dropResult(condition: WhichCase.(R) -> Boolean = { _ -> true }) {
result?.let { (element) ->
if (WhichCase.condition(element)) result = null
}
}
inline fun <T, R> Which<T, R>.subWhich(condition: (T) -> Boolean, code: Which<T, R>.() -> R) =
branch(condition) {
which(this@subBranch.arg, code)
}