ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin in Action] 람다로 프로그래밍
    프로그래밍 언어/kotlin 2021. 7. 26. 16:20
    728x90
    반응형

    * 람다 :  다른 함수로 넘길 수 있는 작은 코드조각

    공통 코드구조를 라이브러리 함수로 뽑아낼 수 있다.

     

    5.1 람다 식과 멤버 참조

    람다 : 코드블록을 함수의 인자로 넘기기

    함수를 값처럼 사용. 함수를 직접 다른 함수에 전달. 

    button.setOnClickListener(new OnClickListener() { // 무명 내부 클래스 선언
        @Override
        public void onClick(View view) {
        
        }
    })

    람다는 메소드가 하나뿐인 무명 객체를 대신할 수 있다.

     

    람다와 컬렉션

    • 람다를 사용해서 컬렉션 검색
    people.maxBy{ it.age }

    it.age -> 비교에 사용할 값을 돌려주는 함수 

    it 은 컬렉션의 원소를 가리킨다.

    • 멤버 참조를 사용해 컬렉션 검색
    people.maxBy(Person::age)

    함수나 프로퍼티를 반환하는 역할을 하는 람다는 멤버 참조로 대치할 수 있다.

    • 정식 람다 작성 
    people.maxBy({ p: Person -> p.age })

    Person 타입 값을 인자로 받아서 인자의 age를 반환한다.

    코틀린에서는 함수 호출시 마지막 인자가 람다식이면 그 람다를 괄호 밖으로 뺄 수 있다. 

    람다가 유일한 인자라면 괄호를 제거해도 된다.

    람다 파라미터 타입 제거 가능하다.

    people.maxBy { p -> p.age }

    파라미터 이름을 it으로 만들면 람다 식이 더 간단해진다.

    people.maxBy { it.age }

     

    람다 식 문법

    람다를 변수에 저장할 때는 파라미터 타입을 추론할 문맥이 존재하지 않기 때문에, 타입을 명시해야한다.

     val sum = { x: Int, y: Int -> x + y }

    람다식 직접 호출도 가능

    { println(42) }()

    그치만 이렇게 람다를 만들자 마자 바로 호출하느니 그냥 람다 본문을 직접 실행하는 편이 낫다.

    코드의 일부분을 블록으로 둘러싸 실행할 필요가 있다면 run을 사용한다. run 은 인자로 받은 람다를 실행해주는 라이브러리 함수다.

    run { println(42) }

    여러 줄로 이루어진 람다의 경우 본문 맨 마지막에 있는 식이 람다의 결과 값이 된다.

    val sum = { x: Int, y: Int ->
        println("Computing the sum of $x and $y...")
        x + y
    )

    변수 포획

    람다를 함수 안에서 정의하면 함수의 파라미터, 람다 정의 앞에 선언된 로컬 변수까지 모두 사용할 수 있다.

    람다 안에서 final 변수가 아닌 변수에 접근할 수 있다. 또한, 바깥 변수를 변경해도 된다.

    만약, 로컬 변수를 람다를 반환하거나 변수에 저장하게 되면, 로컬 변수의 생명 주기와 함수의 생명주기가 달라질 수 있다. 

    • final 변수를 포획한 경우 - 람다 코드를 변수 값과 함께 저장
    • final이 아닌 변수를 포획한 경우 - 변수를 특별한 래퍼로 감싸서 나중에 변경하거나 읽을 수 있게 한 다음, 래퍼에 대한 참조를 람다 코드와 함께 저장
    fun printProblemCounts(responses: Collection<String>) {
        var clientErrors = 0 // 람다가 포획한 변수
        var serverErrors = 0 // 람다가 포획한 변수
        responses.forEach {
        	if (it.startWith("4")) {
            	clientErrors++
            } else if (it.startWith("5")) {
            	serverErrors++
            }
        }
    }

    람다를 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용하는 경우, 함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다

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

    이 함수는 항상 0을 반환한다. tryToButtonClicks 함수는 clicks 값의 변경을 관찰할 수 없다. clicks 변수를 함수 바깥에 클래스 프로퍼티나 전역 프로퍼티로 빼내서 나중에 변수 변화를 살펴볼 수 있게 해야한다. 

    멤버 참조

    넘기려는 코드가 이미 함수로 선언된 경우, 이 함수를 직접 다른 함수의 인자로 넘길 수는 없을까? -> 함수를 값으로 바꾸자.

    var getAge = Person::age

    멤버참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다.

    최상위에 선언된(다른 클래스의 멤버가 아닌)함수나 프로퍼티를 참조할 수도 있다.

     

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

    val action = { 
        person: Person, 
        message: String -> sendEmail(person, message) // sendEmail 함수에게 작업위임
    }
    
    val nextAction = ::sendEmail // 람다 대신 멤버 참조를 쓸 수 있다.

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

    val createPerson = ::Person // Person의 인스턴스를 만드는 동작을 값으로 저장한다.
    val p = createPerson("Alice", 29)
    print(p)
    // result : Person(name=Alice, age=29)

    확장 함수도 멤버 함수와 똑같은 방식으로 참조가 가능하다.

    fun Person.isAdult() = age >= 21
    val predicate = Person::isAdult

     

    5.2 컬렉션 함수형 API

    함수형 스타일로 컬렉션 다루기

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

    filter 와 map

    • filter 함수 : 컬렉션을 이터레이션 하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다.
    println(list.filter { it % 2 == 0 })
    • map 함수 : 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 만든다
    println(list.map { it * it })

    멤버 참조를 사용

    println(people.map {People::name})

    예를 들어, 가장 나이많은 사람의 이름을 알고싶다.

    people.filter{ it.age == people.maxBy(People::age)!!.age }

    이는 100번의 최대값 연산을 하므로 비효율적이다. 아래는 한번만 계산하도록 만든 코드이다.

    val maxAge = people.maxBy(People::age)!!.age
    people.filter { it.age == maxAge }

     

    필터와 변환 함수를 맵에 적용할 수도 있다.

    맵은 key 와 value 값을 처리하는 함수가 각각 따로 존재한다.

     

    all, any, count, find : 컬렉션에 술어 적용

    val canBeInClub27 = { p: Person -> p.age <= 27 }
    • all, any : 컬렉션의 모든 원소가 어떤 조건을 만족하는지 판단하는 연산
    people.all(canBeInClub27)
    people.any(canBeInClub27)

    어떤 조건에 대해 any와 !all의 결과는 같다. !any와 all의 결과도 같다. 

    • count : 조건을 만족하는 원소의 개수를 반환
    people.count(canBeInClub27)

    size는 조건을 만족하는 모든 원소가 들어가는 중간 컬렉션이 생긴다.

    반면, count는 조건을 만족하는 원소의 갯수만을 추적하기 때문에, 훨씬 효율적이다.

    people.filter(canBeInClub27).size
    • find : 조건을 만족하는 첫번째 원소를 반환

     

    groupBy: 리스트를 여러 그룹으로 이뤄진 맵으로 변경

    컬렉션의 모든 원소를 어떤 특성에 따라 여러 그룹으로 나누고 싶다.

    연산의 결과는 컬렉션의 원소를 구분하는 특성이 키이고, 키 값에 따른 각 그룹이 값인 맵이다.

    각 그룹은 리스트이다. 결과 타입은, Map<Int, List<Person>> 이다.

     

    flatMap과 flatten : 중첩된 컬렉션 안의 원소 처리

    class Book(val title: String, val authors: List<String>)

    books 컬렉션에 있는 책을 쓴 모든 저자의 집합

    books.flatMap { it.authors }.toSet()

    먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고, 람다를 적용한 결과 얻어지는 여러 리스트를 한 리스트로 한데 모은다.

     

    5.3 지연 계산(lazy) 컬렉션 연산

    map이나 filter같은 함수는 결과 컬렉션을 즉시(eagerly) 생성한다. 

    시퀀스를 사용하면 중간 컬렉션 결과를 임시로 저장하지 않고도 컬렉션 연산을 연쇄할 수 있다.

    people.asSequence() // 컬렉션 -> 시퀀스
        .map(Person::name)
        .filter { it.startWith("A") }
        .toList() // 시퀀스 -> 컬렉션

    시퀀스의 원소는 필요로 할 때 계산된다. 따라서, 중간 처리 결과를 저장하지 않고도 연산을 연쇄적으로 적용해서 효율적으로 계산을 할 수 있다.

    시퀀스는 다시 컬렉션으로 되돌려야 한다(위에서 toList()). 시퀀스의 원소를 하나하나 iteration 해야하면 상관없지만, 인덱스를 사용해서 접근하는 등 다른 API 메서드가 필요하다면 시퀀스를 리스트로 변환해야 한다.

     

    시퀀스 연산 실행: 중간 연산과 최종 연산

    • 중간 연산 : 다른 시퀀스를 반환 ( 항상 지연 계산 )
    • 최종 연산 : 연산 결과를 반환. 결과는 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터 일련의 계산을 수행해 얻을 수 있는 컬렉션이나 원소, 숫자 또는 객체다. 
    • 컬렉션에 대한 map, filter 연산 구현 : 각 원소에 map 함수에 대해 수행해서 시퀀스 얻음 -> 그 시퀀스에 대해 다시 filter 수행
    • 시퀀스에 대한 map, filter : 모든 연산이 각 원소에 대해 순차적으로 적용

    시퀀스 만들기

    generateSequence 함수를 사용하여 시퀀스를 만들 수 있다. 이 함수는 이전의 원소를 인자로 받아 다음 원소를 계산한다.

    val naturalNumbers = generateSequence(0) { it + 1 } // 시퀀스
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 } // 시퀀스
    println(numbersTo100.sum()) // sum()이 최종연산

    객체의 조상으로 이뤄진 시퀀스를 만들어내는 것이다. 모든 조상의 시퀀스에서 어떤 특성을 알고싶을 때가 있다. 

    첫번째 원소를 지정하고 시퀀스의 한 원소로부터 다음 원소를 계산하는 방법을 제공하면서 시퀀스를 만든다.

    fun File.isInsideHiddenDirectory() =
        generateSequence(this) { it.parentFile }.any { it.isHidden }
    >>> val file = File("/Users/svtk/.HiddenDir/a.txt")
    >>> println(file.isInsideHiddenDirectory)
    // result : true

     

    5.4  자바 함수형 인터페이스를 코틀린에서 사용

    코틀린에서는 함수형 인터페이스를 인자로 취하는 자바 메소드를 호출할 때 (무명 클래스 인스턴스 대신) 람다를 넘길 수 있다.

    람다의 파라미터는 메소드의 파라미터와 대응한다.

    button.setOnClickListener { view -> ... }

     

    함수형 인터페이스/SAM 인터페이스 : 추상 메소드가 단 하나만 있는 인터페이스

    *SAM = 단일 추상 메소드

    자바 API는 Callable 과 Runnable과 같은 함수형 인터페이스와 그런 함수형 인터페이스를 활용하는 메소드가 많다. 무명 내부 클래스 인스턴스를 정의하고 활용할 필요가 없어 깔끔하다.

     

    자바 메소드에 람다를 인자로 전달

    Runnable 타입의 파라미터에 람다를 넘길 수 있다. 컴파일러는 자동으로 람다를 Runnable 인스턴스로 변환해준다.

    /* 자바 메소드 */
    void postponeComputation(int delay, Runnable computation);
    
    /* 코틀린 */
    postponeComputation(1000) { println(42) }

     

    Runnable을 구현하는 무명 객체를 명시적으로 만들어서 사용할 수도 있다.

    객체를 명시적으로 선언하는 경우, 메소드를 호출할때마다 새로운 객체가 생성된다.

    반면, 람다는 정의가 들어있는 함수의 변수에 접근하지 않는 람다에 대응하는 무명 객체를 메소드를 호출할 때마다 반복 사용한다.

    postponeComputation(1000, object : Runnable {
        override fun run() {
            println(42)
        }
    })
    
    val runnable = Runnable { println(42) }
    fun handleComputation() {
        postponeComputation(1000, runnable)
    }

    컬렉션을 확장한 메소드에 람다를 넘기는 경우, 코틀린은 람다에 대해 무명 클래스를 만들고 그 클래스의 인스턴스를 생성해 메소드를 넘기는 방식을 사용하지 않는다. 코틀린 inline으로 표시된 코틀린 함수에게 람다를 넘기면 아무런 무명 클래스도 만들어지지 않는다. 대부분 코틀린 확장 함수들은 inline 표시가 붙어있다. ( = inline 되지 않은 모든 람다식은 무명 클래스로 컴파일됨 )

     

    자바 함수형 인터페이스와 람다 사이의 변환을 수동으로 해야할 경우

    > SAM 생성자:  람다를 함수형 인터페이스로 명시적으로 변경

    예시로, 함수형 인터페이스의 인스턴스를 반환하는 메소드가 있다면, 반환하고싶은 람다를 SAM 생성자로 감싼다.

    SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다.

    fun createAllDoneRunnable(): Runnable {
        return Runnable { println("All done!") }
    }
    >>> createAllDoneRunnable().run()
    // result : All done!

    또, 람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장해야하는 경우에도 SAM 생성자를 활용할 수 있다. (재사용하고싶은 경우)

    val listener = OnClickListener { view ->
        val text - when (view.id) {
            R.id.button1 -> "First button"
            R.id.button2 -> "Second button"
            else -> "Unknown button"
        }
        toast(text)
    }
    button1.setOnClickListener(listener)

     

    5.5 수신 객체 지정 람다: with와 apply

    수신 객체 지정 람다 : 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드를 호출할 수 있게 하는 람다

     

    with 함수

    with : 어떤 객체의 이름을 반복하지 않고도 그 객체에 대해 다양한 연산을 수행할 수 있게 하는 라이브러리 함수

    with 함수의 첫번째 파라미터는 수신 객체이고 두번째 파라미터는 람다이다. this 없이도 수신 객체의 멤버 접근 가능하다.

    with 가 반환하는 값은 람다 코드를 실행한 결과이다. 

    fun alphabet(): String {
        val stringBuilder = StringBuilder()
        return with(stringBuilder) { // 메소드 호출하려는 수신 객체 지정
            for (letter in 'A'..'Z') {
                this.append(letter) // this를 명시해서 앞에서 지정한 수신 객체 메소드 호출
            }
            append("\nNow I know the alphabet!")
            this.toString() // 람다에서 값반환
        }
    }

    더 리팩토링해서 필요없는 stringBuilder 변수를 없애고 함수가 식의 결과를 바로 반환하게 할 수 있다. 

    fun alphabet() = with(StringBuilder()) {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nNow I know the alphabet!")
        toString()
    }

    with 함수 반환 결과가 람다 결과 대신 수신 객체가 필요한 경우, apply 라이브러리 함수를 사용할 수 있다.

     

    apply 함수

    with 랑 비슷하지만, 유일한 차이는 apply 는 항상 자신에게 전달된 객체를 반환한다는 점이다.

    객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화해야하는 경우 유용하다. 

    fun alphabet() = StringBuilder().apply {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nNow I know the alphabet!")
    }.toString()

    객체 초기화로 사용하는 예

    새로운 TextView인스턴스를 만들고 즉시 그 인스턴스를 apply에 넘긴다. apply에 전달된 람다 안에서 TextView가 수신 객체가 된다.

    원하는 대로 TextView의 메소드를 호출하거나, 프로퍼티를 설정할 수 있다. 

    fun createViewWithCustomAttributes(context: Context) =
        TextView(context).apply {
            text = "Sample Text"
            textSize = 20.0
            setPadding(10, 0, 0, 0)
        }

    표준 라이브러리 buildString 함수를 사용하여 단순화.

    buildString 함수는 StringBuilder객체 만드는 일과 toString 호출하는 일을 알아서 해준다.

    fun alphabet() = buildString {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nNow I know the alphabet!")
    }

     

    수신 객체 지정 람다는 DSL을 만들 때 매우 유용한 도구이다.(이후 11장에서 다룬다)

    728x90

    댓글

Designed by Tistory.