5. 람다로 프로그래밍
CS/코틀린 인 액션

5. 람다로 프로그래밍

가독성을 늘리기 위해 줄임말 문법을 계속 만들다보니 오히려 더 복잡해지는 감이 조금 있는 것 같다.

5.1 람다 식과 멤버 참조

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

그냥 만들어서 바로 실행할거면 인자로 받은 람다를 바로 실행해주는 run 라이브러리 함수를 실행하자.

{ println(42) }()
run { println(42) }

 

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

위 식들은 다 같은 역할을 하는 문법들이다. 너무 불필요하다 싶으면 IntelliJ에서 알려준다. 타입은 생략해도 추론할 수 있을 정도로 명확하면 생략해도 된다.

의문의 들만한건 람다 매개변수를 괄호 밖으로 왜 빼내도 되는건지 일 것이다. 마지막 매개변수가 람다식이라면 밖으로 빼내도 된다.

val people = listOf(Person("이몽룡", 29), Person("성춘향", 31))
val names = people.joinToString(separator = " ", transform = { p: Person -> p.name })
println(names)
val names2 = people.joinToString(" ") { p: Person -> p.name }
println(names2)

람다가 여러줄일 경우, 맨 마지막 줄을 return 한다.

 

람다를 함수 안에서 정의하면, 람다 밖의 함수 파라미터와 안의 변수들까지 사용 가능

fun printProblemCounts(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 getAge1 = { person: Person -> person.age }
val getAge2 = Person::age

val person = Person("Alice", 29)
println(getAge1(person))
println(getAge2(person))

위 두 개의 함수는 같은거임.

val action = { person: Person, message: String ->
    sendEmail(person, message)
}
val nextAction = ::sendEmail
data class Person(val name: String, val age: Int)

val createPerson = ::Person
val p = createPerson("Alice", 29)
println(p)

fun Person.isAdult() = age >= 21

println(p.isAdult())
val predicate_lambda = { p: Person -> p.isAdult()}
println(predicate_lambda(p))
val predicate = Person::isAdult
println(predicate(p))

그냥 매개변수만 그대로 전달해주는 것 같다.

 

5.2 컬렉션 함수형 API

다른 언어에도 있는 filter, map, all, any, count, find는 생략

 

groupBy는 평소 알고 있는 것과 같은 역할

val people = listOf(Person("Alice", 29), Person("Bob", 31), Person("Carol", 31))
println(people.groupBy { it.age })

 

flatMap과 flatten은 중첩된 컬렉션 안의 원소를 처리한다.

val strings = listOf("adc", "def")
println(strings.flatMap { it.toList() }) // [a, d, c, d, e, f]

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

val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
        Book("Mort", listOf("Terry Pratchett")),
        Book("Good Omens", listOf("Terry Pratchett",
                "Neil Gaiman")))
println(books.flatMap { it.authors }.toSet())

// [Jasper Fforde, Terry Pratchett, Neil Gaiman]

 

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

정의만 일단 해두고 계산은 나중에 하는 것이다. 컬렉션 뒤에 .asSequence()만 붙이면 된다. java의 stream과 같다고 함.

val people = listOf(
        Person("Alice", 29),
        Person("Bob", 31),
        Person("Carol", 31),
        Person("Dan", 21)
    )
val peopleAnames = people.asSequence()
    .map(Person::name)
    .filter { it.startsWith("A") }
    .toList()
println(peopleAnames) // ["Alice"]
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)

중요한 건 lazy 연산 시 정의만 되고, 꺼내는 함수가 실행될 때 원소 하나하나 순서대로 진행된다는 것. 즉, map을 적용한 리스트 저장 -> 필터 적용된 리스트 저장 -> 출력, 이 아니라 원래 리스트 원소 -> map적용 -> 필터 적용 으로 원소 하나를 끝까지 적용시키고 함.. 그래서 부가적인 메모리 저장공간이 필요 없어서 좋고, 미리 실행 안하니 자원낭비 안해서 좋고 한다는 것.

또 순서를 다르게 하면 같은 결과에서 더 좋아질 수도 있다.

 

asSequance 말고도 generateSequence도 있다고 한다. 이 함수는 이전의 원소를 인자로 받아 다음 원소를 계산한다.

 

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

// =======

fun File.isIfun File.isInsideHiddenDirectory() =
    generateSequence(this) { it.parentFile }.any { it.isHidden }

val file = File("/Users/svtk/.HiddenDir/a.txt")
println(file.isInsideHiddenDirectory()) // true

 

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

자바의 메서드에 코틀린의 람다를 전달하기 위해선 Runnable 클래스에 넣어주면 된다. 컴파일러가 이 역할을 자동으로 해주고 있었다.

// 자바
void postponeComputation(int delay, Runnable computation);
postponeComputation(1000) { println(42) }

 

컴파일러가 자동으로 못바꿔주는 경우, SAM 생성자를 사용해야 한다. SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 벼환할 수 있게 컴파일러가 자동으로 생성한 함수다.

fun createAllDoneRunnable() : Runnable {
    return Runnable { println("All done!") }
}

createAllDoneRunnable().run()
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)
button2.setOnClickListener(listener)

 

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

파이썬이랑 비슷함. 여러 번 쓰고 닫을 거 그냥 한번에 열고닫기 보여주는 용도

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

println(alphabet())

/*
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Now I know the alphabet!
*/
fun alphabet(): String {
    val result = StringBuilder()
    return with(result) {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nNow I know the alphabet!")
        toString()
    }
}

IntelliJ가 편하게 알려준다.

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

 

apply도 with와 거의 유사

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

사실 stringbuilder로 string으로 변환하여 반환해주는 표준 라이브러리의 buildString이 있었다.

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

'CS > 코틀린 인 액션' 카테고리의 다른 글

7. 연산자 오버로딩과 기타 관례  (0) 2023.05.03
6. 코틀린 타입 시스템  (0) 2023.04.30
4. 클래스, 객체, 인터페이스  (0) 2023.04.28
3. 함수 정의와 호출  (0) 2023.04.24
2. 코틀린 기초  (0) 2023.04.23