- 람다 식과 멤버 참조
- 함수형 스타일로 컬렉션 다루기
- 시퀀스: 지연 컬렉션 연산
- 자바 함수형 인터페이스를 코틀린에서 사용
- 수신 객체 지정 람다 사용
💡 익명 함수를 간단히 표현하는 함수로서, 값처럼 여러 곳에 전달할 수 있는 동작의 모음이다.
// 무명 내부 클래스로 리스너 구현하기
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
/* 클릭 시 수행할 동작 */
}
})
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(view: View) {
/* 클릭 시 수행할 동작 */
}
})
// 람다로 리스너 구현하기
button.setOnClickListener { /* 클릭 시 수행할 동작 */ }
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)
}
val people = list(Person("Alice", 29), Person("Bob", 31))
findTheOldest(people) // Person(name=Bob, age=31)
val people = listOf(Person("Alice" 29), Person("Bob", 31))
println(people.maxBy { it.age }) // 나이 프로퍼티를 비교해서 값이 가장 큰 원소 찾기
// 결과: Person(name=Bob, age=31)
{ it.age }
는 비교에 사용할 값을 돌려주는 함수다.people.maxBy(Person::age)
val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2)) // 3
{ println(42) }() // 42
굳이 람다를 만들자마자 바로 호출하느니 람다 본문을 직접 실행하는 편이 낫다. 이렇게 코드의 일부분을 블록으로 둘러싸 실행할 필요가 있다면 run을 사용한다.
✅ 람다 본문을 직접 실행하는 편이 낫다? : 람다를 만들면 객체가 생성되어 가비지 컬렉션의 대상이 된다. 그러나 바로 실행하면 객체가 생성되지 않으므로 가비지 컬렉션 부하가 감소한다.
run { println(42) } // 42
people.maxBy({ p: Person -> p.age })
코틀린에서는 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다는 문법 관습이 있다.
people.maxBy() { p: Person -> p.age }
people.maxBy { p: Person -> p.age }
fun operateOnNumbers(a: Int, b: Int, operation1: (Int, Int) -> Int, operation2: (Int, Int) -> Unit) {
val result = operation1(a, b)
println("Result of the operation: $result")
operation2(a, b)
}
fun main() {
// trailing lambda syntax를 이용하여 두 번째 람다를 함수 호출 밖으로 뺄 수 있음
operateOnNumbers(5, 3 ,{ x, y -> x + y }) { x, y -> println("Numbers: $x, $y") }
// 일반적인 함수 호출 구문을 사용하여 두 람다를 전달
operateOnNumbers(5, 3,
{ x, y -> x + y },
{ x, y -> println("Numbers: $x, $y") }
)
}
// 이름 붙인 인자를 사용해 람다 넘기기
val people = listOf(Person("이몽룡", 29), Person("성춘향", 31))
val name = people.joinToString(separator = " ",
transform = { p: Person -> p.name })
println(names) // 이몽룡 성춘향
// 람다를 괄호 밖에 전달하기
people.joinToString(" ") { p.person -> p.name }
// 람다 파라미터 타입 제거하기
people.maxBy { p: Person -> p.age } // 파라미터 타입을 명시
people.maxBy { p -> p.age } // 파라마티 타입을 생략(컴파일러가 추론)
// 디폴트 파라미터 이름 it 사용하기
people.maxBy { it.age }
it을 사용하는 관습은 코드를 아주 간단하게 만들어준다. 하지만 이를 남용하면 안된다. 특히 람다 안에 람다가 중첩되는 경우 각 람다의 파라미터를 명시하는 편이 낫다. 파라미터를 명시하지 않으면 각각의 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))
// Computingthe sum of 1 and 2...
// 3
// 함수 파라미터를 람다 안에서 사용하기
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach { // 각 원소에 대해 수행할 작업을 람다로 받는다.
println("$prefix $it") // 람다 안에서 함수의 "prefix" 파라미터를 사용한다.
}
}
val errors = listOf("403 Forbidden", "404 Not Found")
printMessagesWithPrefix(errors, "Error:")
// Error: 403 Forbidden
// Error: 404 Not Found
fun printProblemCount(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 response = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error")
printProblemCount(responses) // 1 client errors, 1 server errors
람다가 포획한 변수
라고 부른다.fun createClosure(): () -> Unit {
var counter = 0
val closure: () -> Unit = {
println("Counter inside lambda: $counter")
counter++
}
return closure
}
fun main() {
val closureInstance = createClosure()
closureInstance() // Counter inside lambda: 0
closureInstance() // Counter inside lambda: 1
// 외부 변수 counter의 생명주기가 끝났지만, 여전히 람다가 참조하고 있음
closureInstance() // Counter inside lambda: 2
}
fun main() = runBlocking {
var counter = 0
// 비동기적으로 값을 변경하는 코루틴 런치
val job = launch {
delay(1000) // 시간이 오래 걸리는 작업 시뮬레이션
counter = 42
}
// 함수 호출이 끝난 후에도 코루틴이 실행되고 로컬 변수가 변경될 수 있음
println("Counter before job join: $counter")
job.join() // 코루틴이 끝날 때까지 대기
println("Counter after job join: $counter")
}
// Counter before job join: 0
// Counter after job join: 42
fun tryToCountButtonClicks(button: Button): Int {
var clicks = 0
button.onClick { clicks++ }
return clicks
}
💡 특정 객체의 메소드나 프로퍼티를 참조하여 호출할 수 있는 함수를 생성하는 기능이다. 이를 통해 해당 멤버에 대한 참조를 함수처럼 전달하거나 저장할 수 있다.
이중 콜론(::)
을 사용한다.val getAge = { person: Person -> person.age }
val getAge = Person::age
::
를 사용하는 식을 멤버 참조
라고 부른다. 멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다.people.maxBy(Person::age)
people.maxBy { p -> p.age }
people.maxBy { it.age }
val salute = "Salute!"
fun greet(name: String): String {
return "Hello, $name!"
}
fun main() {
val propertyReference: () -> String = ::salute
val result1 = propertyReference()
println(result1) // Salute!
val greetFunction: (String) -> String = ::greet
val result2 = greetFunction("Alice")
println(result2) // Hello, Alice!
}
fun salute() = println("Salute!")
fun main() {
run(::salute) // Salute!
}
data class Person(val name: String, val email: String)
fun sendEmail(person: Person, message: String) {
println("Sending email to ${person.name} at ${person.email}: $message")
}
fun delegateOperation(person: Person, message: String, operation: (Person, String) -> Unit) {
operation(person, message)
}
fun main() {
// 람다를 사용하여 sendEmail 함수에 작업을 위임
val action: (Person, String) -> Unit = { person, message ->
sendEmail(person, message)
}
// 멤버 참조를 사용하여 sendEmail 함수에 작업을 위임
val nextAction: (Person, String) -> Unit = ::sendEmail
// Person 객체 생성
val alice = Person("Alice", "alice@example.com")
// 작업을 위임한 함수 호출
delegateOperation(alice, "Hello from lambda", action)
delegateOperation(alice, "Hello from member reference", nextAction)
}
data class Person(val name: String, val email: String)
fun sendEmail(person: Person, message: String) {
println("Sending email to ${person.name} at ${person.email}: $message")
}
fun main() {
// 람다를 사용하여 sendEmail 함수에 작업을 위임
val action = { person: Person, message: String ->
sendEmail(person, message)
}
// 멤버 참조를 사용하여 sendEmail 함수에 작업을 위임
val nextAction = ::sendEmail
// Person 객체 생성
val alice = Person("Alice", "alice@example.com")
// 작업을 위임한 함수 호출
action(alice, "Hello from lambda")
nextAction(alice, "Hello from member reference")
}
// Sending email to Alice at alice@example.com: Hello from lambda
// Sending email to Alice at alice@example.com: Hello from member reference
val createPerson = ::Person // "Person"의 인스턴스를 만드는 동작을 값으로 저장한다.
val p = createPerson("Alice", 29)
println(p) // Person(name=Alice, age=29)
fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
val p = Person("Dmitry", 34)
val personsAgeFunction = Person::age
println(personsAgeFunction(p))
val dmitrysAgeFunction = p::age
println(dmitrysAgeFunction())
personsAgeFunction은 인자가 하나(인자로 받은 사람의 나이를 반환)이지만, dmitrysAgeFunction은 인자가 없는(참조를 만들 때 p가 가리키던 사람의 나이를 반환) 함수라는 점에 유의하라.
함수형 프로그래밍에는 람다나 다른 함수를 인자로 받거나 함수를 반환하는 함수를
고차 함수(HOF, Higt Order Function)
라고 부른다. 고차함수는 기본 함수를 조합해서 새로운 연산을 정의하거나, 다른 고차 함수를 통해 조합된 함수를 또 조합해서 더 복잡한 연산을 쉽게 정의할 수 있다는 장점이 있다. 이런 식으로 고차 함수와 단순한 함수를 이리저리 조합해서 코드를 작성하는 기법을컴비네이터 패턴(combinator pattern)
이라 부르고, 컴비네이터 패턴에서 복잡한 연산을 만들기 위해 값이나 함수를 조합할 때 사용하는 고차 함수를컴비네이터
라고 부른다.
data class Person(val name: String, val age: Int)
val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 }) // [2, 4]
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.filter { it.age > 30 }) // [Person(name=Bob, age=31)]
✅ 술어(predicate) : 참/거짓을 반환하는 함수
val list = listOf(1, 2, 3, 4)
println(list.map { it * it }) // [1, 4, 9, 16 ]
// 사람 이름의 리스트 출력
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.map { it.name }) // [Alice, Bob]
// 30살 이상인 사람의 이름 출력 // 멤버 참조 사용
// people.filter({it.age > 30}).map(Person::name)
people.filter { it.age > 30 }.map(Person::name)
// 가장 나이 많은 사람의 이름(들)
people.filter { it.age == people.maxBy(Person::age)!!.age }
val maxAge = people.maxBy(Person::age)!!.age
people.filter { it.age == maxAge }
꼭 필요하지 않은 경우 굳이 계산을 반복하지 말라! 람다를 인자로 받는 함수에 람다를 넘기면 겉으로 볼 때는 단순해 보이는 식이 내부 로직의 복잡도로 인해 실제로는 엄청나게 불합리한 계산식이 될 수 때가 있다.
val numbers = mapOf(0 to "zero", 1 to "one")
println(numbers.mapValues { it.value.toUpperCase() }) {0=ZERO, 1=ONE}
// 모든 원소가 이 술어를 만족하는 지 궁금하다면 all 함수를 쓴다.
val canBeInClub27 = { p: Person -> p.age <= 27 }
val people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.all(canBeInClub27)) // false
// 술어를 만족하는 원소가 하나라도 있는지 궁금하다면 any를 쓴다.
println(people.any(canBeInClub27)) // true
val list = listOf(1, 2, 3)
println(!list.all { it == 3 }) // true
// 가독성을 높이려면 any와 all 앞에 !를 붙이지 않는 편이 낫다.
println(list.any { it != 3 }) // true
// 술어를 만족하는 원소의 개수를 구하려면 count를 사용한다.
val people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.count(canBeInClub27)) // 1
함수를 적재적소에 사용하라: count와 size
count가 있다는 사실을 잊어버리고, 컬렉션을 필터링한 결과의 크기를 가져오는 경우가 있다.
println(people.filter(canBeInClub27).size) // 1
하지만 이렇게 처리하면 조건을 만족하는 모든 원소가 들어가는
중간 컬렉션
이 생긴다. 반면 count는 조건을 만족하는 원소의 개수만을 추적하지 조건을 만족하는 원소를 따로 저장하지 않는다. 따라서 count가 훨씬 더 효율적이다.
// 술어를 만족하는 원소를 하나 찾고 싶으면 find 함수를 사용한다.
val people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.find(canBeInClub27)) // Person(name=Alice, age=27)
firstOrNull
을 쓸 수 있다.val people = listOf(Person("Alice", 27), Person("Bob", 31), Person("Carol", 31))
println(people.groupBy { it.age })
// {29=[Person(name=Bob, age=29)],
// 31=[Person(name=Alice, age=31), Person(name=Carol, age=31)]}
Map<Int, List<Person>>
이다. 필요하면 이 맵을 mapKeys나 mapValues 등을 사용해 변경할 수 있다.val list = listOf("a", "ab", "b")
println(list.groupBy(String::first)) // {a=[a, ab], b=[b]}
class Book(val title: String, val authors: List<String>)
books.flatMap { it.authors }.toSet() // books 컬렉션에 있는 책을 쓴 모든 저자의 집합
val strings = listOf("abc", "def")
println(strings.flatMap { it.toList() }) // [a, b, c, d, e, f]
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]
people.map(Person::name).filter { it.startsWith("A") }
people.asSequence() // 원본 컬렉션을 시퀀스로 변환한다.
.map(Person::name)
.filter { it.startsWith("A") }
.toList() // 결과 시퀀스를 다시 리스트로 변환한다.
왜 시퀀스를 다시 컬렉션으로 되돌려야 할까? 컬렉션보다 시퀀스가 훨씬 더 낫다면 그냥 시퀀스를 쓰는 편이 낫지 않을까?
- 항상 그렇지는 않다. 시퀀스의 원소를 차례로 이터레이션해야 한다면 시퀀스를 직접 써도 된다. 하지만 시퀀스 원소를 인덱스를 사용해 접근하는 등의 다른 API 메소드가 필요하다면 시퀀스를 리스트로 변환해야 한다.
큰 컬렉션에 대해서 연산을 연쇄시킬 때는 시퀀스를 사용하는 것을 규칙으로 삼아라.
8장에서는 중간 컬렉션을 생성함에도 불구하고 코틀린에서 즉시 계산 컬렉션에 대한 연산이 더 효율적인 이유를 설명한다. 하지만 컬렉션에 들어있는 원소가 많으면 중간 원소를 재배열하는 비용이 커지기 때문에 지연 계산이 더 낫다.
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)
println(listOf(1, 2, 3, 4).asSequence()
.map { it * it }.find { it > 3 }) // 4
val people = listOf(Person("Alice", 29), Person("Bob", 31),
Person("Charles", 31), Person("Dan", 21))
println(people.asSequence().map(Person::name)
.filter { it.length < 4 }.toList() ) // [Bob, Dan]
println(people.asSequence().filter { it.lenth < 4 }
.map(Person::name).toList()) // [Bob, Dan]
// 자연수의 시퀀스를 생성하고 사용하기
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
println(numbersTo100.sum()) // 5050
fun File.isInsideHiddenDirectory() =
generateSequence(this) { it.parentFile }.any { it.isHidden }
val file = File("/Users/svtk/.HiddenDir/a.txt")
println(file.isInsideHiddenDirectory()) // true
button.setOnClickListener { /* 클릭 시 수행할 동작 */ } // 람다를 인자로 넘김
public class Button {
public void setOnClickListener(OnClickListener l) { ... }
}
public interface OnClickListener {
void onClick(View v);
}
button.setOnClickListener(new OnclickListener() {
@Override
public void onClick(View v) {
...
}
})
button.setOnClickListener { view -> ... }
함수형 인터페이스
또는 SAM(Single Abstract Method) 인터페이스
라고 한다.void postponeComputation(int delay, Runnable computation);
postponeComputation(1000) { println(42) }
// 객체 식을 함수형 인터페이스 구현으로 넘긴다.
postponeComputation(1000, object: Runnable {
override fun run() {
println(42)
}
})
✅ 함수의 변수에 접근하지 않는 람다? : 클로저를 형성하지 않은 람다
// 프로그램 전체에서 Runnable의 인스턴스는 단 하나만 만들어진다.
postponeComputation(1000) { println(42) }
// 전역 변수로 컴파일되므로 프로그램 안에 단 하나의 인스턴스만 존재한다.
val runnable = Runnable { println(42) }
fun handleComputation() {
// 모든 handleComputation 호출에 같은 객체를 사용한다.
postponeComputation(1000, runnable)
}
fun handleComputation(id: String) {
// handleComputation을 호출할 때마다 새로 Runnable 인스턴스를 만든다.
postponeComputation(1000, runnable)
}
💡 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수(람다 표현식)다.
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
createAllDoneRunnable().run() // All done!
// SAM 생성자를 사용해 listener 인스턴스 재사용하기
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)
val listener = object : View.OnClickListener {
override fun onClick(view: View) {
val text = when (view.id) {
R.id.button1 -> "First button"
R.id.button2 -> "Second button"
else -> "Unknown button"
}
toast(text)
}
}
람다와 리스너 등록/해제하기
람다에는 무명 객체와 달리 인스턴스 자신을 가리키는 this가 없다는 사실에 유의하라. 따라서 람다를 변환한 무명 클래스의 인스턴스를 참조할 방법이 없다. 컴파일러 입장에서 보면 람다는 코드 블록일 뿐이고, 객체가 아니므로 객체처럼 람다를 참조할 수는 없다. 람다 안에서 this는 그 람다를 둘러싼 클래스의 인스턴스를 가리킨다.
class MyClass { private val myProperty: Int = 42 fun doSomething() { // 람다 표현식 내부에서 this는 MyClass의 인스턴스를 가리킴 val myLambda: () -> Unit = { println("Inside lambda: $this.myProperty") } myLambda() println("Outside lambda: $this.myProperty") } } fun main() { val myInstance = MyClass() myInstance.doSomething() } // Inside lambda: MyClass@506e1b77.myProperty // Outside lambda: MyClass@506e1b77.myProperty
import android.view.View
import android.widget.Button
class MyActivity {
private var button: Button? = null
private var myListener: View.OnClickListener? = null
init {
// 무명 객체를 사용하여 OnClickListener 구현
myListener = object : View.OnClickListener {
override fun onClick(v: View?) {
// 이벤트 처리 코드
// 자기 자신의 리스너 등록 해제
button?.setOnClickListener(null)
}
}
// 버튼에 리스너 등록
button?.setOnClickListener(myListener)
}
}
💡 수신 객체를 명시하지 않고, 람다의 본문 안에서 해당 객체의 메서드를 호출할 수 있게 하는 람다
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(stringBuilder) { // 메소드를 호출하려는 수신 객체를 지정한다.
for (letter in 'A'..'Z') {
this.append(letter) // "this"를 명시해서 앞에서 지정한 수신 객체의 메소드를 호출한다.
}
append("\nNow I know the alphabet!") // "this"를 생략하고 메소드를 호출한다.
this.toString() // 람다에서 값을 반환한다.
}
}
println(alphabet())
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Now I know the alphabet!
with(stringBuilder, { ... })
라고 쓸 수 있지만 더 읽기 나빠진다.수신 객체 지정 람다와 확장 함수 비교
확장 함수 안에서 this는 그 함수가 확장하는 타입의 인스턴스를 가리킨다. 그리고 그 수신 객체 this의 멤버를 호출할 때는 this.를 생략할 수 있다. 어떤 의미에서는 확장 함수를 수신 객체 지정 함수라 할 수도 있다. 람다는 일반 함수와 비슷한 동작을 정의하는 한 방법이다. 수신 객체 지정 람다는 확장 함수와 비슷한 동작을 정의하는 한 방법이다.
fun alphabet() = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
메소드 이름 충돌
with에게 인자로 넘긴 객체의 클래스와 with를 사용하는 코드가 들어있는 클래스 안에 이름이 같은 메소드가 있으면 무슨 일이 생길까? 그런 경우 this 참조 앞에 레이블을 붙이면 호출하고 싶은 메소드를 명확하게 정할 수 있다.
alphabet 함수가 OuterClass의 메소드라고 하자. StringBuilder가 아닌 바깥쪽 클래스(OuterClass)에 정의된 toString을 호출하고 싶다면
this@OuterClass.toString()
구문을 사용해야 한다.
fun alphabet() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet")
}.toString()
// apply를 TextView 초기화에 사용하기
fun createViewWithCustomAttributes(context: Context) =
TextView(context).apply {
text = "Sample Text"
textSize = 20.0
setPadding(10, 0, 0, 0)
}
fun alphabet() = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}