Home > OS >  Why am I forced to make the below functional property nullable in Kotlin?
Why am I forced to make the below functional property nullable in Kotlin?

Time:03-06

package algorithms

import algorithms.util.IOUtils

object Calculator {
    /* static abstract class Operation {
     *   private Function logic; //functional interface
     *   Operation(Function f) { this.logic = f );
     * }
     */
    sealed class Operation(val logic : (Int, Int)-> Int)
    /* define singleton types 'ADD', 'SUB', 'MUL', 'DIV' - equivalent to inner class definitions in Java */
    /* static class ADD extends Operation {
     *   ADD(){
     *      super((int x, int y)-> x y);
     *   }
     * }
     */
    object ADD: Operation({ x: Int , y: Int -> x y } )
    object SUB: Operation({ x: Int , y: Int -> x-y })
    object MUL: Operation({ x: Int , y: Int -> x*y })
    object DIV: Operation({ x: Int , y: Int -> x/y })

    private fun getOperationFromChar(ch : Char): Operation? {
        return when(ch){
            ' ' -> ADD
            '-' -> SUB
            '*' -> MUL
            '/' -> DIV
            else -> null
        }
    }

    fun eval(ch: Char, x: Int, y: Int): Int? {
        val op : Operation? = getOperationFromChar(ch)
        return op?.logic?.invoke(x,y)
    }
}


fun main(){
    println("Result : ${Calculator.eval(
        IOUtils.readChar("Enter desired operation ( ,-,*,/) "),
        IOUtils.readInteger("Enter first number"),
        IOUtils.readInteger("Enter second number"))}")
}

The above code works fine, however, IntelliJ forces me to make logic in return op?.logic?.invoke(x,y) nullable

Although the definition of Operation sealed class Operation(val logic : (Int, Int)-> Int) has nowhere mentioned that it can be null.

I would image if the definition of the Operation object was sealed class Operation(val logic : ((Int, Int)-> Int)?) then it would make sense, but it is not so. What is going on here?

CodePudding user response:

It's because the return value of getOperationFromChar() is nullable.

It's not your operation function that returns a nullable value. op itself is already nullable. You defined it yourself with val operation: Operation?. When you use ?. calls, the results are always nullable because null will be the result if there was no object to call the function on.

The input of your getOperationFromChar() function is a Char. A Char can be any of many thousands of possible values, not just the four that you have in your when statement. That's why the compiler is enforcing an else branch. If you want to avoid returning a nullable, you could choose to throw an error if an invalid input is given:

private fun getOperationFromChar(ch : Char): Operation {
    return when(ch){
        ' ' -> ADD
        '-' -> SUB
        '*' -> MUL
        '/' -> DIV
        else -> error("Invalid input $ch")
    }
}

Then you could define val op: Operation and it would be able to accept the result of this function as non-nullable.

Sealed classes help avoid the need for an else branch when the sealed class is the type of the when's subject. Then the compiler can be sure you have a branch for every possible input. Your function is the opposite case, where your sealed class type is the output, not the input.

By the way, it is more sensible for you to use an enum instead of a sealed class for this case, because none of the children of your sealed class have unique properties or functions.

CodePudding user response:

It is because op is nullable; if you set type of op as Operation you don't need to check the nullability of logic.

In fact, it checks the nullability of whole op?.logic to call invoke() method; that can throw NullPointerException due to op nullability.

CodePudding user response:

The safe call operator(?.) returns null if the value to the left is null, otherwise continues to evaluate the expression to the right. for example

val x:Int? = 4

x?.dec()?.inc()?.dec()
    
x?.let { 
 it.dec().inc().dec()
}

CodePudding user response:

If you take the chained evaluation apart, it becomes clear:

  fun eval(ch: Char, x: Int, y: Int): Int? {
    val op: Operation? = getOperationFromChar(ch)
    val logic: ((Int, Int) -> Int)? = op?.logic
    val retval: Int? = logic?.invoke(x, y)
    return retval
  }

logic is not typed ((Int, Int) -> Int) but ((Int, Int) -> Int)?, because if op is null, the result of op?.logic will also be null.

  • Related