Search code examples
kotlinlambdaandroid-jetpack-composehigher-order-functionsfunction-call

How can I invoke a function that returns a lambda which also accepts another lambda as its parameter ( () -> Unit ) -> Unit in Kotlin?


The Code A is from the question answered by Roman Y.

The Code A can work well when it invokes with background(appState)() {...}, why can't I remove parentheses ()?

But Code B fails when it invoke with background(appState) {...}, why?

And more Code C can work well when it invokes with val aa=background(appState) aa{...}, why?

Code A

@Composable
fun NiaApp(
        windowSizeClass: WindowSizeClass,
        appState: NiaAppState = rememberNiaAppState(windowSizeClass) 
) {
        NiaTheme {
            background(appState)() {
                Scaffold(
                    ...
                ) { padding ->
                  }
            }     
        }
}
    
@Composable
fun background(appState: NiaAppState): @Composable (@Composable () -> Unit) -> Unit =
        when (appState.currentDestination?.route) {
            ForYouDestination.route -> { content -> 
                NiaGradientBackground(content = content) }
                else -> { content -> NiaBackground(content = content) }
            } 

Code B

@Composable
fun NiaApp(
        windowSizeClass: WindowSizeClass,
        appState: NiaAppState = rememberNiaAppState(windowSizeClass) 
) {
        NiaTheme {
            background(appState){
                Scaffold(
                    ...
                ) { padding ->
                  }
            }     
        }
}

...

Code C

@Composable
fun NiaApp(
        windowSizeClass: WindowSizeClass,
        appState: NiaAppState = rememberNiaAppState(windowSizeClass) 
) {
        val aa=background(appState)

        NiaTheme {
            aa{
                Scaffold(
                    ...
                ) { padding ->
                  }
            }     
        }
}

...

Solution

  • This is more of a Kotlin function question than a Compose one, so we will omit all Jetpack Compose related codes here, leaving only Kotlin related context to keep everything in focus.

    Lets first define key points and mark them with a character.

    • Key point A. Lambda invocaton : lambda .invoke is equal to ()
    • Key point B. Based on the Documentation

    Passing trailing lambdas:

    According to Kotlin convention, if the last parameter of a function is a function, then a lambda expression passed as the corresponding argument can be placed outside the parentheses:

    ...

    If the lambda is the only argument in that call, the parentheses can be omitted entirely


    We'll make the background function look like this without the @Composable annotation and the NiaAppState arg, leaving it with NO parameters, but we will keep the function calls the same just so we're on track. I also named the returning lambda's parameter for a clearer picture.

    fun background() : (anotherLambda: () -> Unit) -> Unit { ... }
    

    The Code A can work well when it invokes with background(appState)() {...}, why can't I remove parentheses ()?

    But Code B fails when it invoke with background(appState) {...}, why?


    Let's break down your CodeA and CodeB at the same time to answer your 2 questions above. But remember, we are using our own background function and not the compose one.

    1:

    Start first by a simple invocation, here we are just calling the background function, and disregarding the value that it returns, nothing unusual here

    background()
    

    2:

    While here, we are calling the background function, but also INVOKING the returned lambda (Key point A) immediately, we will get a compile error here 'No value passed for parameter anotherLambda' because when we INVOKE it, it requires US to PASS an argument to it which is a type of () -> Unit

    background()() // <-- compile error: No value passed for parameter anotherLambda
    

    3: The Code A can work well when it invokes with background(appState)() {...}

    Here the compile error disappears when we specified the lambda block { ... } , because

    • We invoked the returned lambda (Key point A) immediately
    • And we supplied it with a lambda argument, and since the code follows Key point B it worked by simply calling the argument lambda's block { ... }
    background()() {
       ...
    }
    

    4: But Code B fails when it invoke with background(appState) {...}, why?. Why can't I remove parentheses ()?

    Here, we'll get another kind of error, 'Too many arguments for public fun background() ...'.

    Because we are NOT INVOKING the returned lambda, we are simply calling the background() function itself and it doesn't have any lambda parameters or any parameters at all, check the background function signature we did above and Key point B.

    And your actual background function has only one parameter (appState: NiaAppState) and it's not a lambda type parameter, again check Key point B.

    background() {  // big chunk of compile error here: Too many arguments for public fun background() ...
       ...
    }
    

    5:

    This is the version (check #3) without Key point B. We invoke the returned lambda (Key point A) immediately and pass a lambda argument inside of it.

    background () ( {
       ...
    } )
    

    Equivalent background() calls using the lambda's invoke() instead of a parenthesis ():

    // #2
    background().invoke() // <-- compile error: No value passed for parameter anotherLambda
    
    // #3
    background().invoke {
       ...
    }
    
    // #4
    background(invoke { // big chunk of compile error here: Too many arguments for public fun background() ... and Unresolved reference invoke
       ...
    } )
    
    // #5
    background().invoke ( {
        ...
    } )
    

    And more Code C can work well when it invokes with val aa=background(appState) aa{...}, why?

    Finally lets break down CodeC:

    1:

    Here we call the background function, and because we have an assignment operation with the type inferred, aa is now the returned lambda value from background() invocation

    val aa = background()
    
    // your actual background function equivalent
    val aa = background(appState)
    

    2:

    Assignment declaration with specified type.

    // aa assignment declaration with type specified
    val aa : (() -> Unit) -> Unit = background()
    
    // your actual background function equivalent
    val aa : @Composable (@Composable () -> Unit) -> Unit  = background(appState)
    

    3:

    Assignment declaration with specified type and with a defined name for aa's lambda parameter.

    // aa assignment declaration with type specified and named parameter
    val aa : (aaLambdaParam : () -> Unit) -> Unit = background()
    
    // your actual background function equivalent
    val aa : @Composable (aaLambdaParam: @Composable () -> Unit) -> Unit  = background(appState)
    

    4:

    aa is the returned lambda that accepts an argument of type () -> Unit and because of Key point B, we can omit the parenthesis and directly call the block {...} of the passing lambda argument

    aa {
       ...
    }
    

    5:

    But if we call it this way, we'll get an error, 'No value passed for parameter ...', because aa is now a function that expects an argument of type () -> Unit, see Key point A.

    aa() // <-- No value passed for parameter 'p1' (#2) or 'No value passed for parameter 'aaLambdaParam' (#3)
    

    6:

    This is the version (check #4) without Key point B. We invoke the lambda (Key point A) and pass a lambda argument inside of it.

    aa ( {
       ...
    } )
    

    Equivalent aa calls using the lambda's invoke() instead of a parenthesis ():

    // #4, Key point B
    aa.invoke {
       ...
    }
    
    // #5
    aa.invoke() // <-- No value passed for parameter 'p1' or 'aaLambdaParam'
    
    // #6, not Key point B
    aa.invoke( {
       ...
    } )
    

    I would suggest re-visiting Kotlin's High-Order Functions and Lambdas.