본문 바로가기
Kotlin/Kotlin 프로그래밍

[Kotlin] 함수와 함수형 프로그래밍 (2) - 고차함수와 람다식

by 주 녕 2021. 4. 7.
728x90

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 

 

고차 함수와 람다식

다른 함수를 인자로 사용하거나 함수를 결과값으로 반환하는 함수

고차 함수의 형태

일반 함수를 인자나 반환값으로 사용하는 고차 함수

함수의 인자로 함수를 사용하는 예제

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

함수를 반환값으로 사용하는 예제

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

fun funcFunc(): Int{
    return sum(2, 2)
}

 

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

변수에 람다식 할당

fun main() {
    var result: Int
    val multi = {x: Int, y: Int -> x*y}
    result = multi(10, 20)
    println(result)
}
  • 변수 multi : x*y를 반환하는 람다식 할당
    • 자료형 추론 가능 → 변수 이름이 multi()와 같이 함수 형태로 사용할 수 있음
  • val multi: (Int, Int) -> Int = {x: Int, y: Int -> x*y}
    • 람다식의 자료형 : 람다식 매개변수에 자료형이 명시된 경우 생략 가능
    • 람다식의 매개변수 : 선언 자료형이 명시되어 있으면 생략 가능
    • 람다식 내용 : 표현식이 여러 줄인 경우 마지막 표현식이 반환
    // 람다식의 자료형 생략 
    // 반환 자료형이 없는 경우
    val greet: () -> Unit = {println("Hello World!")}
    // val great = {println("Hello World!"}
    
    // 매개변수가 하나만 있는 경우
    val square: (Int) -> Int = {x -> x*x}
    // val square = {x: Int -> x*x}
    
    // 람다식 안에 람다식이 있는 경우
    val nestedLambda: () -> () -> Unit = {{println("Hello World!")}}
    // val nestedLambda = {{println("Hello World!")}}

 

매개변수에 람다식 할당

// 매개변수에 람다식 함수를 이용한 고차함수
fun main() {
    var result: Int
    result = highOrder({x, y -> x+y}, 10, 20)
    println(result)
}

fun highOrder(sum: (Int, Int) -> Int, a: Int, b: Int): Int {
    return sum(a, b)
}
  1. main에서 highOrder() 함수를 호출함
  2. 3개의 인자를 highOrder() 함수의 매개변수로 전달
    • sum은 람다식, a와 b는 10과 20를 가리킴
  3. sum은 전달받은 a, b를 인자처럼 받아 람다식의 x, y로 대체됨
    • 함수의 내용인 x+y에 의해 덧셈 연산을 수행한 후 결과를 반환
  4. 반환한 결과는 원래 코드의 result로 반환

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

fun main() {
    val out: () -> Unit = {println("Hello World!")}

    out()
    val new = out
    new()
}

 


람다식과 고차 함수 호출하기

  • 기본형 변수
    • 할당된 값은 스택
    • 다른 함수에 인자로 전달하는 경우, 해당 값이 복사되어 전달
  • 참조형 변수
    • 할당된 객체는 참조 주소가 스택에 저장
    • 객체는 힙에 존재
    • 함수에 전달될 때 참조된 주소가 복사되어 전달됨

JVM에서 실행되는 자바, 코틀린은 함수를 호출할 때 인자의 값만 복사하는 '값에 의한 호출(Call by Value)'이 일반적

포인터 주소 연산이 없기 때문에 주소 자체를 사용하여 호출하는 '참조에 의한 호출(Call by Reference)가 없음

코틀린은 람다식을 사용하면서 확장된 호출 방법을 사용할 수 있음

 

값에 의한 호출

람다식 함수는 값으로 처리되어 그 즉시 함수가 수행된 후 값을 전달함

fun main() {
    val result = callByValue(lambda())
    println(result)
}

fun callByValue(b: Boolean): Boolean {
    println("callByValue function")
    return b
}

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

callByValue 결과

  1. main()의 callByValue(lambda())에서 인자로 전달된 lambda()가 먼저 수행
  2. 람다식에 의해 println() 실행, true 반환
  3. callByValue()의 b에 반환값 복사
  4. callByValue()의 본문의 println() 실행, b 반환
  5. 반환 값은 main()의 result 변수에 할당되어 true를 출력

이름에 의한 람다식 호출

fun main() {
    val result = callByName(otherLambda)
    println(result)
}

fun callByName(b: () -> Boolean): Boolean {
    println("callByName function")
    return b()
}

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

callByName의 결과

'값에 의한 람다식 호출'의 코드와 거의 동일해보이지만, callByName()에서 매개변수 b의 자료형을 람다식으로 선언하면서 람다식의 이름을 호출하는 것으로 바뀜

  1. main()의 callByName(otherLambda)에서 람다식 자체가 매개변수 b에 복사됨
  2. callByName()에서 b()처럼 함수 형태로 호출되어야 람다식이 실행됨
    • 이름이 전달된 시점이 아니라 callByName() 함수 블록에 사용되는 b()에 의해 호출되는 것
  3. otherLambda에서 true가 반환
  4. callByName()에서 b()의 결과인 true 반환
  5. main()의 result에 true 할당

상황에 맞춰 즉시 실행할 필요가 없는 코드를 작성하는 경우

이름에 의한 호출 방법을 통해 필요할 때만 람다식을 작동하도록 만들 수 있음

 

다른 함수의 참조에 의한 일반 함수 호출

fun main() {
    // 인자와 반환값이 있는 함수
    val res1 = funcParam(3, 2, ::sum)
    println(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"))
}
  1. funcParam(3, 2, ::sum)
    • sum()은 람다식이 아니기 때문에 위의 예시처럼 이름으로 호출할 수 없음
    • sum()과 funcParam()의 매개변수 c의 선언부 구조를 보면 인자 수와 자료형의 개수가 동일
    • 이때는 2개의 콜론(::) 기호를 함수 이름 앞에 사용해 소괄호와 인자를 생략하여 사용 가능 
  2. hello(::text)
    • hello()는 반환값이 없음
    • hello() 함수 선언부의 body() 함수와 함수에 인자로 전달한 2개의 문자열이 text() 함수로 넘어가 Hi!와 결합함
  3. likeLambda = ::sum
    • ::sum을 변수에 할당하여 사용
    // 인자가 없는 함수
    hello(::text)
    hello({a, b -> text(a, b)})
    hello {a, b -> text(a, b)}

모두 동일한 결과를 출력함

→ 매개변수와 인자 구조가 동일한 경우 람다식 표현법이 간략화된 함수 참조 기호 :: 을 사용하는 것이 편리함

 


람다식의 매개변수

매개변수 개수에 따라 람다식을 구성하는 방법이 다름

매개변수와 인자 개수에 따라 람다식의 생략된 표현이 가능하기 때문에 코드를 더 간략화 할 수 있음

람다식에 매개변수가 없는 경우

fun main() {
    noParam({"Hello World!"})
    noParam {"Hello World!"} // 소괄호 생략 가능
}

fun noParam(out: () -> String) = println(out())
  • noParam()은 람다식 매개변수 1개를 가지고 있음 → noParam()을 사용할 때 소괄호 생략 가능
  • main()에서 사용된 noParam()의 인자에는 람다식 표현식인 {"..."} 형태의 인자가 있음
    • 이 람다식은 매개변수가 없으므로 화살표(->) 기호가 사용되지 않음
    • 매개변수는 없지만 반환 자료형은 문자열을 반환하고 있음

람다식의 매개변수가 1개인 경우

람다식에 매개변수가 1개 있을 경우에는 람다식에 화살표(->) 기호 왼쪽에 필요한 변수를 써줘야 함

fun main() {
    // 매개변수가 1개 있는 람다식
    oneParam({ a -> "Hello World! $a"})
    oneParam { a -> "Hello World! $a"}
    oneParam { "Hello World! $it"}
}

fun oneParam(out: (String) -> String) {
    println(out("OneParam"))
}
  • 변수와 화살표를 추가하여 a -> 와 같이 나타내고, 문자열에 표현하기 위해 $a 사용
  • 매개변수가 1개인 경우에는 화살표를 생략하고 $it으로 대체 가능

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

fun main() {
    // 매개변수가 2개 있는 람다식
    moreParam {a, b -> "Hello World! $a $b"}
}

fun moreParam(out: (String, String) -> String) {
    println(out("OneParam", "TwoParam"))
}
  • 매개변수가 2개 이므로 $it을 사용해 변수를 생략할 수 없음
  • 람다식의 특정 매개변수를 사용하고 싶지 않을 때는 이름 대신에 언더스코어(_)로 대체할 수 있음

일반 매개변수와 람다식 매개변수를 같이 사용하는 경우

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

fun withArgs(a: String, b: String, out: (String, String) -> String) {
    println(out(a, b))
}
  • 일반 함수의 마지막 매개변수가 ㄹ마다식 함수라면 withArgs()의 소괄호 바깥으로 마지막 인자인 람다식을 뺄 수 있음
    • 이런 형태로 함수를 간략화 하려면 람다식 매개변수가 꼭 마지막 인자 위치에 있어야 함

일반 함수에 람다식 매개변수를 2개 이상 사용하는 경우

fun main() {    
    // 2개의 람다식을 매개변수로 사용하는 경우
    twoLambda({ a, b -> "First $a $b"}, { "Second $it" })
    twoLambda({ a, b -> "First $a $b"}) { "Second $it" }
}

fun twoLambda(first: (String, String) -> String, second: (String) -> String) {
    println(first("OneParam", "TwoParam"))
    println(second("OneParam"))
}
  • 람다식 매개변수를 2개 사용하는 경우에는 소괄호를 생략할 수 없음
  • 마지막 인자는 소괄호 밖에 둘 수 있음
    • ({첫번째}, {두번째})
    • ({첫번째}) {두번째}
  • 매개변수에 람다식이 3개가 되었을 때도 마찬가지로 마지막 람다식만 빼낼 수 있음

 

전체 코드

fun main() {
    // 매개변수 없는 람다식
    noParam({"Hello World!"})
    noParam {"Hello World!"} // 소괄호 생략 가능

    // 매개변수가 1개 있는 람다식
    oneParam({ a -> "Hello World! $a"})
    oneParam { a -> "Hello World! $a"}
    oneParam { "Hello World! $it"}

    // 매개변수가 2개 있는 람다식
    moreParam { a, b -> "Hello World! $a $b" }
    moreParam { _, b -> "Hello World! $b" }

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

    // 2개의 람다식을 매개변수로 사용하는 경우
    twoLambda({ a, b -> "First $a $b"}, { "Second $it" })
    twoLambda({ a, b -> "First $a $b"}) { "Second $it" }
}

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

fun oneParam(out: (String) -> String) {
    println(out("OneParam"))
}

fun moreParam(out: (String, String) -> String) {
    println(out("OneParam", "TwoParam"))
}

fun withArgs(a: String, b: String, out: (String, String) -> String) {
    println(out(a, b))
}

fun twoLambda(first: (String, String) -> String, second: (String) -> String) {
    println(first("OneParam", "TwoParam"))
    println(second("OneParam"))
}
728x90

댓글