-
[Kotlin In Action] Chapter 5 - Programming with lambdasDev/Kotlin 2021. 1. 16. 02:37
* 해당 포스트는 "Kotlin In Action" 책을 읽고 난 이후의 정리 내용입니다.
자세한 내용은 "Kotlin In Action" 책을 통해 확인해주세요.
람다 표현식
다른 함수들에 전달할 수 있는 작은 코드 덩어리
Lambda expressions and member references
1) Introduction to lambdas: blocks of code as function parameters
Java 8 이전(람다 표현식을 사용할 수 없는 버전)에는 익명 클래스를 사용해 구현했음
/* Java Anonymous inner class */ button.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { /* * 클릭 이벤트가 발생했을 때의 세부 행동 작성 */ } }); /* Java with Lambda */ button.setOnClickListener( (View v) -> { /* * 클릭 이벤트가 발생했을 때의 세부 행동 작성 */ });
OnClickListener의 Object를 setOnClickListener 메소드에서 바로 생성해서 사용
onClick을 구현해 저 메소드가 호출될 때 적용되도록 함
물론 Java 8 이상에서도 익명 클래스 대신 람다 표현식을 사용해서 행동을 나타낼 수 있음
Kotlin에서 람다 표현식을 사용해 같은 객체 호출
button.setOnClickListener { /* 세부 행동 */ }
Java와 달리 Kotlin의 람다 표현식은 항상 중괄호 안에서 행동을 나타냄
2) Lambdas and Collections
최댓값, 최솟값을 찾기 위해 function을 사용하는 방식과 람다 표현식을 사용하는 방식을 비교해보면 다음과 같음
// 사용될 데이터 클래스 data class Person(val name: String, val age: Int) // 가장 나이가 많은 사람을 계산하는 function 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) } // 위에서 정의된 function 사용 fun runWithFunction(){ val people = listOf(Person("Alice", 29), Person("Bob", 31)) findTheOldest(people) // Person(name=Bob, age=31) } // Lambda식 사용 fun runWithLambda(){ val people = listOf(Person("Alice", 29), Person("Bob", 31)) // Person 데이터 클래스의 age를 기준으로 최댓값을 가져옴 println(people.maxByOrNull { it.age }) // Person(name=Bob, age=31) // Member Reference를 사용해 검색 println(people.maxByOrNull(Person::age)) // Person(name=Bob, age=31) }
findTheOldest 함수는 Person의 List만을 처리하는 함수로 전체 로직을 구성해서 사용
maxByOrNull 함수는 어떤 Collection에서도 사용할 수 있는 함수
검색 영역은 멤버 참조로 바꿀 수 있음
// 내장된 maxByOrNull 함수 public inline fun <T, R : Comparable<R>> Iterable<T>.maxByOrNull(selector: (T) -> R): T? { val iterator = iterator() if (!iterator.hasNext()) return null var maxElem = iterator.next() if (!iterator.hasNext()) return maxElem var maxValue = selector(maxElem) do { val e = iterator.next() val v = selector(e) if (maxValue < v) { maxElem = e maxValue = v } } while (iterator.hasNext()) return maxElem }
maxByOrNull: 반복할 수 있는 Type을 기준으로 사용할 수 있음
Kotlin에는 이러한 내장 함수들이 다양한 형태로 존재함
3) Syntax for lambda expressions
람다 식: 독립적으로 선언 + 변수에 저장 가능, 항상 중괄호로 둘러쌓여 있음
// Kotlin에서의 람다 표현식 대입 val sum = { x: Int, y: Int -> x+y} println(sum(1, 2)) // 3
Kotlin에서는 이렇게 변수에 저장
// 비교: Scala에서의 Lambda 표현식 변수 대입(x*2) val ex = (x:Int) => x + x
Scala와는 달리 Kotlin은 인수 주위에 괄호가 없음
// 람다 표현식을 직접 호출할 수 있음 val test = { println(42) }() // run을 통해 전달된 람다 함수를 실행(라이브러리 함수) run{println(42)}
syntax shortcut을 사용하지 않고 수정해보자
// println(people.maxByOrNull { it.age }) // p:Person -> p.age은 maxByOrNull 함수에 argument로 전달함 println(people.maxByOrNull { p:Person -> p.age }) // syntactic convention을 사용 // 람다 표현식이 Argument의 마지막일 경우 괄호 밖으로 뺄 수 있음 println(people.maxByOrNull(){p:Person -> p.age}) // 람다 표현식이 유일한 Argument이므로 () 생략 가능 people.maxBy { p: Person -> p.age } // people의 Type이 이미 정해져 있기 때문에, 생략 가능 people.maxBy { p -> p.age } // 변수에 삽입할 때에는 p의 Type을 모르기 때문에 명시해야 한다. 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)) /* Computing the sum of 1 and 2... * 3 */
4) Accessing variables in scope
기본적으로 이렇게 사용됨
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) { messages.forEach { println("$prefix $it") } } fun main(){ val errors = listOf("403 Forbidden", "404 Not Found") printMessagesWithPrefix(errors, "Error:") // Error: 403 Forbidden // Error: 404 Not Found }
람다 표현식 외부의 변수에 접근, 수정이 가능하다.
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") } fun main(){ val responses = listOf("200 OK", "418 I'm a teapot","500 Internal Server Error") printProblemCounts(responses) // 1 client errors, 1 server errors }
5) Member references
/* Member Reference * Java 8과 동일한 방식 */ val getAge = Person::age val getAge = { person: Person -> person.age } // 요렇게도 가능 people.maxBy(Person::age) // salute는 최상위 함수이므로 다음과 같은 사용이 가능 fun salute() = println("Salute!") run(::salute) // Salute! // 두 가지 방법으로 sendEmail을 할당하는 방법 val action = { person: Person, message: String -> sendEmail(person, message) } val nextAction = ::sendEmail data class Person(val name: String, val age: Int) fun main(){ val createPerson = ::Person val p = createPerson("Alice", 29) println(p) } // Person(name=Alice, age=29) //reference extensions을 다음과 같이 사용할 수도 있음 fun Person.isAdult() = age >= 21 val predicate = Person::isAdult
Functional APIs for collections
Collection을 가공하는 다양한 API 존재
1) Essentials: filter and map & 2) “all”, “any”, “count”, and “find”: applying a predicate to a collection
list.filter { it % 2 == 0} // 조건을 충족하는 것만 걸러냄 list.map { it * it } // 값을 변환해서 새로운 콜렉션 생성 list.count // Int list.all { it > 3 } // boolean, 모든 원소가 만족하면 true list.any { it > 2 } // boolean, 하나라도 만족하면 true list.find { it > 3 } // 널가능 타입, 조건을 충족하는 첫 번째 원소 (firstOrNull과 동일) map.filter { entry ‐> entry.key % 2 == 0 } // entry: Map.Entry map.map { entry ‐> entry.key * 2 to entry.value } // 맵은 filterKeys, mapKeys, filterValues, mapValues 제공
3) groupBy: converting a list to a map of groups
Collection을 그룹화
val people = listOf( Person("Alice", 31), Person("Bob", 29), Person("Carol", 31) ) println(people.groupBy { it.age }) // {31=[Person(name=Alice, age=31), Person(name=Carol, age=31)], 29=[Person(name=Bob, age=29)]} // 사전 정렬처럼 이렇게 그룹화하는 것도 가능 val list = listOf("a", "ab", "b") println(list.groupBy(String::first)) // {a=[a, ab], b=[b]}
4) flatMap and flatten: processing elements in nested collections
class Book(val title: String, val authors: List<String>) fun main(){ // books.flatMap { it.authors }.toSet() 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()) // 이렇게도 표현 가능 println(books.flatMap(){ b->b.authors }.toSet()) // [Jasper Fforde, Terry Pratchett, Neil Gaiman] }
Lazy collection operations: sequences
1) Executing sequence operations: intermediate and terminal operations
Sequences를 사용해 지연 연산을 수행
더보기🛑 Scala, Spark에서의 지연 연산의 의미
지연 연산: 실제 Action이 수행될 때 앞의 Transform 연산을 수행하는 것
Action: 실제 출력, 저장, 기타 등의 행동이 일어나는 것
Transform: 변형이 일어나는 것
Kotlin에서는 Intermediate operations과 Terminal operation으로 나뉨
Java 8의 stream()과 동일
2) Creating sequences
fun File.isInsideHiddenDirectory() = generateSequence(this) { it.parentFile }.any { it.isHidden } fun main(){ val naturalNumbers = generateSequence(0) { it+1} val numbersTo100 = naturalNumbers.takeWhile { it <= 100 } println(numbersTo100.sum()) // 5050 val file = File("/Users/svtk/.HiddenDir/a.txt") println(file.isInsideHiddenDirectory()) // true }
Using Java functional interfaces
1) Passing a lambda as a parameter to a Java method
람다 표현식을 Java Interface argument로 넘길 수 있음
fun postponeComputation(delay: Int, computation: Runnable){ computation.run() } /* * In Java, * void postponeComputation(int delay, Runnable computation); */ fun main(){ postponeComputation(1000) { println(42) } postponeComputation(1000, object : Runnable { override fun run() { println(43) } }) }
2) SAM constructors: explicit conversion of lambdas to functional interfaces
변수로 선언해 넘길 수 있음
val runnableVal = Runnable{println(45)} postponeComputation(1000, runnableVal) 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)
Lambdas with receivers: “with” and “apply”
1) The “with” function
/* fun alphabet(): String { val result = StringBuilder() for (letter in 'A'..'Z') { result.append(letter) } result.append("\nNow I know the alphabet!") return result.toString() } */ fun alphabet(): String { val stringBuilder = StringBuilder() // with를 통해 stringBuilder를 내부에서 자기 자신인 것(this)처럼 사용 return with(stringBuilder) { for (letter in 'A'..'Z') { this.append(letter) } append("\nNow I know the alphabet!") this.toString() } } fun main(){ println(alphabet()) // ABCDEFGHIJKLMNOPQRSTUVWXYZ // Now I know the alphabet! }
2) The “apply” function
with와 거의 동일
차이점: apply는 항상 자신에게 전달된 객체를 반환함
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) } // StringBuilder를 사용하는 방식을 buildString으로 변경 fun alphabet() = buildString { for (letter in 'A'..'Z') { append(letter) } append("\nNow I know the alphabet!") }
'Dev > Kotlin' 카테고리의 다른 글
[Kotlin In Action] Chapter 3 - Defining and calling functions (0) 2021.01.09 [Kotlin In Action] Chapter 1 (0) 2020.12.30