Search code examples
kotlinternaryinfix-notation

infix parameter getting calculated even when it is not called in function body


I am trying to make use of

infix fun <T> Boolean.then(param: T): T? = if (this) param else null

but it throws ArrayIndexOutOfBoundsException for

(index > 0) then data[index - 1].id, where index == 0

since

data[-1] doesn't exist.

How could I make it work in Kotlin?


Solution

  • In Kotlin, function parameters are evaluated eagerly: when you call a function, the value of each parameter is worked out before passing control to the function.  This happens regardless of whether the value will get used within the function.  (After all, in general you can't tell whether it will be used without actually running the code.*)

    (This is true for infix functions as well as standard calls; the meaning is exactly the same, despite the different-looking syntax.)

    In fact, this is true for most other operators, too: when you add two numbers, concatenate two strings, return a value from a function, or whatever, each operand is evaluated first.  There are only a few exceptions, of which the short-circuiting && and || operators are the most obvious.

    So in your case, a call such as then data[index - 1].id will always first evaluate data[index - 1].id before passing it to the then() function; so if index is 0, that will throw an ArrayIndexOutOfBoundsException, as you see.

    If you don't want it evaluated, then you have to pass a lambda instead, e.g.:

    infix fun <T> Boolean.then(lazyValue: () -> T): T?
            = if (this) lazyValue() else null
    

    Then you can use it like this:

    (index > 0) then { data[index - 1].id }
    

    What happens there is that the code is evaluated, giving a lambda, which is passed to the function as lazyValue; but the content of the lambda isn't evaluated unless/until* it gets to lazyValue() in the function.

    You can see this pattern in library functions such as require(value, lazyMessage); since the requirement will nearly always be satisfied, and the message is usually a complex string that has to be constructed at runtime, this avoids creating unnecessary String objects by evaluating its second argument only if the condition is false.

    The down-side of passing a lambda is that it's marginally less efficient: it needs to create an object to represent the lambda, so that adds a little extra CPU and heap.  (Depending on the circumstances, it may need to create a new object each time, or it may be able to reuse the same one.)

    But Kotlin provides a way around that: if you mark the function as inline, then it avoids both the function call and the lambda, and effectively ‘pastes’ your code directly into the inlined copy of the function, making it just as efficient as if you'd written out the function body ‘by hand’.


    (* There's an experimental feature called contracts which will let you tell the compiler if you do know for sure under what circumstances a lambda will be evaluated.  That could potentially avoid some types of warning or error — though it doesn't change the evaluation order, so you'd still need a lambda here.)