Language Design: outfix function (original) (raw)

What you call outfix functions is more generally known as mixfix or free form syntax.
In fact, it has already been proposed in this post, in a different context, and in the post are discussed some workarounds to achieve it.

This is a popular feature among proof assistants and languages oriented towards formal reasoning, since mathematicians (myself included) consider it an important language feature.
For example, consider the macros or free form syntax supported by Lean 4, which is flexible enough that you could theoretically define Kotlin’s syntax and import Kotlin’s syntax in some files of your project to write some parts in Kotlin (if you’re willing to also implement Kotlin’s semantics), for an outlandish example.
One rationale for this is that often in math, finding the right notation is half the battle; as good notation lifts a substantial part of the complexity of a problem to the syntax level, where even without proper intuition, deduction can be easily performed by anyone who understands its basic rules, even a computer.

In pragmatic languages, however, such as Kotlin, allowing this kind of complex reasoning with expressions is not a priority of language design.
In fact, you could argue the opposite: designing the language so that minimal reasoning and context are required to understand the semantics of any expression makes the language easier to read and learn, and makes it hard to hide errors in artificial complexity introduced by the syntax.
For this reason, following the design philosophy of Kotlin, I don’t think we’ll see this sort of feature any time soon, at least not with so much flexibility.

It is easy to provide simple examples where allowing mixfix syntax in the language makes things more readable, but it’s just as easy to abuse it.


Besides, Kotlin is already pretty expressive for how explicit it is.
For example, a convenient syntax for the performTaskWhenConditionsAreMet function that motivated your idea can be simply achieved by swapping the arguments’ order to benefit from the trailing lambda syntax that already exists in Kotlin:

// Usage
fun main() {
    whenCondition(::buttonClicked) {
        mainViewNavigation()
    }
}

// Implementation
fun whenCondition(condition: () -> Boolean, task: () -> Unit) {
    // Do whatever registration needs to happen here
    performTaskWhenConditionsAreMet(task, condition)
}

Or, more generally, you could define a simple DSL (known as type-safe Builders in the documentation), if you expect your wiring to become complex:

// Usage
fun main() {
    whenCondition(::buttonClicked) {
        // Within this block `this` is a `TaskDeclarationScope`, defined below
        perform { mainViewNavigation() }
        acquireModalFocusUntil { buttonReleased() } // silly example
    }
}

// DSL interface
interface TaskDeclarationScope {
    fun perform(task: () -> Unit)
    // It's hard to come up with good examples when `perform(task)` is already
    // so general, but I hope you get the idea
    fun acquireModalFocusUntil(condition: () -> Boolean)
}
// DSL implementation
class TaskDeclarationScopeImpl : TaskDeclarationScope {
    val declaredTasks = mutableListOf<() -> Unit>()
    var acquiresModalFocusUntil: (() -> Boolean)? = null
    override fun perform(task: () -> Unit) = declaredTasks.add(task)
    override fun acquireModalFocusUntil(condition: () -> Boolean) {
        acquiresModalFocusUntil = condition
    }
}
// Function implementation
fun whenCondition(
    condition: () -> Boolean, tasks: TaskDeclarationScope.() -> Unit
) {
    val declarations = TaskDeclarationScopeImpl().apply(tasks)
    declarations.declaredTasks.forEach { task ->
        performTaskWhenConditionsAreMet(condition, task)
    }
    declarations.acquiresModalFocusUntil?.let { condition ->
        // ...
    }
}

(That being said, your example usage reeks of event-handler-hell UI framework design, so, just in case you are not aware, you should consider using or learning from Compose, currently the most idiomatic UI framework for Kotlin.)

The examples you provided in the initial question are easily written as plain infix functions, extension functions, or simply using == in these concrete cases. I understand they were fabricated to illustrate the concept and don’t represent a real scenario.