코틀린학습 - #4 람다

2020. 11. 16. 14:31개발&TIL/kotlin

람다 식과 멤버 참조

lambda expression - 기본적으로 다름 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.

무명 내부 클래스로 선언해서 사용하던 방식을 함수형 프로그래밍으로 전환하여 함수를 값처럼 다르는 접근 방법을 택하는것

  • 무명 내부 클래스로 리스너 구현하기

    button.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View view) {
          /* TODO */
      }
    });
  • 람다로 리스너 쿠현하기

    button.setOnClickListener { /* TODO */ }

람다와 컬렉션

  • 람다를 사용해 컬렉션 검색하기 (가장 나이 많은 사람 찾기)
val people = listOf(Person("Allice", 29), Person("Bob", 31))
println(people.maxBy { it.age })
//result: Person(name=Bob, age=31)

//정식으로 람다로 작성한 코드  
println(people.maxBy({ p: Person -> p.age }))  
//result: Person(name=Bob, age=31)

모든 컬렉션에서 maxBy 사용가능, 내부에서 it이 컬렉션의 원소의 인자를 가르킨다.

람다 식의 문법

람다를 따로 선언해서 변수에 저장하기도 하지만 함수에 인자로 넘기로 바로 람다를 정의하는 경우가 대부분이다.

코틀린 람다 식은 항상 중괄호에 둘러싸여 있다.

{ x: Int, y: Int -> x + y } // {파라미터 -> 본문}

코드의 일부분을 블록으로 둘러싸 실행할 필요가 있다면 run을 사용한다.

  • run은 인자로 받은 람다를 실행해 주는 라이브러리 함수다.

    run { println(42) }

함수 호출시에 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다는 문법 관습이 있다.

  • 코드의 축약 절차 보기
//정식으로 람다로 작성한 코드 
people.maxBy({ p: Person -> p.age })

//함괄호 밖으로 빼낼 수 있다는 문법 관습이 있다.  
people.maxBy() { p: Person -> p.age }

//어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썻다면 호출시 빈괄호 삭제 가능  
people.maxBy { p: Person -> p.age }

//파라미터 타입을 생략  
people.maxBy { p -> p.age }

//디폴트 이름(it)으로 변경  
people.maxBy { it -> age }

//람다의 파라미터가 하나뿐이고 그 타입이 추론가능하면 it 바로 사용가능  
people.maxBy { it.age }

it을 사용하는 관습은 코드를 아주 간결하게 만들어준다. 하지만 복잡한 중첩 람다에서는 명시하는게 좋다. (복잡하니까)

람다를 변수에 저장할 때는 파라미터의 타입을 추론할 문맥이 없으므로 파라미터 타입을 명시해야 한다.

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

여러줄로 이뤼진 람다의 경우 맨 마지막에 있는 식이 람다의 결과이다.

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

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

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

  • 함수 파라미터를 람다 안에서 사용하기

    fun printMessageWithPrefix(messages: Collection<String>, prefix: String) {
      messages.forEach { 
          println("$prefix $it")
      }
    }

val errors = listOf("403 Forbidden", "404 Not Found")
printMessageWithPrefix(errors, "Error:")


자바와 다르게 람다 안에서는 변수에 접근이 가능하고 바깥의 변수를 변경해도 된다.
```kotlin
fun printProblemCouns(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach { 
        if (it.startsWith("4")) {
            clientErrors++  //람다 밖의 변수 변경
        } else if (it.startsWith("5")) {
            serverErrors++  //람다 밖의 변수 변경
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}
val responses = listOf("200 OK", "418 I`m a teapot", "500 Internal Server Error")
printProblemCouns(responses)

람다를 실행 시점에 표현하는 데이터 구조는 람다에서 시작하는 모든 참조가 포함된 닫힌(closed) 객체 그래프를
람다 코드와 함께 저장해야 한다. 이런 데이터 구조를 클로저(Closure)라고 부른다.

멤버 참조

자바 8과 마찬가지로 함수를 값으로 바꿀 수 있다. 이때 이중 콜론(::)을 사용한다.

:: 을 사용하는 식을 멤버 참조(member reference) 라고 부른다.

멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어 준다.
멤버 참조 뒤에는 괄호를 넣으면 안된다.

  • 멤버 참조 문법

    클래스 :: 멤버
    Person::age

val getAge = { person: Person -> person.age } //위와 같다.


- 최상위 선언된 함수나 프로퍼티를 참조 할 수 있다. 
```kotlin
fun salute() = println("salute!")
run(::salute)

생성자 참조(constructor reference) 를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.
::뒤에 클래스이름을 넣으면 생성자 참조를 만들 수 있다.

  • 생성자 참조 예제
data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)
println(p)

//확장함수 멤버 참조 예제  
fun Person.isAdult() = age >= 21  
val predicate = Person::isAdult
  • 바운드 멤버 참조
    코틀린 1.0에서는 클래스의 메소드나 프로퍼티에 대한 참조를 얻은 다음에 그 참조를 호출할 때
    항상 인스턴스 객체를 제공해야 핬다.

코틀린 1.1부터는 바운드 멤버 참조를 지원한다.

바운드 멤버 참조를 사용하면 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대한 멤버를 호출해 준다.

val p = Person("Dmitry", 30)
val personAgeFunction = Person::age
println(personAgeFunction(p)) /* 인자로 받은 사람의 나이를 반환 */

val dmitryAgeFunction = p::age //바운드 멤버 참조
println(dmitryAgeFunction()) /* 바운드 참조로 만든 p가 가르키던 사람의 나이를 반환 */

필수적인 함수: filter와 map

filter - 컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다.

val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 })

map - 컬렉션의 각 원소를 변환할때 map 함수를 사용한다.

val list = listOf(1, 2, 3, 4)
println(list.map { it * it })

val people = listOf(Person("Bob", 29), Person("Alice", 31))
println(people.map { it.name })

println(people.filter { it.age > 30 }.map(Person.name))

맵의 경우 키와 값을 처리하는 함수가 따로 존재 환

filterKey와 mapKey 는 키를 걸러내거나 변환

filterValue와 mapValue 는 값을 걸러내거나 변환

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

모든 원소가 술어를 만족하는지 궁금하다면 all, 술어를 만족하는 원소가 하나라도 있는지 궁금하면 any

val canBeInClub27 = { p: Person -> p.age <= 27 }
val people3 = listOf(Person("Bob", 29), Person("Alice", 27))
println(people3.all(canBeInClub27))
//false
println(people3.any(canBeInClub27))
//true

count 함수가 있다는 사실을 잊어버리고 size() 로 크기를 가져오면 중간에 컬렉션이 추가로 생긴다.
갯수만 세고 싶다면 count 함수를 이용

find 함수를 이용해서 만족하는 원소를 하나만 찾고 싶을때 사용
find = firstOrNull 과 같다 더 명확하게 사용하고 싶으면 firstOrNull를 사용해라.

val canBeInClub27 = { p: Person -> p.age <= 27 }
val canBeInClub10 = { p: Person -> p.age <= 10 }
val people4 = listOf(Person("Bob", 29), Person("Alice", 27))
println(people4.count(canBeInClub27))
//1

println(people4.find(canBeInClub27))
//Person("Alice", 27)

println(people4.firstOrNull(canBeInClub10))
//null

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

val people5 = listOf(Person("Alice", 31), Person("Bob", 28), Person("Mark", 28))
println(people5.groupBy { it.age })

groupBy의 결과타입은 Map<Int, List> 이다.

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

flatMap 함수는 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고 람다를 적용한 결과
얻어지는 여러 리스트를 한 리스트로 한데 모은거나 펼친다(flatten)

val strings = listOf("abc", "def")
println(strings.toList())

//[abc, def]

strings.flatMap {
    println(it.toList())
    it.toList()
}

//[a, b, c]
//[d, e, f]

println(strings.flatMap { it.toList() })

//[a, b, c, d, e, f]

flatten 함수 를 사용하면 리스트의 리스트를 모든 중첩된 리스트의 한 리스트로 모을때 사용할 수 있다.

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

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

people.map(Person::name).filter { it.startWith("A") }

filter, map 은 리스트를 반환한다. 위의 경우 새로운 리스트 2개를 만든다.

val people6 = listOf(Person("Alice", 31), Person("Bob", 28), Person("Mark", 28))
println(people6.asSequence() //원본 컬렉션을 시퀀스로 변환
    .map(Person::name)      //시퀀스도 컬렉션과 같은 api 제공
    .filter { it.startsWith("A") }
    .toList()) //결과를 다시 리스트로 변환

시퀀스의 원소는 필요할 때 비로소 계산된다. 따라서 중간 처리 결과를 저장하지 않는다.
큰 컬렉션에 대해서 연산을 연쇄시킬 때 시퀀스를 사용하는 것이 좋다.

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

               ---중간연산---           최종연산
sequence.map { ... }.filter { ... }.toList()

중간 연산은 항상 지연 계산되어 결과를 얻을 필요가 있을때 적용된다.
따라서 toList() 가 없다면 아무것도 출력되지 않는다.

  • 최종 연산이 없는 예제
listOf(1, 2, 3, 4).asSequence()
    .map { print("map($it)"); it * it }
    .filter { print("filter($it)"); it % 2 == 0 }

//아무 결과도 나오지 않는다.
  • 최종 연산이 있는 예제
listOf(1, 2, 3, 4).asSequence()
    .map { print("map($it)"); it * it }
    .filter { print("filter($it)"); it % 2 == 0 }
    .toList()

//map(1)filter(1)map(2)filter(4)map(3)filter(9)map(4)filter(16)

일반적으로 map, filter는 각 단계별로 처리되지만 스퀀스에서 map과 filter 처리등의 모든 연산의 각 원소에 대해 순차적으로 적용된다
아래 코드의 경우 일반 컬렉션을 수행하면 map 단계에 제곱된 모든 값이 반환되고 그결과를 가지고 find를 하지만
시퀀스의 경우는 1을 제곱하고 find, 2를 제곱하고 find() 후에 끝난다.

listOf(1, 2, 3, 4).asSequence()
    .map { print("map($it)"); it * it }
    .find { it > 3 }

map과 filter를 어떤 순서로 수행햐느냐에 따라 수행횟수가 달라질 수 있다.

  • 직접 시퀀스 만들기 예제

    va naturalNumbers = generateSequence(0) { it + 1 }
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
    println(numbersTo100.sum())

자바 함수형 인터페이스 활용

단 하나의 추상메소드만 존재하는 인터페이스를 함수형 인터페이스 또는 SAM 인터페이스 라고 부른다.
SAM(single abstract method) 은 단일 추상 메소드이다.

SAM일때 코틀린은 해당 java api 인자에 람다를 넣을 수 있다.

button.setOnClickListener { view -> ... }

함수형 인터페이스를 인자로 원하는 자바 메소드에도 코틀린 람다를 전달 가능

코틀린에서의 인라인(inline) 되지 않은 모든 람다 식은 무명 클래스로 컴파일된다. 현재는..

람다에서 무명 객체와 달리 인스턴스 자신을 가르키는 this가 없다.
따라서 람다를 변환한 무명 클래스의 인스턴스를 참조할 방법이 없다.
람다는 코드 블록일 뿐이고, 객체가 아니므로 객체처럼 람다를 참조 할 수 없다.
람다 안에서 this는 그 람다를 둘러싼 클래스의 인스턴스를 가르킨다.

with 함수

어떤 객체의 이름을 반복하지 않고도 그 객체에 대해 다양한 연사을 수행 하도록 돕는다.

  • 알파벳 만들기 예제(result 를 여러번 사용한다.)
fun alphabet() : String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
        result.append(letter)    
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}

//ABCDEFGHIJKLMNOPQRSTUVWXYZ  
//Now I know the alphabet!
  • with를 사용해 알파벳 만들기
fun alphabet2() : String {
    val stringBuilder = StringBuilder()
    return with(stringBuilder) {
        for (letter in 'A'..'Z') {
            this.append(letter)    
        }
        append("\nNow I know the alphabet!") //this 생략 
        this.toString()
    }
}

//ABCDEFGHIJKLMNOPQRSTUVWXYZ  
//Now I know the alphabet!

with 함수는 2개의 파라미터를 가지고 첫번째는 인자, 두번째는 람다이며
람다를 괄호 밖으로 빼는 관례를 사용해서 특별한 구분처럼 보인다.

  • with와 식을 본문으로 하는 함수를 활용해 알파벳 만들기
fun alphabet3() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        this.append(letter)    
    }
    append("\nNow I know the alphabet!") //this 생략 
    this.toString()
}

//ABCDEFGHIJKLMNOPQRSTUVWXYZ  
//Now I know the alphabet!

apply 함수

apply 함수는 거의 with와 같다. 유일한 차이는 apply 함수는 자신에게 전달된 객채를 반환한다.

  • apply 함수를 사용해 알파벳 만들기
fun alphabet4() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)    
    }
    append("\nNow I know the alphabet!")
}.toString()

//ABCDEFGHIJKLMNOPQRSTUVWXYZ  
//Now I know the alphabet!  
728x90

'개발&TIL > kotlin' 카테고리의 다른 글

코틀린학습 - #6 오버로딩  (0) 2020.11.17
코틀린학습 - #5 타입 시스템  (0) 2020.11.17
코틀린학습 - #3 클래스  (0) 2020.11.11
코틀린학습 - #2 함수  (0) 2020.11.09
코틀린학습 - #1 기초  (0) 2020.11.09