Proposal: mixfix function syntax

Currently, when a function takes more than one callback, you end up with the following code:

myTry({ 
  do something 
}, { 
  alert("Exception Caught $message")
}) { 
  close handles... 
}

However, it would be much more nicer to write it as

myTry { 
  do something 
} myCatch { 
  alert("Exception Caught $message") 
} myFinally { 
  close handles... 
}

Surprisingly, this feature would not require almost any modifications to Kotlin API, as function argument names can be used as keywords:

fun myTry(fn: Fn, myCatch: Fn, myFinally: Fn) = ...

myTry({ a }, { b }, { c }) == myTry { a } myCatch { b } myFinally { c }
1 Like

While I really love the idea, I often would like to design an api {} like {} this {} or declare a function with multiple lambdas, I see one problem. How do you distinguish if this is a function that accepts myTry and myCatch lambdas or myCatch is an infix function running on the result of myTry?

Hi! I have not given much thought yet to the implementation details.

However:

  1. I believe that mixfix arguments should have always priority over infix methods
  2. Perhaps the ambiguity is manageable, similar to multiple this in scope.
  3. Alternatively, the syntax could be .. myCatch = { .. } myFinally = { .. }.
1 Like

Implementation is another problem, because I guess the compiler parses the code to abstract from without the knowledge about available functions and their args, so it can’t know if this is myTry(myCatch) or myTry().myCatch().

But I’m not really talking about the implementation, but about the concept itself. Even from the developer perspective it is not clear, what this code does/is. I think ambiguity on variables (this, local variable vs property, etc). and functions (overloads) is much simpler than ambiguity on what the code actually is.

But as I said, I think it would be an awesome feature, so maybe this is just a matter of finding the right syntax. Also, I have almost zero experience in designing programming languages, so it doesn’t really matter what I think :wink:

1 Like

This can actually be done today in a DSL-style kind of thing, but it has some performance overhead since it has to pass lambdas as objects around. This is a valid and extensible implementation of this idea:

fun main() {
    val normalTrySuccess: Int = try {
        42
    } catch(e: Throwable) {
        println(e)
        5
    } finally {
        println("Done!")
    }
    val normalTryFail: String = try {
        "Success"
    } catch(e: Throwable) {
        println(e)
        "Failed"
    } finally {
        println("Done!")
    }
    val myTrySuccess: Int = myTry {
        42
    } myCatch {
        println(it)
        5
    } myFinally {
        println("Done!")
    }
    val myTryFail: String = myTry {
        "Success"
    } myCatch {
        println(it)
        "Failed"
    } myFinally {
        println("Done!")
    }
    println(normalTrySuccess)
    println(normalTryFail)
    println(myTrySuccess)
    println(myTryFail)
}

@JvmInline
value class MultifixPart<Next, Data> @PublishedApi internal constructor(@PublishedApi internal val _data: Any?){
    inline val data: Data get() = _data as Data
}

inline fun <Next, Data> MultifixPartOf(data: Data): MultifixPart<Next, Data> = MultifixPart(data)

object MyTry {
    object MyCatch
    object MyFinally
}

fun <T> myTry(tryBlock: () -> T): MultifixPart<MyTry.MyCatch, () -> T> = MultifixPartOf(tryBlock)
inline infix fun <T> MultifixPart<MyTry.MyCatch, () -> T>.myCatch(crossinline catchBlock: (Throwable) -> T): MultifixPart<MyTry.MyFinally, () -> T> {
    return MultifixPartOf {
        try {
            data()
        } catch(e: Throwable) {
            catchBlock(e)
        }
    }
}
inline infix fun <T> MultifixPart<MyTry.MyFinally, () -> T>.myFinally(finallyBlock: () -> Unit): T {
    val result = data()
    finallyBlock()
    return result
}

I used some things about this try-catch example specifically to make it a bit more performant, but as a general implementation you’d simply have to use Pair, Triple, and higher-arity types like those so that you can carry all the lambdas until the last function in the sequence

2 Likes

Yes, I was experimenting with something like this - mostly for fun. Seems like an interesting approach for making DSLs or some crazy voodoo magic framework. It has one big issue though: we need to divide our functions/blocks into intermediate and terminal ones. Or create something like end() which is ugly. For example, you can’t use your above code to perform try...catch, but without finally, because it won’t return a proper value.

2 Likes

@kyay10 I think that your solution, chaining high-order infix functions, is the best you can do currently, but there is a lot of overhead.

I would like to see a way to write multi-part functions without that overhead. For example a trifix function that’s like infix but with three arguments and two names. The callsite looks like arg1 trifixFun1 arg2 trifixFun2 arg3. The goal is to be equivalent sugar for trifixFun(arg1, arg2, arg3)

1 Like

By the way, this is not the answer you expected, but I sometimes use functions that receives several functions and to me the cleanest is to write it as e.g.:

doSomething(
    onSuccess = {
        ...
    },
    onFailure = {
        ...
    }
)

In your case it could be:

myTry(
    block = {
        ...
    },
    myCatch = {
        ...
    },
    myFinally = {
        ...
    }
)

Or you can hack the code formatting a little to get this:

myTry({
    ...
}, myCatch = {
    ...
}, myFinally = {
    ...
})

I’m not a big fan of the last example, but maybe you think otherwise.

1 Like

You can implement this particular example very efficiently today – just make myCatch and myFinally inline extension functions on a Result receiver. No extra objects, no lambda objects.

The only thing that’s really missing is an automatic conversion from Result to Result.getOrThrow() if myFinally is not called.

I think that’s not so bad – you can always call getOrThrow directly instead of myFinally.

Given how well we can do already, I don’t think the language needs a special mixfix call syntax. Automatic conversions might be nice, though.

1 Like

There are extra objects and a lot of overhead. You need some intermediary that your first infix function outputs for your second infix function to work on. And if your functions are high-order functions, then you’d like to inline all of it. But you can’t achieve the same result as a single inlined function that takes many arguments with seperate functions that take only 2 arguments each.

1 Like

Actually, you can implement this with chaining in the way that it will inline everything and it won’t create any intermediate objects*. But it would be still suboptimal as it generates more bytecode and internally performs two try...catch operations instead of one.

edit: *) Well, we would need to create an additional object in the case of an exception.

1 Like