Kotlin

람다, 고차함수, 함수합성, 클로저

kakaroo 2022. 3. 25. 21:34
반응형

Lambda 식 ? Value 처럼 다룰 수 있는 익명 함수

 

 

val lambdaA : returnType = { argument -> body }

 

argument  는 하나일 경우 생략 가능하다.

//val lambdaA : (Int) -> Int= { x : Int -> x*2 }
val lambdaA : (Int) -> Int= { it*2 }

 

return type 도 compiler가 추론 가능할 경우 생략 가능하다.

val square : Int = { x : Int -> x * x }
-> val square = { x : Int -> x * x }  // 매개변수의 Int 로 타입추론이 가능하다.

 

고차함수

고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수다. 코틀린에서는 람다나 함수 참조를 사용해 함수를 으로 표현할 수 있다. 따라서, 고차 함수는 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수다.

예) 표준 라이브러리 함수 filter는 술어 함수를 인자로 받으므로 고차 함수다.

  list.filter{ it > 0 }

 

fun Calculator(a : Int, b : Int, p: (Int, Int) -> Int){ 
// Calculator는 고차함수라고 말할 수 있다.
    println("$a, $b -> ${p(a, b)}")
}
In : Calculator(5, 4, { a : Int, b : Int -> a + b})
Out : 5, 4 -> 9

 

이때 lambda 함수가 인자의 제일 마지막에 있다면 밖으로 뺄 수 있다.

In : Calculator(5, 4) { a: Int, b: Int -> a + b }
Out : 5, 4 -> 9

 

또한 고차함수에 타입이 정의되어있는 경우 타입을 생략할 수 있다.

In : Calculator(5, 4) { a, b -> a + b }
Out : 5, 4 -> 9

람다함수가 아닌 일반함수를 인자로 넣으려면 일반함수 앞에 ::를 붙여 다음과 같이 하면 된다.

fun sum(a : Int, b : Int) = a + b
// main
In : Calculator(5,7, ::sum)
Out : 5, 7 -> 12

함수형 변수를 인자타입에 넣는 경우는 다음과 같이 한다.

In[0] : val minus : (Int, Int) -> Int = {a, b -> a-b}
In[1] : Calculator(5,2, minus)
Out : 5, 2 -> 3

고차함수의 인자가 하나인 경우

fun Square(a : Int, p: (Int) -> Int){
    println("square $a -> ${p(a)}")
}

위와 같이 a라는 인자 하나를 받는 경우 조금 다르게 표현할 수도 있는데 다음과 같다.

In : Square(3) {a -> a * a}
Out : square 3 -> 9
In : Square(3) {it * it}
Out : square 3 -> 9

위 두 호출은 같은 호출이다. 다만 인자가 하나일 때는 it 을 사용해 더 간단하게 표현이 가능하다.

의미있는 반환값이 없는 경우

fun PrintInfo(p : () -> Unit) {
	print("Calculator Version : ")
    p()
}

위와 같이 의미있는 반환값이 없을 경우 Unit을 사용한다. main에서 호출해보자.

// main 
In : PrintInfo() {println("1.0")}
Out : Calculator Version : 1.0

방금 경우와 같이 고차함수의 인자로 매개변수없이 함수식만 있는 경우 소괄호() 는 생략가능하다.

// main
In : PrintInfo {println("1.1")}
Out : Calculator Version : 1.1

또한 매개변수 함수식을 Nullable로 할수도 있다.

fun PrintInfo(p : (() -> Unit)? = null{
	print("Calculator Version : ")
    p?.invoke()?: println("no version")
}

함수식에 null이 들어왔을 경우 ?: 기호 뒤의 식을 실행한다. main에서 호출해보자.

In : PrintInfo()
Out : Calculator Version : no version

 


 

람다 표현식의 예

fun double(x: Int) : Int = x * 2
println(double(10))

//함수 값 사용하기
//파리미터 타입은 괄호로 둘러싸서 표시, 반환타입은 화살표 오른쪽에 위치
//함수 정의는 등호 뒤에 온다. 함수정의를 중괄호로 둘러싼 람다식 형태 사용
val double : (Int) -> Int = { x -> x * 2}
println(double(10))

//함수 참조 사용하기
val double1: (Int) -> Int = {double(it)}
println(double(10))
val double2: (Int) -> Int = ::double
println(double2(10))


//함수 합성
fun square(n: Int) = n * n  //return 타입은 추론가능해서 생략할 수 있다
fun triple(n: Int) = n * 3
fun compose(s: (Int)->Int, t: (Int)->Int) : (Int) -> Int = {s(t(it))}
println(compose(::square, ::triple)(2))	//36 : (2*3)*(2*3)

fun sum(x: Int, y: Int) = x + y
fun calculate(x: Int, y: Int, operation:(Int, Int) -> Int): Int {
    return operation(x, y)
}
fun calculate1(x: Int, operation:(Int) -> Int): Int {
    return operation(x)
}
//고차함수
//val value = calculate(3, 4, ::sum)
val value = calculate(3, 4) {a, b -> a + b}
println(value)
val value1 = calculate1(7){a -> a * a}
println(value1)

 

클로저(Closure)는 outer scope(상위 함수의 영역)의 변수를 접근할 수 있는 함수를 말합니다. 코틀린은 클로저를 지원하며 그렇기 때문에 익명함수는 함수 밖에서 정의된 변수에 접근할 수 있습니다.

 

내부함수에서 외부함수의 데이터를 사용할 경우,

외부함수의 코드불럭이 끝나도 내부함수에서 참조하는 변수(outerVarX)는 계속 살아남아 있는 현상

fun outer(): () -> Unit {
    var outerVarX = 10  //전역변수처럼 사라지지 않는다.
    var innerFunc = fun() {
        return println(++outerVarX)
    }
    return innerFunc
}

val func1 = outer()

func1() // 11
func1() // 12

 

 

람다식을 사용하다 보면 내부 함수에서 외부 변수를 호출하고 싶을 때가 있다.

클로저(Closure)란 람다식으로 표현된 내부 함수에서 외부 범위에 선언된 변수에 접근할 수 있는 개념을 말한다.

이때 람다식 안에 있는 외부 변수는 값을 유지하기 위해 람다식이 포획(Capture)한 변수라고 부른다.

기본적으로 함수 안에 정의된 변수는 지역 변수로 스택에 저장되어 있다가 함수가 끝나면 같이 사라진다.

하지만 클로저 개념에서는 포획한 변수는 참조가 유지되어 함수가 종료되어도 사라지지 않고 함수의 변수에 접근하거나 수정할 수 있게 해준다.

클로저의 조건은 다음과 같다.

  • final 변수를 포획한 경우 변수 값을 람다식과 함께 저장.
  • final이 아닌 변수를 포획한 경우 변수를 특정 래퍼(wrapper)로 감싸서 나중에 변경하거나 읽을 수 있게 한다. 이때 패러에 대한 참조를 람다식과 함께 저장.

자바에서는 외부의 변수를 포획할 때 final만 포획할 수 있다.

따라서 코틀린에서 final이 아닌 변수를 사용하면 내부적으로 변환된 자바 코드에서 배열이나 클래스를 만들고 final로 지정해 사용된다.

func main()
{
    val calc = Calc()
    var result = 0 // 외부 변수
    calc.addNum(2, 3) { x, y -> result = x + y } // 클로저
    println(result) // 값을 유지하여 5 출력
    // 5
}

class Calc {
    fun addNum(a: Int, b: Int, add: (Int, Int) -> Unit) {	//반환값이 없다.
        add(a, b)
    }
}
반응형