오늘도 더 나은 코드를 작성하였습니까?

람다로 프로그래밍 본문

Kotlin in Action/코틀린 기초

람다로 프로그래밍

hik14 2020. 8. 16. 14:51

람다

- 다른 함수에 넘길 수 있는 작은 코드 조각

- 공통된 코드 구조를 쉽게 함수로 뽑아낼 수 있다.

 

이벤트가 발생 하면 이 핸들러를 실행하자!

데이터 구조의 모든 원소에 이연산을 적용하자!

생각을 코드로 표현하기 위해 일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야 하는 경우가 종종 있다.

 

기존에 자바는 무명 클래스로 이것을 해결하였지만 상당히 번거롭다.

 

함수형 프로그래밍은 함수를 값처럼 다루는 접근방법을 택하여 문제를 해결한다.

클래스를 선언하고 그 인스턴스를  함수에 넘겨 처리하는 대신에 함수를 직접 다른 함수에 전달한다.

 

예시) 사람들로 이루어진 리스트에서 연장자 찾기

data class Person(val name:String, val age: Int)

fun findTheOldest(people: List<Person>){
    var maxAge = 0

    var theOldest: Person? = null

    for(person in people){
        if(person.age > maxAge ) {
            maxAge = person.age
            theOldest = person
        }
    }
    println(theOldest)
}



fun main() {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))

    findTheOldest(people) // 직접 만든 함수.
    println(people.maxBy { it.age }) // 람다를 이용한 컬렉션 함수호출.
}

findTheOldest가 복잡한 함수는 아니고 경험 있는 개발자면 이런 코드는 쉽게 만들어 낼 수 있지만 상당히 코드량이 많다.

 

maxBy는 모든 컬렉션에 호출할 수 있는 함수이다. 이 함수는 함수를 인자로 받는다 { it.age }

 

Person  ----> { it.age } ---> age 사람 객체를 받아 비교 기준인 나이를 리턴해주는 함수를 maxBy에 넘겨주는 것이다.

단순히 함수나 프로퍼티를 반환하는 람다는 멤버 참조( Person::age )로 대체할 수 있다. 

 

이렇게 람다 함수를 사용함으로써 더 짧고 가독성이 좋아 이해하기 쉽다.

 

람다식의 문법

람다는 여기저기 전달할 수 있는 동작의 모음이다.

 

    val sum = { x:Int, y:Int -> x + y}
    println(sum(10,2))

인자 목록에 괄호( ) 가없다는 사실을 주목하라.

화살표(  -> ) 목록과 본문을 구별한다.

람다식은 변수에 저장 가능하면 일반 함수와 동일하게 호출 가능하다.

 

 {println(42)}()

람다식을 직접 호출도 가능하다. 하지만 읽기 어렵고 별로 쓸모도 없기 때문에 코드의 일부분을 블록으로 둘러싸서 실행할 필요가 있다면 run함수를 사용한다. run은 인자로 받은 람다를 실행해주는 라이브러리 함수다.

   run { println(42) }

 

위에서 사용했던 람다식을 정식으로 작성하면 다음과 같다.

    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    
    println(people.maxBy ({ p: Person -> p.age }))

1. 컴파일러가 문맥으로부터 유추할 수 있는 인자 타입은 굳이 적을 필요가 없다.

컴파일러는 Person 타입의 리스트에 maxBy를 호출하는 사실을 알고 있기 때문에 람다의 파라미터도 Person임을 유추한다.

처음에 타입을 쓰지 않고 람다를 작성하고 컴파일러가 타입을 모르겠다고 불평할 경우에만 명시해줘라.

people.maxBy{ p -> p.age }

2. 람다의 파라미터가 하나뿐이고 타입이 단 하나뿐이면 디폴트 파라미터 이름인 it을 사용한다.

   인자가 단 하나인 경우 인자에 이름을 붙일 필요가 없다.(즉, 아래 코드에서 it -> 을 생략할 수 있음)

people.maxBy{ it -> it.age }
people.maxBy{ it.age }

3.  코틀린에서 함수 호출 시 마지막 인자가 람다 함수이면 그 람다를 괄호 밖으로 빼낼 수 있다.(유일 인자도 가능)

* 2개 이상의 람다 식을 받는 함수라면 인자의 마지막만 빼낼 수 있다.

people.maxBy(){ p: Person -> p.age }

4. 유일한 인자라면 괄호를 생략 가능하다.

people.maxBy{ p: Person -> p.age }

 

* 람다 안에 람다가 중첩될 시 각 람다의 파라미터를 명시하는 게 좋다 it이 어떤 파라미터인지 헷갈리게 된다. 

* 람다를 변수에 저장할 때는 문맥 추론이 적용할 수 없어 반드시 타입을 명시한다.

* 본문이 여러 줄로 되어있다면 본문의 마지막에 있는 식이 리턴 값이다.

 

현재 영역에 있는 변수에 접근

 

자바 무명 내부 클래스를 정의할 때 메서드의 로컬 변수를 무명 내부 클래스 안에서 사용할 수 있다.

람다 역시 람다를 함수 안에서 선언 시 함수의 파라미터뿐 아니라 람다 앞에 선언된 로컬 변수까지 람다에서 모두 사용 가능하다.

 

fun printMessagesPrefix(messages: Collection<String>, prefix: String){
    val postfix = "occurred"
    messages.forEach {
        println(" $prefix $it $postfix")
    }
}

fun main() {

   val errors = listOf("403 Forbidden", "404 Not found")
    printMessagesPrefix(errors, "Error:")
}

코틀린 람다에서 파이널 변수가 아닌 변수에도 접근 가능하다.

또한 그 변수를 변경 가능하다. 

fun printProblemCounts(response: Collection<String>){
    var clientErrors = 0
    var serverErrors = 0

    response.forEach {
        if(it.startsWith("4")){
            clientErrors++
        }else if (it.startsWith("5")){
            serverErrors++
        }
    }
    println(" $clientErrors client errors $serverErrors server errors")
}

fun main() {

   val errors = listOf("200 Ok", 
   "403 Forbidden",  "404 Not found", "418 i'm a teapot", 
   "500 Internal Server Error")
   
    printProblemCounts(errors)
}

 

외부 변수를 람다가 포획한 변수라고 부른다.

 

- 일반적으로 함수 안에 정의한 로컬 변수는 생명주기는 함수가 반환되면 생명이 끝난다. 하지만 어떤 함수가 자신의 로컬 변수를 포획한 람다를 반환하거나 다른 변수에 저장한다면 로컬 변수의 생명주기가 달리 질 수 있다.

 

val (final) - 파이널 변수를 포획한 경우 람다 코드를 변숫값과 함께 저장한다.

var  - 파이널 변수가 아닐 경우 특별한 래퍼로 감싸서 나중에 변경하거나 읽을 수 있게 한 다음 래퍼에 대한 참조를 람다 코드와 저장한다.

class Ref<T>(var value: T)

fun main() {
    val counter = Ref(0)
    val inc = { counter.value++ }
}

실제 코드에서는  이런 래퍼를 만들 필요는 없지만 작동하는 원리는 위에 코드와 같다고 보면 된다.

 

* 람다를 이벤트 핸들러나 다른 비동기적인 실행으로 되는 코드를 활용할 때 함수가 종료된 후 다음에 로컬 변수를 변경할 경우인데.

fun tryCountButtonClicks(button: Button): Int{
    var clicks = 0
    button.onClick{ clicks++ }
    return clicks
}

이경우 항상 0이 반환된다 실제로 이벤트는 이 함수가 종료된 후에 발생되면 clicks를 증가시키기는 건 가능하지만 관찰을 할 수는 없다.  관찰이 필요하다면 클래스의 프로퍼티나 전역 프로퍼티로 선언을 해야 된다.

멤버 참조

람다를 사용해 코드 조각을 다른 함수의 인자로 넘기는 것을 봤는데 넘기려고 하는 함수가 미리 선언되었는 경우에 대해 알아보자

 

코틀린에서는 자바 8과 마찬가지로 함수를 값으로 변경할 수 있다.  이때 이중 콜론 ( :: )을 사용한다.

val getAge = Person::age

:: 를 사용하는 식을 멤버 참조라 부른다 멤버 참조는 프로퍼티나 메서드를 단 하나만 호출하는 함수를 값으로 생성한다.

즉,  person::age == { p.age } 와 동일하다

peoples.maxBy(Person::age)
peoples.maxBy{p -> p.age}
peoples.maxBy{it.age}

람다가 인자를 여럿인 다른 함수(sendEmail)한테 작업을 위임하는 경우 람다(action)를 정의 하지 않고 직접 위임 함수에 대한 참조(::sendEmail)를 제공하면 편리하다.

 val action = { person: Person, message: String -> sendEmail(person, message)}
 val nextAction = ::sendEmail

생성자 참조를 사용하면  클래스 생성작업을 연기하거나 저장해 둘 수 있다.

data class Person(val name:String, val age: Int)

fun main() {
    
    val createPerson = ::Person

    val person = createPerson("Alice",29)
    println(person)
}

바운드 멤버 참조

 

- 멤버 참조를 생성할 때 클래스의 인스턴스를 함께 저장한 다음 그 인스턴스에 대한 멤버를 호출해준다. 따라서 호출 시 수신 대상 객체를 별로로 지정해 줄 필요가 없다.

fun main() {
    val p = Person("Dmitry",34)

    val personsAgeFunction = Person::age
    println(personsAgeFunction(p))

	// p에 대한 참조를 저장했기에 인자 없이 호출해도 34을 출력한다. 
    val dmitryAgeFunction = p::age
    println(dmitryAgeFunction())

}