[HeadFirst] Kotlin 람다와 고차함수

timothy jeong·2021년 11월 6일
0

코틀린

목록 보기
16/20

lambda 에 대하여

lambda(람다) 표현식은 코드블럭을 가지고 있는 객체이다. 객체이기 때문에 람다를 변수에 할당할 수도 있다. 또한 람다를 함수에 넘겨주어서 코드 블럭을 실행시키는 함수로 만들 수도 있다.

Collection 과 사용할때 람다를 함수에 넘길 수 있는 기능은 특히 유용하다. Collection 중 mutable 객체는 sortBy 함수를 가지고 있는데, 이 함수가 어떤식으로 sort 할지를 람다를 넘겨줌으로써 지정할 수 있다.

lambda 는 어떻게 만드는가

{ x: Int -> x + 5 }

위의 코드가 x 를 넘겨주면 5를 더해서 리턴해주는 간단한 람다 코드이다.
-> 를 기준으로 왼쪽이 파라미터, 오른쪽이 body 이다. 람다는 파라미터를 아무것도 가지지 않을수도, 하나만 가질수도 있다.

이 람다 코드의 경우 어떠한 이름도 가지고 있지 않다. 따라서 익명 람다이다. 아래처럼 쓰면 그냥 Pow! 라는 문자열을 반환하는 람다 코드가 되는 것이다.

{ "Pow!" }

변수에 lambda 할당하기

람다를 변수에 할당하는 방식은 별로 특별할게 없다. 아래의 예시는 val 변수가 람다를 갖고 있으므로, 다른 람다를 할당할 수 없다. 만약 다른 람다를 할당하고 싶다면 var 로 선언해야한다.

val addFive = {x: Int -> x + 5}

변수에 람다를 할당하는 것은 람다의 리턴값이 아니라 코드 블럭을 할당하는 것이다. 이렇게 할당된 코드(람다)를 실행시키기 위해서는 명시적으로 람다를 호출해야 한다.

람다 호출하기

람다는 invoke 함수를 통해 호출할 수 있다. 물론 이때 필요한 파라미터를 넘겨줘야한다.

fun main() {
    val addInt = {x: Int, y: Int -> x + y}
    println(addInt.invoke(5, 6))
}

람다 표현식 자료형

다른 모든 객체들이 그렇듯, 람다도 자료형이라는게 있다. 다른 객체들과의 차이라면, 람다는 객체의 이름을 특정하지 않는다는 것이다. 대신 람다는 파라미터와 리턴값의 자료형을 특정한다. 이러한 람다의 자료형은 함수 자료형이라고도 알려져 있다.

따라서 람다의 타입을 표현하는 방식은 특이한데, (parameters) -> return_type 이런 식으로 표현한다. 즉, 아래와 같이 Int 를 파라미터로 받고 String 을 리턴하는 람다는 (Int) -> Stirng 이라고 표현한다.

val msg = {x: Int -> "The value is $x"}

람다를 변수에 할당할때 컴파일러는 이러한 변수를 추정하여 변수의 자료형으로 대입한다. 그 과정을 줄여주고 싶다면 아래와 같이 자료형을 직접 입력해도 된다. 만약 이처럼 람다를 할당받는 변수에 자료형을 특정해줬다면, 람다 코드 부분에서 파라미터에 자료형을 선언하지 않아도 된다. 이 부분을 컴파일러가 추정하기 때문이다.

fun main() {
    val addInt: (Int, Int) -> Int = {x: Int, y: Int -> x + y}
    // val addInt: (Int, Int) -> Int = {x, y -> x + y} 이것역시 가능
    println(addInt.invoke(5, 6))
}

람다의 리턴 타입을 Unit 으로 대체하면 리턴이 없는 람다를 만들 수도 있다.

val myLam: () -> Unit = {println("Hi")}

it 으로 대체하기

만약 하나의 파라미터를 가진 람다이면서 컴파일러가 이 파라미터의 타입을 추정할 수 있다면 (변수의 자료형에 선언되어 있다면) 파라미터를 생략할 수 있다. 그리고 람다 body 부분에서는 it 키워드로 이를 대체할 수 있다.

// 이 코드가
var addFive: (Int) -> Int = {x: Int -> x + 5}
// 아래처럼 바뀐다.
addFive: (Int) -> Int = {it + 5}

it 키워드에 대하여
it 키워드는 하나의 파라미터를 정확히 인지할 수 있는 상황에서 해당 파라미터를 대체하는 키워드이다. 예를 들어 아래와 같이 사용될 수 있다.

collection.forEach { println(it)}
val strings = someArray.map { it.toString() }

it 키워드에 대한 질문

함수에 람다 할당하기

함수의 파라미터로 1개 이상의 람다를 할당할 수 있다. 이렇게 함수를 설계하면 더 일반화된 함수를 만들 수 있다. 이렇게 람다를 파라미터로 받거나, 람다를 리턴하는 함수를 고차 함수(higher-order function) 이라고 부른다.

아래는 람다를 파라미터로 받는 함수의 정의와 사용을 보여준다.

fun convert(x: Double, converter: (Double) -> Double): Double {
    val result = converter(x) // 왜 invoke 를 안써도 되는걸까?
    println("$x is converted to $result")
    return result
}

fun main() {
    convert(20.0,{ it * 1.8 + 32 })
    convert(20.0) { it * 1.8 + 32 } // 함수의 마지막 파라미터가 람다라면 이렇게 가능
}

만약 함수의 파라미터가 람다 뿐이라면, () 를 쓰지않고 함수를 호출할 수 있다.

fun hello(word: ()-> String) {
    println(word.invoke())
}

fun main() {
    hello { "Hello!" }
}

람다 포맷에 대하여
람다 body 부분이 한줄로만 되어있으란 법은 없다. 여러줄로도 람다를 구성할 수 있다. 람다가 여러줄일 경우 마지막 줄을 반환값으로 인지한다.

val lam = {c: Double -> println(c)
                        c * 1.8 + 32 }

람다를 반환하는 함수

fun convert(x: Double, converter: (Double) -> Double): Double {
    val result = converter(x)
    println("$x is converted to $result")
    return result
}

fun getConversionLambda(str: String) : (Double) -> Double {
    when (str) {
        "CentigradeToFahrenheit" -> { return {it * 1.8 + 32} }
        "KgsToPounds" -> { return  { it * 2.204623 } }
        "PoundsToUSTons" -> { return { it / 2000.0} }
        else -> { return { it } }
    }
}

fun main() {

    val pounds = getConversionLambda("KgsToPounds")(2.5)
    // 람다를 반환하는 함수를 람다를 인자로 받는 함수의 파라미터 자리에 넣을 수 있다.
    convert(20.0, getConversionLambda("CentigradeToFahrenheit"))
    
}

람다를 인자로 받고 람다를 반환하는 함수

fun combine(lam1: (Double) -> Double, lam2: (Double) -> Double) : (Double) -> Double {
    return {x: Double -> lam2(lam1(x))}
}

fun main() {
    val kgsToPounds = { x: Double -> x * 2.204623 }
    val poundsToUSTons = { x: Double -> x / 2000.0 }
    val kgsToUSTons = combine(kgsToPounds, poundsToUSTons)
    val usTons = kgsToUSTons.invoke(1000.0)
    println(usTons)
}

type alias : 람다의 가독성을 올리자

함수의 타입(자료형)을 명시하는 것은 코드를 조금더 복잡하게 만든다. 위에 만든 combine 함수의 경우 (Double) -> Double 이 반복되고 있다. 이러한 부분들을 type alias 으로 대체하면 가독성을 높일 수 있다.

type alias 는 이미 존재하는 타입을 새롭게 명명할 수 있도록 해준다.

typealias DoubleConversion = (Double) -> Double
fun combine(lam1: DoubleConversion, lam2: DoubleConversion) : DoubleConversion {
    return {x: Double -> lam2(lam1(x))}
}

이러한 type alias 는 이미 존재하는 타입이라면 모두 적용할 수 있다. 심지어 array 와 collection 에 대해서 적용할 수 있다.

class Animal(val name: String)
typealias AnimalArr = Array<Animal>
typealias AnimalMap = Map<String, Animal>

fun main() {
    val arr: AnimalArr = arrayOf(Animal("Cat"),Animal("Dog"),Animal("Hippo"))
    val map: AnimalMap = mapOf("Cat" to Animal("Cat"),"Dog" to Animal("Dog"), "Hippo" to Animal("Hippo"))
}
profile
개발자

0개의 댓글