Action Kotlin 4 - 람다로 프로그래밍

Lambda

Lambda 란?

다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다.

람다를 이용하여 불필요한 코드를 제거하기

버튼에 onClick리스너를 구현한다고 가정해보자.
java에서는 다음과 같이 구현할 수 있다.

button.setOnClickListener(new OnClickListener() {
    @override
    public void onClick(View view){
        /* 수행할 동작 */
    }
})

무명 내부 클래스를 선언하느라, 코드가 복잡해진 것을 볼 수 있다.

여기서 무명 내부 클래스new OnClickListener() ... 부분을 의미한다.

kotlin에서는 다음과 같이 구현이 가능하다.

button.setOnClickLinstener{/* 수행할 동작 */}

두 코드 다 같은 역할을 수행하지만, kotlin에서 훨씬 간결하고 읽기 쉽다.

람다식을 사용함으로써, 무명객체와 같은 역할을 하지만, 간결하고 읽기 쉽게 만들어 준다.

컬렉션 검색하기

예를 들어 이름과 학번을 저장해두는 데이터 클래스가 있다고 가정해보자.

data class student(val name : String, val number : Int)

학생들로 이루어진 배열이 있고, 해당 배열에서 가장 학번이 높은 사람을 찾고 싶다.
만약 lambda를 사용하지 않는다면,

for(student in students) {
    /* 학번 비교를 통해 연장자를 찾는 코드 */
}

다음과 같이 연장자를 찾는 loop를 작성해야 한다.

이러한 코드는 연산자를 잘못 사용하게 되는 경우 문제가 발생할 수 있다.

Kotlin에서는 이러한 상황을 방지하기 위한 라이브러리 함수가 있다.

val students = listOf(student("A",2000000), student("B",2000001))
println(students.maxByOrNull{it.number})

maxByOrNull을 사용하면 된다.

멤버 참조를 이용한 컬렉션 검색

println(students.maxByOrNull(student::number))

위의 코드처럼 멤버참조를 통해서도 컬렉션 검색이 가능하다.

람다 식 문법

{x : Int, y : Int -> x + y}

다음과 같은 람다 식이 있을 때
x: Int, y : Int 는 파라미터이다. x+y는 본문이다. 이 둘은 ->를 통해 구분이 된다.

Kotlin에서 람다 식은 일급 객체이므로, 변수에 할당할 수 있다.

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

run을 이용한 실행

블록으로 작성한 코드를 실행할 필요가 있다면, run을 사용하면 된다.

람다 안에서 밖의 로컬 변수 변경하기

lambda에 장점은 밖의 로컬 변수가 final이 아니라면 접근할 수 있다.

예를 들어 오류 횟수를 측정하는 코드를 작성했다고 했을 때

fun problemCounts(responses : Collection<String>){
    var clientError = 0
    var serverError = 0
    responses.forEach{
        if(it.startWith("4")) clientError++
        else if(it.startWith("5")) serverError++
    }
}

람다 안에서 사용된 clientErrorserverError와 같은 변수를 포획한 변수라고 한다.

filter 와 map

filter

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

예를들어 숫자가 담긴 배열에서 짝수값만 얻고 싶다면 아래와 같이 작성한다.

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

위 코드는 주어진 술어 (it % 2 == 0)를 만족하는 원소만으로 이루어진 컬렉션을 반환한다.

filter는 값을 변환할 수 없다. filter는 술어를 만족하는 값만 가져오며 즉, 조건에 맞지 않는 원소는 제거하는 것이다.

만약 값을 변환하고 싶다면, map을 사용해야 한다.

map

아까 위의 예시 코드에서 numbers 배열의 각 값을 제곱하고 싶다면 다음과 같이 작성하면 된다.

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

map은 원본 리스트의 원소 개수는 같지만, 주어진 함수에 따라 변환된 새로운 컬렉션이다.

filter 와 map 같이 쓰기

filtermap을 같이 쓸 수도 있다.
짝수인 원소를 찾아 제곱한 컬렉션을 얻고 싶다면, 아래와 같이 작성할 수 있다.

numbers.filter{it % 2 == 0}.map{it * it}

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

all, any

  • 컬렉션에 자주 수행하는 연산으로, 컬렉션의 모든 원소가 어떤 조건을 만족하는지 판단하는 연산이다.
  • all : 모든 원소가 술어를 만족해야 true를 반환한다.
  • any : 하나라도 만족하는 원소가 있으면 true를 반환한다.

count

  • 조건을 만족하는 원소의 개수를 반환한다.

filtersize를 통해 특정 조건을 만족하는 경우를 찾을 수 있는데?

위와 같은 의문이 들 수도 있다. 하지만 filter는 컬렉션을 반환한다. 위와 같이 처리하게 되면, 조건을 만족하는 모든 원소가 담긴 컬렉션이 생기게 된다. 반면 count는 특정 조건을 만족하는 개수만을 추적하기 때문에 count가 더 효율적이다.

find

  • 조건을 만족하는 첫 번째 원소를 반환한다.

GroupBy

컬랙션의 모든 원소를 특정한 조건에 따라 여러 그룹으로 나누고 싶을 때 사용한다.
groupBy의 결과는 Map<Int, List<>> 형태로 반환이 된다.

flatMap과 flatten

flatMap

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

>> val strings = listOf("abc", "def")
>> strings.flatMap{it.toList()}
[a,b,c,d,e,f]

결과가 [a,b,c,d,e,f]가 왜 나오는지 과정을 통해 설명하면,

가장 먼저, 모든 객체에 toList()를 적용한다. 문자열에 toList()를 적용하게 되면, 문자로 이루어진 리스트가 생긴다.

그리고 각각의 리스트는 한 리스트로 모아지게 된다.

flatten

특정 조건 없이 그냥 리스트를 펼치고만 싶다면, flatten()을 사용하면 된다.

lazy (지연 계산)

val list = listOf(1, 2, 3, 4, 5) // 일반적인 컬렉션 사용 
val resultList = list
                .map { it * 2 } // 임시 컬렉션 생성 
                .filter { it % 3 == 0 } // 또 다른 임시 컬렉션 생성

다음과 같은 filtermap을 통해 새로운 컬렉션을 얻는 코드를 작성했을 때,
임시 컬렉션은 filter map각각의 연산의 결과로 2개가 생기게 된다.

지금은 원소의 개수가 5개라 큰 문제가 발생하지 않는다.

만약에 수백만개의 원소를 담는 리스트라면, 효율성이 떨어지게 된다.
이를 해결하기 위해 asSequence()를 사용한다.

asSequence()는 원본 컬렉션을 시퀀스로 변환하고, 마지막 연산이 발생할 때만 컬렉션이 생긴다.

처음 작성한 코드를 다음과 같이 작성이 가능하다.

// 시퀀스 사용
val resultSequence = list.asSequence() 
                        .map { it * 2 } // 임시 컬렉션 생성되지 않음 
                        .filter { it % 3 == 0 }// 임시 컬렉션 생성되지 않음 
                        .toList() // 최종 결과를 리스트로 변환

with과 apply

with

with은 연속적인 호출을 보다 깔끔하게 하기 위해 사용이 된다.

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

fun main() {
    val person = Person("John", 25)

    val description = with(person) {
        age += 1  // person.age += 1 과 동일
        "Name: $name, Age: $age"  // 마지막 표현식이 with의 결과값이 됩니다.
    }
    println(description)  // 출력: Name: John, Age: 26
}

person에 대한 연속적인 연산을 수행하고, 가장 마지막 표현식이 description에 저장이 된다.

apply

applywith과 같은 역할을 하지만 with은 마지막 표현식이 반환되지만, apply는 전달된 객체가 다시 반환된다.

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

fun main() {
    val person = Person("Alice", 25)

    val withResult = with(person) {
        age += 1
        "Name: $name, Age: $age"  // 이 문자열이 withResult에 할당됩니다.
    }

    val applyResult = person.apply {
        age += 1
        name = "Bob"
        // 여기에 특정 값을 작성하더라도, person 객체 자체가 applyResult에 할당됩니다.
    }

    println(withResult)     // 출력: Name: Bob, Age: 27
    println(applyResult)   // 출력: Person(name=Bob, age=27)
    println(person) // 출력 : Person(name=Bob, age=27)
    println(applyResult == person) // 출력 : true
}

위의 결과의 차이를 보면, 확실히 알 수 있다.
with의 결과는 마지막 표현식인 Name: Bob, Age: 27이 반환이 되고
applyage값에 1이 더해지게 되고, nameBob으로 수정된 person객체가 반환이 된다.

물론 apply에 의해 기존의 person객체의 내부의 값도 바뀌게 된다.

applyResultperson에 값을 적용하고 전달된 객체가 다시 반환되므로,
applyResult == persontrue이다.