티스토리 뷰

728x90

고차 함수 사용

고차 함수란 함수의 인자에 함수를 사용할 수 있고 함수의 반환 값에 함수를 사용할 수 있다고 했다. 

간단한 코드를 통해서 어떻게 사용되는지 알아봄.

 

먼저 람다식 함수 형태가 아닌 일반 함수를 인자로 넘기고 리턴 값을 함수로 넘기는 방법을 알아본 후에 점차 생략하는 방법 등을 알아가 보는 게 이해하기가 쉽다. 그런데 나는 개인적으로 생략이 가능해도 필요 이상으로 생략하는 것은 오히려 가독성면에서 더 안 좋은 것 같다고 생각한다.

 

인자에 일반 함수를 사용

fun main() {
    val res1 = sum(3, 2) //일반 인자
    val res2 = mul(sum(3, 3), 3)    //인자에 함수를 사용

    println("res1: $res1, res2: $res2")
}

fun sum(a: Int, b: Int) = a + b
fun mul(a: Int, b: Int) = a * b

//결과
res1: 5, res2: 18

코드를 살펴보면 mul() 함수에 인자 값으로 sum() 함수를 사용했다. mul() 함수는 sum() 함수에 대한 반환 값인 6이 mul() 함수의 첫 번째 인자로 사용되면서 결과는 6*3으로 18이 된다.

 

반환 값에 일반 함수 사용

fun main() {
    println("funcFunc: ${funcFunc()}")
}

fun sum(a: Int, b: Int) = a + b

fun funcFunc() :Int{    //함수의 반환값으로 함수 사용
    return sum(2,2)
}

이번에는 함수의 반환 값에 함수를 반환하는 코드이다. funcFunc() 함수의 반환 값으로 sum() 함수를 사용한다.

 

람다식을 인자나 반환값으로 사용하는 함수

fun main() {
    var result: Int
    val multi = { x: Int, y: Int -> x * y }    //일반 변수에 람다식 활용
    result = multi(10, 20)   //람다식이 할당된 변수는 함수처럼 사용 가능
    println(result)
}

multi 변수를 살펴보면 람다식이 사용되었다. Int 형식의 x, y 매개변수 2개를 받고 x, y를 곱해서 반환하는 함수이다. 

람다식이 변수에 할당되어서 함수처럼 사용이 가능하다.

multi 변수는 자료형이 생략되어있지만 컴파일에서 자료형 추론으로 Int 형임을 알게 된다.

//생략된 코드
val multi: (Int, Int) -> Int = { x: Int, y: Int -> x * y }

저자님의 강의 유튜브중 람다식의 선언과 할당에 대한 이미지

출처 - Do it 코틀린 프로그래밍 강의 https://youtu.be/z8prdZgk4kA

람다식의 화살표 (->) 를 기준으로 왼쪽이 람다식의 매개변수, 오른쪽이 람다식의 몸통(body)이라고 생각하면 된다.

이미지에 나온 것처럼 람다식의 선언 자료형은 람다식 매개변수에 자료형이 명시된 경우 생략이 가능하다고 하는데 처음 써보는 우리들(?) 같은 경우는 항상 선언 자료형을 써주는 것이 헷갈리지 않고 좋다. 나중에 적응이 되고 이제 추론이 가능한 시점까지 능력치가 올라가면 그때 생략해도 무방하다.

 

다음은 변수에 람다식을 지정할 때 생략이 가능한 케이스와 그렇지 않은 케이스이다.

val multi: (Int, Int) -> Int = { x: Int, y: Int -> x * y }  //전체 표현
val multi = { x: Int, y: Int -> x * y } //선언 자료형 생략
val multi: (Int, Int) -> Int = { x, y -> x * y }    //람다식 매개변수 자료형 생략

//선언 자료형과 람다식 매개변수 자료형 둘다 생략하면 컴파일 오류
val multi = {x, y -> x * y} //오류

람다식 매개변수 자료형은 생략해도 선언 자료형이 있기 때문에  추론이 가능하고 람다식 매개변수 자료형을 생략해도 선언 자료형이 있기 때문에 자료형을 추론 가능하다. 그러나 밑에 나와있는 것처럼 둘 다 생략해버리면 컴파일러가 자료형을 추론하지 못하고 오류가 발생한다.

다시 한번 이야기하지만 처음 할 때는 전체 표현을 사용하고 나중에 익숙해지면 선언 자료형을 생략하든지 람다식 매개변수를 생략하던지 하는 것이 좋다고 생각한다.

 

매개변수에 람다식 사용

fun main() {
    val result: Int
    result = highOrder({ x, y -> x * y }, 3, 4) //람다식을 매개변수와 인자로 사용한 함수
    println("highOrder = $result")
}

fun highOrder(sum: (Int, Int) -> Int, a:Int,  b:Int) : Int{
    return sum(a, b)
}

//결과
highOrder = 12

highOrder 함수의 매개변수로 람다식을 사용했고 highOrder 함수를 호출할 때 첫 번째 인자로 람다식을 넘겼다. 이때 첫 번째 인자의 람다식은 highOrder 함수의 첫 번째 매개변수의 자료형과 일치해야 한다. 3개의 인자 람다식과 일반 정수 2개를 넘겼으니 highOrder 함수에서는 sum 함수는 람다식 함수를 가리키고 a와 b는 넘어온 두 개의 일반 정수 3과 4를 가리킨다. highOrder 함수에서는 전달받은 람다식 함수를 실행하는데 람다식 바디가 두 값을 곱해서 반환하는 값이므로 3*4를 연산해서 나온 결괏값인 12를 반환한다. 다시 그 반환한 결과를 result 변수에 담는다. 그리고 출력한다.

람다식이란 실행 코드를 던진다고 생각하면 된다. 모던 자바 인 액션에서는 람다식을 설명할 때 동작을 파라미터화 한다고 설명한다.  람다식 자체가 매개변수에 복사되어서 람다식을 실행했을 때  람다식 바디가 실행되는 것이다.

 

인자와 반환 값이 없는 람다식 함수

fun main() {
    val out: () -> Unit = { println("Hello World")} //인자와 반환값이 없는 람다식의 선언
    //자료형 추론이 가능하므로 val out = {println("Hello World")} 가능

    out()   //함수처럼 사용 가능
    val new = out   //람다식이 들어 있는 변수를 다른 변수에 할당
    new()

}

람다식은 인자가 없거나 반환식이 없을 수 있는데 람다식 선언 자료형 부분이 비어있고 매개변수도 없는 것을 확인할 수 있다. 반환 값도 없으니 Unit으로 선언되어 있고 본문에도 출력문만 딱 하나 있다. 

람다식과 고차 함수 호출

값에 의한 호출

코틀린에서 값에 의한 호출은 함수가 또 다른 함수의 인자로 전달될 경우 람다식 함수는 값으로 처리되어 그 즉시 함수가 수행된 후 값을 전달한다.

fun main() {
    val result = callByValue(lambda())  //람다식 함수를 호출
    println("result = $result")
}

fun callByValue(b: Boolean): Boolean {  //일반 변수 자료형으로 선언된 매개변수
    println("callByValue function")
    return b
}

val lambda: () -> Boolean = {	//람다 표현식이 2줄
    println("lambda function")
    true	//마지막 표현식 문장의 결과가 반환된다.
}

//결과
lambda function
callByValue function
result = true

호출 순서 이미지 유튜브 캡처

출처 - Do it 코틀린 프로그래밍 강의 https://youtu.be/z8prdZgk4kA

차례대로 1번에 표시된 callByValue(lambda())에서 인자로 전달된 lambda() 함수가 먼저 수행되며 2번의 true를 반환한 후에 3번의 callByValue 함수에 b의 값을 복사한다.

그다음 callByValue() 함수의 b값을 최종적으로 반환하고 반환된 값을 result 변수에 할당되어서 true를 출력한다.

이름에 의한 람다식 호출

fun main() {
    val callByName = callByName(otherLambda)
    println("callByName = $callByName")
}


fun callByName(b: () -> Boolean): Boolean{  //람다식 자료형으로 선언된 매개변수
    println("callByName function")
    return b()
}

val otherLambda: () -> Boolean = {
    println("otherLambda function")
    true
}

//결과
callByName function
otherLambda function
callByName = true

위의 코드와 비슷해 보이지만 차이점은 위의 코드는 함수를 직접 실행한 값을 넘겼지만 여기서는 이름만 넘기는 것이 차이가 있다. 

역시나 저자님의 유튜브 강의 캡처

출처 - Do it 코틀린 프로그래밍 강의 https://youtu.be/z8prdZgk4kA

위의 callByValue 함수와 callByName 함수의 다른 점은 매개변수에 있다. callByValue는 매개 변수를 boolean 자료형 값으로 받았지만 callByName 함수는 매개변수로 람다식 자료형으로 선언되었다는 점이다.

이것은 람다식 자체가 매개변수 b에 복사되어서 사용되기 전까지는 람다식이 실행되지 않는다.

callByName 함수에서 람다 함수를 실행했을 때 그때 람다식이 실행된다. 

일반 함수 참조

람다식을 매개변수로 사용하는 것이 아닌 일반 함수를 다른 함수의 인자로 호출하는 방법도 있다.

fun main() {
    //인자와 반환값이 있는 함수
    val res1 = funcParam(3,2, ::sum)
    println("res1 = $res1")

    //인자가 없는 함수
    hello(::text)

    //일반 변수에 값처럼 할당
    val likeLambda = ::sum
    println(likeLambda(6, 6))
}

fun sum(a: Int, b: Int) = a + b

fun text(a: String, b: String) = "HI! $a $b"

fun funcParam(a: Int, b: Int, c: (Int, Int) ->Int): Int {
    return c(a, b)
}

fun hello(body: (String, String) -> String) : Unit{
    println(body("Hello", "World"))
}

일반 함수의 매개변수와 함수의 선언된 람다식의 매개변수의 자료형과 개수가 같으면 2개의 콜론(::) 기호를 사용해서 함수를 넘길 수 있다.

코드에서 sum 함수는 Int 형인 a, b 2개의 매개변수가 선언되어 있는데 funcParam의 매개변수의 마지막이 람다식을 받는 매개변수로 되어있다. 이럴 때 funcParam 함수를 호출할 때 콜론 2개(::) 기호를 사용해서 함수의 소괄호와 인자를 생략하고 사용할 수 있다.

hello(::text)	//함수 참조 기호
hello({a, b -> text(a,b) })	//람다식 표현
hello {a, b -> text(a, b) } //소괄호 생략 

즉 위의 3가지의 표기법은 모두 같지만 매개변수와 인자의 개수와 자료형이 같을 경우 람다식 표현법이 간략화된 함수 참조 기호인 ::을 사용하면 좀 더 편리하게 작성할 수 있다.

람다식 매개변수

매개변수와 인자 개수에 따라서 함수를 호출할 때 람다식의 생략된 표현이 가능하다.

 

1. 람다식에 매개변수가 없을 때

fun main() {
    //매개변수 없는 람다식
    noParam({ "Hello World" })
    noParam { "Hello World" }   //위와 동일, 소괄호 생략 가능, 인텔리제이에서 생략하라고 경고문구
}

fun noParam(out: () -> String) = println(out())

매개변수가 없을 때는 소괄호를 생략하고 중괄호만 입력해서 작성할 수 있다. 추가로 매개변수가 없기 때문에 화살표(->)도 없다.

 

2. 람다식 매개변수가 1개 있을 때

fun main() {
    // 매개변수가 하나 있는 람다식 함수
    oneParam({ a -> "Hello World! $a" })
    oneParam { a -> "Hello World! $a" } // 위와 동일 결과, 소괄호 생략 가능
    oneParam { "Hello World! $it" }  // 위와 동일 결과, it으로 대체 가능
}

// 매개변수가 하나 있는 람다식 함수가 oneParam함수의 매개변수로 지정됨
fun oneParam(out: (String) -> String) {
    println(out("OneParam"))
}

매개변수가 1개 있을 때는 람다식에 필요한 변수를 하나 써줘야 한다. 저 코드에서는 a -> 이 부분이다. 

그런데 코틀린에서는 매개변수가 1개인 경우에는 화살표(->) 를 생략하고 $it으로 대체할 수 있다.

 

3. 람다식 매개변수가 2개 이상인 경우

fun main() {
    // 매개변수가 두개 있는 람다식 함수
    moreParam { a, b -> "Hello World! $a $b" } // 매개변수명 생략 불가
    
    //특정 람다식의 매개변수를 사용하고 싶지 않을 때
    moreParam {_, b -> "Hello World! $b"}	//첫 번째 문자열은 사용하지 않고 생략
}

// 매개변수가 두개 있는 람다식 함수가 moreParam함수의 매개변수로 지정됨
fun moreParam(out: (String, String) -> String) {
    println(out("OneParam", "TwoParam"))
}

매개변수가 2개 이상일 때는 $it 으로 생략이 불가능하다. 

또 특정 람다식에서 매개변수를 사용하지 싶지 않을 때는 언더스코어(_)를 사용할 수 있다.

 

4. 일반 매개변수와 람다식 매개변수 같이 사용

fun main() {
    // 인자와 함께 사용하는 경우
    withArgs("Arg1", "Arg2", { a, b -> "Hello World! $a $b" })
    // withArgs()의 마지막 인자가 람다식인 경우 소괄호 바깥으로 분리 가능
    withArgs("Arg1", "Arg2") { a, b -> "Hello World! $a $b" }
}

// withArgs함수는 일반 매개변수 2개를 포함, 람다식함수를 마지막 매개변수로 가짐
fun withArgs(a: String, b: String, out: (String, String) -> String) {
    println(out(a, b))
}

지금까지는 람다식만 매개변수로 가지는 함수를 호출했을 때 였는데 일반 매개변수와 람다식 매개변수를 같이 사용할 때는 소괄호가 생략될 수 없다. 그러나 람다식이 제일 마지막 매개변수로 있을 때는 먼저 일반 매개변수 인자는 소괄호로 감싸고 람다식만 따로 밖으로 빼서 작성할 수 있다. 단 이때 조건이 람다식 매개변수가 마지막 인자 위치에 있어야 한다는 규칙이 있다.

참고

do it 코틀린 프로그래밍 www.yes24.com/Product/Goods/74035266

 

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함