ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [코틀린] 람다
    코틀린 2022. 3. 19. 01:20

    1. 람다

    람다 표현식은 파라미터 리스트와 바디만으로 정의된 함수이다.

    // { parameter -> body }
    { e: String -> e + 1 }

    람다는 함수형 프로그래밍과 궁합이 잘 맞는다.

    fun isPrime(n: Int) = n > 1 && (2 until n).none { k -> n % k == 0}
    

    위의 코드는 소수인지 아닌지를 알려주는 로직을 함수형으로 구현한 것이다. 2 이상이면서 2와 n - 1 까지의 숫자중 어떤 숫자로도 나누어 떨어지지 않으면 소수이다. 2 until n 이 리턴한 IntRange의 none 은 함수를 하나 받아 정수 범위의 원소 하나하나에 함수를 적용시킨다. 모든 결과가 false 라면 none은  true 를 리턴한다.

    람다가 한 개의 파라미터를 받는다면 it라는 implicit parameter를 활용해서 람다를 사용할 수도 있다.

    fun isPrime(n: Int) = n > 1 && (2 until n).none { n % it == 0}

     

    람다를 받는 함수

    람다를 받는 함수는 아래와 같이 정의할 수 있다. 아래는 Int -> Int 형의 람다를 받아 내부에서 사용하는 함수이다.

    fun modifyOneToTen(func: (Int) -> Int): List<Int> {
        return (1 .. 10).map { func(it) }
    }
    
    modifyOneToTen { it * 2 } // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

    다른 파라미터와 함께 람다 파라미터를 전달할 때는 마지막 파라미터로 전달하는게 좋다. 예를들어 아래 같이 선언을 하면 좀 긴 람다를 활용해야 할 경우 아래처럼 호출하게된다.

    fun manyParameters(func: (Int) -> Int, n: Int): List<Int> {
        return (1 .. n).map { func(it) }
    }
    
    manyParameters({ 
        val k = 25
        it * k
    }, 5)
    

    코틀린에서 람다를 함수 파라미터의 가장 뒤에 선언하게 되면 아래처럼 함수를 호출하는게 가능하다. 이렇게 호출하는게 위 보다 이해하기가 쉬워 람다를 마지막 함수 파라미터로 선언하는것이 권장된다.

    fun manyParameters1(n: Int, func: (Int) -> Int): List<Int> {
        return (1 .. n).map { func(it) }
    }
    
    manyParameters1(5){
        val k = 25
        it * k
    }

     

    :: 사용

    함수에 람다를 파라미터로 넘겨주는 방법은 아래와 같았다.

    fun doSomething(str: String, func: (String) -> Unit) = func(str)
    
    doSomething("arg") { arg -> println(arg) } // arg 출력
    doSomething("it") { println(it) } // it 출력

    그런데 이 때 람다는 단지 함수를 호출하기 위한 역할만 하고 있다. 코틀린에서는 이런 경우 좀 더 간편하게 할 수 있는 문법을 제공한다. 함수앞에 ::를 붙이면 람다를 활용할 때와 같은 의미이다.

    doSomething("::", ::println) // :: 출력

     

    함수를 리턴하는 함수

    아래와 같이 람다를 리턴하는 함수를 만들 수도 있다.

    fun thisFunctionReturnFunction(n: Int): (Int) -> Boolean {
        return { it -> it % n == 0 }
    } // n의 배수이면 true를 리턴하는 람다
    
    println((1 .. 10).filter(thisFunctionReturnFunction(3))) // [3, 6, 9]

     

    익명함수

    만약 람다를 여러곳에서 재사용하고 싶으면 어떻게할까. 람다는 변수에 할당할 수 있다. 이 때 람다의 파라미터의 타입을 정의해두고 람다의 리턴타입은 추론에 맡기거나, 람다를 할당받는 변수의 타입을 명시할 수 있다.

    val tenTimes = { n: Int -> (n * 10).toString()} // 람다 내에 파라미터 타입 지정
    tenTimes(10) // 100
    
    val tenTimes: (Int) -> String = { n -> (n * 10).toString() } // 변수에 타입 지정
    tenTimes(10) // 100

    또 다른 방법으로는 익명함수를 사용하는 것이 있다. 익명함수는 fun, 파라미터 리스트, 바디만으로 이루어진 함수로 변수에 할당할 수 있다.

    val tenTimes = fun(n: Int): String { return (n * 10).toString() }
    tenTimes(10) // 100

     

    2. 클로저(Closure)와 렉시컬 스코핑 (Lexical scoping)

    렉시컬 스코핑: 중첩된 함수 선언에서 내부 함수는 외부 함수의 스코프를 포함한다. 외부함수가 내부함수를 리턴하더라도 유지된다.

    fun outerFunction(string: String): (String) -> String {
        val name = string
        return { str -> "$name $str"}
    }
    
    val function1 = outerFunction("Lexical")
    println(function1("Scoping")) // Lexical Scoping

    outerFunction이 람다를 리턴할때 람다는 클로저를 형성한다. 클로저는 람다가 형성 될 때의 스코프에 있는 모든 변수들을 담고 있다. 따라서 리턴된 이후에도 람다가 생성될때의 name 값을 그대로 가지고 있다.

     

    3. labeled return

    기본적으로 람다는 별도의 리턴 값을 가질 수 없다. 아래와 같은 코드는 문법에 맞지 않다.

    (1..3).map {
        if(it == 2) return
        it
    }
    fun invokeWith(n: Int, action: (Int) -> Unit) {
        println("enter invokeWith $n")
        action(n)
        println("exit invokeWith $n")
    }
    
    fun caller() {
        (1..3).forEach {
            i -> invokeWith(i) {
                println("enter for it")
                if(it == 2) return // 문법적으로 맞지 않음
                println("exit for $it")
            }
        }
        println("end of caller")
    }

    위의 코드에서도 역시 return은 문법적으로 사용할 수 없다. 코틀린은 이 return이 action(n)을 빠져나오라는 것인지, invokeWith 자체를 빠져나오라는것인지, forEach를 빠져나오라는 것인지 지정해주지 않으면 알지 못한다.

    라벨 리턴은 이런 경우에 return이 어디에 적용될지를 정해 준다. 아래와 같이 빠져나올 곳을 label@로 지정해주면 return@label을 만났을때 빠져나올 수 있다.

    fun invokeWith(n: Int, action: (Int) -> Unit) {
        println("enter invokeWith $n")
        action(n)
        println("exit invokeWith $n")
    }
    
    fun caller() {
        (1..3).forEach {
            i -> invokeWith(i) here@ {
                println("enter for $it")
                if(it == 2) return@here
                println("exit for $it")
            }
        }
        println("end of caller")
    }
    
    caller()
    

    라벨리턴은 리턴될 곳을 명시할 수도 있고 메소드 이름을 라벨로 사용할 수도 있다.

    fun caller() {
        (1..3).forEach {
            i -> invokeWith(i) {
                println("enter for $it")
                if(it == 2) return@invokeWith
                println("exit for $it")
            }
        }
        println("end of caller")
    }

     

    4. 인라인 함수

    람다를 받는 함수에 inline 키워드를 함수에 붙이면 그 함수가 호출되는 부분에 그 함수의 바이트코드가 들어간다. 이렇게 하면 함수 콜 스택이 줄어드는 장점이 있지만 바이트코드가 커지게 된다. 

    아래는 inline 키워드를 붙인 함수와 그렇지 않은 함수의 콜 스택을 출력하는 예제이다.

    fun printStackTrace(func: () -> Unit) {
        func()
    }
    
    inline fun printStackTraceInline(func: () -> Unit) {
        func()
    }
    
    fun call() {
        printStackTrace { println(RuntimeException().stackTrace.size) }
        printStackTraceInline { println(RuntimeException().stackTrace.size) }
    }
    
    call()
    // 출력
    // 34
    // 31

    inline을 붙인 함수의 호출 스택 수가 더 적다.

    만약 일부 람다는 inline 최적화를 하고싶지 않으면 noinline으로 표시하면 된다.

    inline fun printStackTraceInline(
        func1: () -> Unit,
        noinline func2: () -> Unit) {
        func1()
        func2()
    }

    inline 함수는 nonlocal 리턴을 통해 람다를 종료할 수 있다.

    inline fun printStackTraceInline(func1: () -> Unit) {
        func1()
    }
    
    fun call() {
        printStackTraceInline { (1..5).map {
            if(it == 3) return
            println(it * 10) }
        }
    }
    
    // 출력
    // 10
    // 20

    만약 inline 함수의 람다 파라미터로 받은 람다가 함수 내에서 실행되는 것이 아니라 다른 람다로 리턴될경우는 어떨까

    이럴 경우에는 noinline으로 다시 인라인 최적화를 풀거나 crossinline으로 호출되는 쪽에서 inline 최적화를 하도록 할 수 있다. crossinline으로 선언된 람다 파라미터의 경우에는 논로컬 리턴은 할 수 없다. 

    inline fun printStackTraceInline(crossinline func1: (n: Int) -> Unit): (Int) -> Unit {
        return {input -> func1(input) }
    }
    

    '코틀린' 카테고리의 다른 글

    [코틀린] 연산자 오버로딩  (0) 2022.03.20
    [코틀린] 내부 반복(map, filter, groupBy, reduce)  (0) 2022.03.19
    [코틀린] 델리게이션  (0) 2022.03.16
    [코틀린] 클래스와 상속  (0) 2022.03.13
    [코틀린] 객체와 클래스  (0) 2022.03.13

    댓글

Designed by Tistory.