깨알 개념/Kotlin

[Kotlin] 람다(Lambda) 표현식

interfacer_han 2024. 2. 1. 12:13

#1 람다 대수와 람다 표현식

#1-1 람다 대수

// 수학 - 함수의 표현
f(x) = x + 1
g(x, y) = x * y + 1

// 수학 - 람다 대수로 함수 표현
λx. x + 1
λx. λy. x * y + 1

프로그래밍에서의 람다 표현식(Lambda Expression)은 수학에서의 람다(λ) 대수(Lambda Calculus)에서 비롯된 개념이다. 람다 대수는 함수에 f나 g와 같은 이름을 붙이지 않는다. 함수의 몸통 즉, 계산 부분만을 단순하게 표현한다.

 

#1-2 람다 표현식

// 코틀린 - 함수의 표현
fun f(x: Int): Int {
    return x + 1
}
fun g(x: Int, y: Int): Int {
    return x * y + 1
}

// 코틀린 - 람다 표현식으로 함수 표현
{ x: Int -> x + 1 }
{ x: Int, y: Int -> x * y + 1 }

하지만, 람다 대수에서 함수를 표현할 때 사용하는 람다(λ) 기호를 프로그래밍에서는 "->"(대부분의 경우) 또는 "lambda"(파이썬 등의 경우)로 바꿔 표현한다. 즉, 프로그래밍에서의 람다 표현식은 람다없는 람다식이다. 예를 들어, f(x) = x + 1 라는 함수를 람다 대수에서는 λx. x + 1 로 표현하고 코틀린에선 x -> x + 1로 표현한다.

 

또, 람다 표현식으로 표현한 코틀린 함수는 f나 g같은 함수의 이름이 없음을 볼 수 있다. 람다 대수의 취지대로, 함수을 이름없이 표현했다. 하지만, 재미있게도 이러한 람다 표현식 함수에 이름을 붙일 수도 있다.

 

#1-3 이름이 붙은 람다 표현식

// 코틀린 - 이름이 붙은 람다 표현식
val f: (Int) -> Int = { x: Int -> x + 1 }
val g: (Int, Int) -> Int = { x: Int, y: Int -> x * y + 1 }

왜 이런 짓을 하냐면, 람다 표현식의 (처음 배우기엔 어렵지만) 간결한 문법을 취하면서 동시에 (이름을 붙임으로써) 해당 함수를 재사용할 수 있다는 이점이 있기 때문이다.

 

예시 코드에 있는 (Int) -> Int 및 (Int, Int) -> Int는 람다 표현식이 아님에 유의한다. 해당 표현식은 코틀린의 함수 타입(Function Type) 표현식으로 어떤 함수가 가지는 매개변수의 타입과 반환 타입을 정의하는 표현식이다. 화살표(->)가 있다고 다 람다 표현식인게 아니다.

 

#1-4 타입 생략

// 타입 생략 - 함수 타입 표현식을 생략
val f = { x: Int -> x + 1 }
val g = { x: Int, y: Int -> x * y + 1 }

// 타입 생략 - 람다 표현식 내부의 매개변수 타입을 생략
val f: (Int) -> Int = { x -> x + 1 }
val g: (Int, Int) -> Int = { x, y -> x * y + 1 }

// 타입 생략 - 둘 다 생략 (#2-3 참조)
val f = { println("흔들리는 꽃들 속에서 네 샴푸향이 느껴진거야") }

#1-3의 코드에서, 코틀린이 지원하는 타입 추론에 의거해 코드의 일부분을 생략할 수도 있다. 단 한가지 경우(#2-3 참조)를 제외하고, 함수 타입 표현식과 람다 표현식 내부의 매개변수 타입 둘 다를 생략할 수는 없다. 코틀린의 컴파일러 입장에서 타입 추론이 불가능하기 때문이다.

 

#2 다양한 상황에서의 람다 표현식 문법

#2-1 매개변수가 없는 경우의 표현

// 기본 (타입 생략 안 함)
val noParameterLambda: () -> String = { () ->

    "Hello, World!"
}

// 타입 생략 - 함수 타입 표현식을 생략
val noParameterLambda = { () ->
    "Hello, World!"
}

// 타입 생략 - 람다 표현식 내부의 매개변수 타입을 생략
val noParameterLambda: () -> String = {
    "Hello, World!"
}

매개변수가 없다면 ()와 같이 함수 타입 표현식의 괄호 안에 아무것도 넣지 않으면 된다. 그러나, 이는 함수 타입 표현식에서 가능하고 람다 표현식 내부의 매개변수 타입 부분에선 문법상 불가능하다. 가능하게 만들어줘도 괜찮을 것 같은데 왜 막아놨는지 의문이 든다. 아무튼, 매개변수가 없는 경우 반드시 람다 표현식 내부의 매개변수 타입이 생략된 형태로만 함수를 짤 수 있다.

 

#2-2 return을 하지 않는 경우 (자바로 치면, 리턴 타입이 void)의 표현

// 기본 (타입 생략 안 함)
val noReturnTypeLambda: (Int, String) -> Unit = { number: Int, text: String ->
    println("Received number: $number and text: $text")
}

// 타입 생략 -함수 타입 표현식을 생략 (에러가 나진 않지만, 사용을 지양하자.)
val noReturnTypeLambda = { number: Int, text: String ->
    println("Received number: $number and text: $text")
}

// 타입 생략 - 람다 표현식 내부의 매개변수 타입을 생략
val noReturnTypeLambda: (Int, String) -> Unit = { number, text ->
    println("Received number: $number and text: $text")
}

함수 타입 표현식에서 함수의 return이 없는 경우 return 타입 자리에 Unit을 넣는다. Unit은 자바의 void에 대응되는 키워드다. return이 없는 함수에서 함수 타입 표현식을 생략하면 코틀린의 컴파일러가 타입 추론을 통해 return 타입을 정한다. 하지만 웬만해서는 함수 타입 표현식을 생략하지 말자.

 

생략한다고 에러가 나는 건 아니다. 중간에 있는 코드를 복사해서 코틀린 IDE에 붙여놓으면 잘 작동하는 모습을 볼 수 있다. 하지만, 해당 코드는 프로그래머의 통제를 벗어난 코드다. 예를 들어, 어떤 프로그래머가 return 타입이 없는 함수를 만들기로 마음먹었으나, 어쩌다 실수로 중괄호 끝에 { ... "아무말" }와 같이 String 객체를 배치하면 타입 추론에 의해 해당 함수의 return 타입은 String이 되어버린다. 함수 타입 표현식을 통해 얻을 수 있는 return 타입의 명시성을 이용하는 프로그래머가 되자.

 

#2-3 매개변수도 없고 return도 하지 않는 경우의 표현

// 기본 (타입 생략 안 함)
val noParaNoReturn: () -> Unit = { () ->

    println("This is a lambda with no parameters and no return type.")
}

// 타입 생략 - 함수 타입 표현식을 생략
val noParaNoReturn = { () ->
    println("This is a lambda with no parameters and no return type.")
}

// 타입 생략 - 람다 표현식 내부의 매개변수 타입을 생략
val noParaNoReturn: () -> Unit = {
    println("This is a lambda with no parameters and no return type.")
}

---

// 타입 생략 - 둘 다 생략
val noParaNoReturn = {
    println("흔들리는 꽃들 속에서 네 샴푸향이 느껴진거야")
}

#2-1과 #2-2의 교집합은, 람다 표현식 내부의 매개변수 타입이 생략된 형태다. 따라서, 해당 형태로만 함수를 짤 수 있다. 추가로, 매개변수와 리턴이 둘다 없는 경우에는 함수 타입 표현식과 람다 표현식 내부의 매개변수 타입을 둘 다 생략할 수 있다. 이 때는 화살표(->)마저 생략한다. 이는 둘 다 생략할 수 있는 단 한 가지의 특별 케이스다. 그리고 이 특별 케이스의 존재 때문에 람다식은 반드시 화살표(->)를 가진다고 말할 수 없게 된다.

 

#2-4 it 키워드 (매개변수가 하나인 경우)

// 평범한 람다 표현식
val f: (Int) -> Int = { x: Int -> x + 1 }

// it을 사용한 람다 표현식
val f: (Int) -> Int = { it + 1 }

 it(그거)라는 키워드로 해당 매개변수를 표현할 수 있다. 매개변수가 하나일 때만 it 키워드를 사용할 수 있다. it을 사용할 수 있는 상황이면, 중괄호 { ... } 내부에서 화살표와 그 왼쪽 부분을 생략해도 에러가 발생하지 않는다.

 

#2-5 소괄호 생략 (인자로 쓰인 람다 표현식이 마지막 인자인 경우)

class calculator(
    val args1: Int,
    val args2: Int,
    val args3: (Int, Int) -> Int
) {
    fun additionOperation(): Int {
        return args1 + args2
    }
    fun differenceOperation(): Int {
        return Math.abs(args1 - args2)
    }
    fun customOperation(): Int {
        return args3(args1, args2)
    }
}

fun main() {
    val myCalculator1 = calculator(3, 4, {x, y -> (x * x) + y}) // 람다 표현식이 소괄호 안에 있음
    val myCalculator2 = calculator(3, 4) {x, y -> (x * x) + y} // 람다 표현식이 소괄호 밖에 있음
    
    println(myCalculator1.customOperation()) // 출력 결과: 13
    println(myCalculator2.customOperation()) // 출력 결과: 13
}

코틀린에선 이와 같이 람다 함수를 인자로서 전달할 수 있다. 이 때, 람다 표현식이 마지막 인자로 사용되는 경우에는 myCalculator2에서처럼 소괄호에서 빼고 그 오른쪽에 적어도 된다. 이는 가독성 향상을 위해 제공되는 편의성 문법이다.

 

#3 함수형 인터페이스의 구현

// SAM Conversions된 단일 추상 메소드 인터페이스
fun interface MyOperation {
    fun operate(x: Int, y: Int): Int
}

fun main() {
    val addition = MyOperation { x, y -> x + y }
    val subtraction = MyOperation { x, y -> x - y }

    val result1 = addition.operate(5, 3)
    val result2 = subtraction.operate(5, 3)

    println("Result of addition: $result1") // 출력 결과: 8
    println("Result of subtraction: $result2") // 출력 결과: 2
}

fun 키워드와 함께 단일 추상 메소드를 구현하는 인터페이스인 함수형 인터페이스는 람다 표현식이 활용되는 대표적인 사처다. 코틀린이 지원하는 문법으로 간편하게 구현 클래스를 만들 수 있다. 그 문법은 바로 어떤 변수에, 인터페이스 이름 + 람다 표현식을 할당하면 끝이다. 이러면 나머지는 코틀린 컴파일러가 알아서 작업한다. 그 작업은 바로, 해당 람다 표현식 함수를 인터페이스의 하나 있는 메소드로서 오버라이드하고, 익명 클래스를 만드는 것이다 (여기에선 그 익명 클래스를 addition 및 subtraction에 할당함으로써 이름 있는 클래스로 만들었다). 함수형 인터페이스에 대한 자세한 내용은 다음 게시글에서 살펴볼 수 있다.

 

 

[Kotlin] 함수형 인터페이스 (Single Abstract Method Interface)

#1 일반 인터페이스 #1-1 평범한 인터페이스 interface MyNormalInterface { fun myFirstMethod(value: Int): String fun mySecondMethod(value1: Int, value2: String): Int ... } 우리가 잘 알고있는 인터페이스의 모습이다. #1-2 함수

kenel.tistory.com

 

#4 요약

람다 표현식은 처음 배우기엔 분명 어렵지만, 쓰면 쓸수록 그 간편함을 인정하게 된다.