[Kotlin Tutorial] 람다로 프로그래밍 하기 - Chap5. Programming with Lambdas |
참조 : Kotlin in Action
5.1. Lambda expressions and member references
5.1.1. Introduction to lambdas : blocks of code as function parameters
5.1.2. Lambdas and collections
-
val people = listOf(Person(“Alice”, 29), Person(“Bob”, 31))
println( people.maxBy{ it.age } ) // function 을 argument 로 받는다. { } 는 lambda syntax
lambda 가 단순 function 이나 property 로 delegate 한다면 people.maxBy( Person::age )) 로 대체 가능. ( member reference )
원래 full code 는 people.maxBy( { p: Person -> p.age } ) 인데 type inference 등으로 생략가능하다
5.1.3. Syntax for lambda expressions
-
lambda 는 anonymous class 처럼 independent 하게 쓰일 수도 있고, variable 에 저장될 수도 있다.
보통은 anonymous 형태로 쓰인다.
{ x : Int, y: Int -> x + y } // parameters -> body 의 형태, parameter 쪽에 괄호는 없다
-
lambda 는 직접 호출할 수도 있지만, 보통은 run 에 넘기는 것이 가독성이 더 좋다.
아래 두 코드는 동일하다
{ println(42) }()
run { println(42) }
-
argument 가 lambda 만 받을 경우에는 함수 call 에서 괄호를 생략할 수 있다.
또한 lambda 가 마지막 argument 일 경우에는 가독성을 고려해 () 바깥에 쓸 수 있다.
people.maxBy( { it.age } )
people.maxBy() { it. age }
peopl.maxBy{ it.age }
-
IDE 툴에서 “Move lambda expression out of parentheses”, “Move expression into parentheses” 기능을 쓸 수 있다.
-
lambda 가 1개의 parameter 만 갖는다면 it 로 받을 수 있다.
코드를 간결히 하지만 nested lambda 같은 경우에는 가독성을 해칠 수 있기 때문에 it 을 안 쓰는 것이 좋다.
-
lambda 를 variable 에 assign 할 때는 type inference 가 어렵다.
그래서 이 때는 다 써줘야 한다.
val getAge = { p: Person -> p.age }
5.1.4. Accessing variables in scope
-
anonymous inner class 정의하는 것과 마찬가지로 lambda 에서도 outer scope 의 변수를 접근할 수 있다.
그리고 inner class 와 마찬가지로 Kotlin 에서는 final 이 아닌 녀석도 접근할 수 있다. (captured 라고 함)
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(“$clientErros client errors, $serverErrors server errors”)
}
-
mutable variable 을 capture 하는 것이 주의사항을 만들기도 하는데..
lambda 코드가 event handler 처럼 등록되서 쓰이거나, async 로 작동한다거나 한다면
local variable 수정이 lambda 가 수행될 때만 유효하다는 것을 항상 기억해야 한다.
fun tryToCountButtonClicks(button: Button): Int{
var clicks = 0
button.onClick { clicks++ }
return clicks
}
// 이 함수 호출하면 return 값은 항상 0 이다.
5.1.5. Member references
-
만약에 lambda expression 에 들어가는 코드 내용이 이미 함수로 정의되어 있다면, 함수를 바로 넘길 수 있다.
이것을 member reference 라고 한다
val getAge = Person::age // 괄호 없음
-
top-level 에 정의된 함수의 경우 다음과 같이 접근할 수 있다.
fun salute() = println(“Salute!”)
run(::salute)
-
constructor reference 를 통해 class 생성을 변수에 저장할 수도 있다.
val createPerson = ::Person
val p = createPerson(“Alice”, 29)
그리고 extension function 도 member reference 로 쓸 수 있다.
fun Person.isAdult() = age >= 21
val predicate = Person::isAdult
-
bound reference 라는 것이 있다. Kotlin 1.1 부터 사용가능하다. ( 2017. 07. 23. 현재 1.13 )
val p = Person(“Dmintry”, 34)
val personsAgeFunction = Person::age // Person 을 arg 로 받는 one-argument function
println( personsAgeFunction(p) )
// from Kotlin 1.1
val dmitrysAgeFunction = p::age // p 로 접근한다, bound member reference, zero argument
println(dmitrysAgeFunction())
5.2. Functional Apis for collections
5.2.1. Essentials: filter and map
-
filter 와 map 은 collection 을 lambda 로 조작하는데 필수적인 녀석들이다.
-
val list = listOf(1,2,3,4)
println( list.filter{ it % 2 == 0 } ) // return 결과는 predicate 를 충족시키는 new collection
filter 는 원하는 녀석만 추리는 기능을 한다.
-
val list = listOf(1,2,3,4)
println( list.map { it * it } ) // return 결과는 new collection
map 은 transform 을 담당한다고 보면 된다.
val people = listOf( Person(“Alice”, 29), Person(“Bob”, 31) )
people.map( Person::name )
-
collection lambda 의 장점은 chain 이 될 수 있다는 것!!!
people.filter{ it.age > 30 }.map( Person::name ) // 30살 이상의 이름이 쫙 나온다
-
코드의 동작을 잘 알아야 한다.
가장 나이 많은 사람들을 추려내고 싶을 때 둘의 차이를 잘 알아야 한다.
people.filter{ it.age == people.maxBy(Person::age).age } // 매 person 마다 maxBy 를 타게 된다.
val maxAge = people.maxBy(Person::age).age // maxBy 는 1번만 탄다
people.filter{ it.age == maxAge }
-
Map 에도 물론 적용할 수 있다.
val numbers = mapOf(0 to “Zero”, 1 to “one”)
numbers.mapValues{ it.value.toUpperCase() } // <key, transform> 으로 구성된 map 이 나온다
5.2.2. “all”, “any”, “count” and “find” : applying a predicate to a collection
-
val canBeInClub27 = { p: Person -> p.age <= 27 } // predicate
val people = listOf(Person(“Alice”, 27), Person(“Bob”, 31))
people.all(canBeInClub27) // true, false (boolean) return. 모두가 그 조건을 만족하는가를 물어보는 것이다. 결과는 false
people.any(canBeInClub27) // 마찬가지로 true, false (boolean) return. 결과는 true
-
negate 조건이 있을 때 all 과 any 는 서로 교체 가능하다.
val list = listOf(1,2,3)
! list.all { it == 3 }
list.any{ it != 3 } // 가독성과 성능 모두가 좋다.
-
몇 개가 조건을 충족하는지 보고 싶으면 count 를 쓰면 된다.
people.count(canBeInClub27)
물론 people.filter(canBeInClub27).size 를 쓸 수도 있다. 그러나 이 경우는 별 필요없는 중간과정의 list 를 생산한다
-
find 는 first matching element 를 return 한다.
없는 경우 null 이 나온다.
find 는 firstOrNull 과 같다.
( 참고로 filter 는 모두 걸러내는 것이고 find 는 first match )
5.2.3. groupBy: converting a list to a map of groups
-
val people = listOf( Person(“Alice”, 31), Person(“Bob”, 29), Person(“Carol”, 31))
people.groupBy{ it.age } ) // key 가 age, value 가 group 될 collection 의 형태가 된다. Map<Int, List<Person>>
5.2.4. flatMap and flatten: processing elements in nested collections
-
flatMap 은 2가지 일을 한다.
첫번째는 each element 를 주어진 조건으로 collection 으로 transform(map) 시킨다.
두번째는 여러개의 list 를 하나로 combine ( flatten ) 시킨다.
val strings = listOf(“abc”, “def”)
strings.flatMap{ it.toList() } // “abc”, “def” 가 각각 “a”, “b”, “c” 그리고 “d”, “e”, “f” 를 element 로 갖는 list 가 되고, 그 다음 1개의 list 로 통합된다.
-
val books = listOf(
Book("Thursday Next", listOf("Jasper Fforde")),
Book("Mort", listOf("Terry Pratchett")),
Book("Good Omens", listOf("Terry Pratchett”, "Neil Gaiman")))
books.flatMap { it.authors }.toSet()
-
flatMap 을, collection 의 element 로 collection 을 가진 녀석들을 a(하나의) collection 으로 만들 때 쓴다고 생각하면 쉽다. (??)
mapping 없이 단순히 flatten 시키고 싶다면 flatten 을 호출하면 된다.
listOfLists.flatten()
-
이 외에도 정말 많은 collection 관련 operation function 들이 있다.
그러나 여기에서는 다 다루지 않는다.
중요한 것은 어떻게 transform 을 할 것이고 library 를 활용하여 어떻게 간단히 잘 쓸것인가를 고민하는 것이다.
5.3. Lazy collection operations: Sequences
-
기본적으로 map, filter 등을 사용하게 되면 중간 과정에 collection 이 계속 생겨난다.
Sequence 라는 것을 쓰면 중간과정의 collection 생성은 줄어든다.
people.map(Person::name)
.filter{ it.startsWith(“A”) }
people.asSequence()
.map(Person::name)
.filter{ it.startsWith(“A”) }
.toList()
collection 의 갯수가 많을 때는 performance 차이가 엄청나다고 한다.
-
Sequence 는 Kotlin 의 interface 인데, iterator 라는 한개의 method 만 지원한다.
Sequence interface 의 강점은, element 들이 lazily 하게 평가된다는 것이다.
그래서 중간 결과값을 가지는 collection 을 생성할 필요가 없다.
-
어떤 Collection 이던 asSequence extension function 을 통해 sequence 를 만들 수 있다.
그리고 이 녀석을 다시 collection 으로 만드려면 toList 와 같은 녀석을 쓰면 된다.
( 꼭 toList 로 전환할 필요는 없다. Sequence 자체로 계속 사용해도 되는 케이스에는 그렇게 쓰면 된다 )
( 갯수가 많지 않으면 그냥 중간 collection 을 만드는게 더 효율적이다. 이에 대해서는 나중에 다룬다. )
5.3.1. Executing sequence operations: intermediate and terminal operations
-
sequence 의 operation 은 intermediate 와 terminal 두개로 나뉘어진다.
intermediate operation 은 다른 sequence 를 return 하고, terminal operation 은 collection, element, number 또는 sequence transformation 에 의한 다른 값이 나올 수 있다.
intermediate 는 항상 lazy 로 동작한다. ( lazy 의 반대는 eager 라고 표현.. )
위에 있던 예제에서는 toList 가 terminal operation 이고, 중간과정은 모두 intermediate 이다.
-
listOf(1,2,3,4).asSequence()
.map{ print(“map($it) “); it * it }
.filter{ print(“filter($it) “); it % 2 == 0 }
.toList()
toList 호출 전까지는 아무 print 도 하지 않는다. 즉 terminal operation 이 있어야 실제 로직이 돈다.
결과는…
map(1) filter(1) map(2) filter(4) … map(4) filter(16)
순서도 주목할만하다.
sequence 가 아니면 1, 2, 3, 4 를 print 하며 1, 4, 9, 16 을 가진 list 를 만들고 그 다음 1, 4, 9, 16 을 찍으면서 4, 16 을 가진 list 를 만들 것이다.
-
Kotlin 의 sequence 는 Java8 의 stream 과 동일하다.
한가지 차이가 있다면 현재 Kotlin 은 parallel stream 을 제공하지 않는다는 것.
그러나 Kotlin 은 Java6 까지 cover 한다는 것 (장점이자 단점)
5.3.2. Creating sequences
-
asSequence 대신 generateSequence 를 써서 sequence 를 만들 수도 있다.
val naturalNumbers = generateSeuqnece(0){ it + 1 } // 0 은 초기값
val numbersTo100 = naturalNumbers.takeWhile{ it <= 100 }
numbersTo100.sum()
5.4. Using Java functional interfaces
-
Kotlin 의 lambda 는 Java API 들과 완벽하게 호환된다.
button.setOnClickListener{ println(“gamza”) }
이것이 가능한 이유는 OnClickListener 가 functional interface 또는 SAM interface 이기 때문이다.
(SAM : single abstract method)
-
Java 와 달리 Kotlin 은 function type 을 지원한다.
그래서 Kotlin function 은 lambda 를 param 으로 받을 때 function type 을 써야 좋다. (functional interface type 대신)
이에 대해서는 나중에 공부할께요!
5.4.1. Passing a lambda as a parameter to a Java method
-
functional interface 를 param 으로 받는 Java 함수에 lambda 를 전달할 수 있다.
/* Java */
void postponeComputation(int delay, Runnable computation);
/* Kotlin */
postponeComputation(1000) { println( 42 ) }
compiler 가 똑똑해서 알아서 해준다.
물론 아래와 같이 써도 된다. 그러나 그럴 이유가 전혀 없당..
게다가 아래 코드는 매번 호출할 때마다 object 를 생성하지만, 위의 lambda 에서는 (variable capture 를 하지 않으면) single instance 로 재활용된다.
ponstponeComputation(1000), object : Runnable{
override fun run(){
println( 42 )
}
})
-
Kotlin 1.0 에서 lambda 식은 inline lambda 가 아니라면, 모두 anonymous class 로 compile 된다.
Java8 supporting Kotlin 은 계획중에 이 버전에서는 그럴 필요는 없을 것이다.
( 물론 variable capture 를 하는 경우에는 Java8 일때도 어쩔 수 없겠징.. )
inline 으로 전달되는 lambda 의 경우는 anonymous class 는 만들어지지 않는다.
inline 에 대해서는 나중에 배운다.
5.4.2. SAM constructors: explicit conversion of lambdas to functional interfaces
-
SAM constructor 는 lambda 를 functional interface 로 명시적으로 바꾸는 compiler 가 만든 녀석이다.
compiler 가 자동으로 conversion 하지 않는 케이스에 쓸 수 있다.
예를 들어 functional interface 를 return 하는 함수가 있다면 lambda 를 바로 return 할 수 있는 것이다.
SAM constructor 로 감싸기만 하면 된다.
fun createAllDoneRunnable(): Runnable{
return Runnable{ println(“All done!”) }
}
SAM constructor 이름은 기저가 되는 functional interface 이름과 동일하다.
SAM constructor 는 lambda 를 single argument 로 받는다.
-
SAM constructor 는 변수에 SAM 을 저장할 때도 쓰인다.
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)
object keyword 를 써서 정의할 수 있지만, SAM constructor 가 더 간단하고 좋다.
-
Lambda 안에서는 그 자신을 가리키는 this 라는 것이 없다. 실제 lambda 는 object 가 아니라 compiler 는 code block 으로 보기 때문이다.
Lambda 안에서의 this 는 surrounding class 를 가리킨다.
그래서 예를 들어 button 에 listener 를 등록하고, listener 안의 로직에서 자신을 해제하려고 하면 lambda 를 사용할 수 없다… ( 헐 )
이 때는 lambda 가 아닌 anonymous object 로 만들어야 한다.
-
method call 에서의 SAM conversion 이 보통 자동으로 이루어지지만, compiler 가 SAM conversion 을 잘 하지 못하는 경우가 있다.
overload 가 되어 있는 케이스가 그렇다.
그럴 경우에는 explicit 로 SAM constructor 를 호출해주어야 한다.
5.5. Lambda with receivers: "With" and "Apply"
-
with 와 apply 는 Java 에서는 할 수 없는 Kotlin 만의 기능이다.
나중에는 너만의 with 와 apply 를 구현하는 법도 배울 것이다.
5.5.1. The “with” function
-
여러 language 에 한 object 에 대해 object 이름을 반복하지 않고 여러 operation 을 호출하는 기능들이 있다.
Kotlin 에서는 with 로 이를 제공한다.
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()
return with(stringBuilder){
for (letter in ‘A’..’Z’){
this.append(letter)
}
append(“\nNow I know the alphabet!”)
this.toString()
}
}
-
with 는 special constructor 처럼 생겼지만, 사실은 object 와 lambda 2개의 argument 를 받는 method 이다.
첫번째 param 은 2번째 lambda 의 receiver 라 부른다.
lambda 에서 이 receiver 를 this 를 호출해서 접근할 수도 있고, 생략할 수도 있다.
lambda with receiver 는 extension function 과 비슷하게 생각할 수 있다.
-
fun alphabet() = with(StringBuilder()){
for (letter in ‘A’..’Z’){
append(letter)
}
append(“\nNow I know the alphabet!”)
toString() // with 의 마지막 expression 을 return 으로 전달
}
-
with 에 전달된 객체와 outer class 의 method name conflict 나면 어쩌냐고 묻고싶었지?
기본적으로 with 에 전달된 receiver 의 녀석을 호출하고 outer 의 녀석을 호출하고 싶다면..
아래와 같이 호출해야 한다.
this@OuterClass.toString()
-
with 의 경우 lambda 의 실행결과 ( 마지막 expression ) 을 return 으로 하는데, receiver object 를 전달하고 싶을 수 있다.
이 때 apply 를 사용하면 된다.
5.5.2. The “apply” function
-
apply 는 with 와 완전 동일하다. 차이는 receiver 를 return 한다는 것.
fun alphabet() = StringBuilder().apply{
for(letter in ‘A’..’Z’){
append(letter)
}
append(“\n Now I know the alphabet!”)
}.toString()
-
apply 는 extension function 으로 정의되어 있다.
-
apply 가 잘 쓰여지는 곳은 Java 의 builder pattern 대체일 것이다.
Builder 를 따로 만들 필요가 없다. ( 호! 짱짱! )
fun createViewWithCustomAttributes(context: Context) = TextView(context).apply{
text = “Simple Text”
textSize = 20.0
setPadding(10, 0, 0, 0)
}
-
with 와 apply function 은 receiver 와 lambda 를 사용하는 기본 generic 의 예이다.
같은 pattern 을 사용하는 여러 함수들이 있다.
예를 들면 buildString 이라는 standard lib 을 사용해서 더 쉽게 코딩할 수 있다.
fun alphabet() = buildString{
for( letter in ‘A’..’Z’ ){
append(letter)
}
append(“\nNow I know the alphabet!”)
}
buildString 의 argument 는 lambda 이고 이 lambda 에 receiver 로 항상 StringBuilder 를 넘겨준다.
그리고 마지막에 toString 을 자동으로 붙여준다.
나중에 더 흥미로운 예제들이 기다리고 있다.
특히 Lambda with receiver 는 DSL 에 엄청 좋은 tool 이다.
5.6. Summary
-
Lambda 는 function 에 code block 을 argument 로 던질 수 있게 해준다.
-
Kotlin 은 lambda 를 괄호 바깥쪽에 정의해서 던질 수 있게 해준다.
그리고 single lambda param 의 경우 it 으로 참조할 수 있다.
-
Lambda 안의 code 는 lambda 를 포함하는 outer scope 의 변수를 참조할 수도 변경할 수도 있다.
-
method, constructor, property 들을 :: 를 통해 reference 할 수 있다.
그리고 이 member reference 를 lambda 대신 전달할 수 있다.
-
대부분의 일반적인 operation 은 직접 iteration 돌지 않고, filter, map, all, any 등을 통해 처리할 수 있다.
-
Sequence 는 collection 에 대한 여러 operation 을 종합해서 lazy 하게 수행하며, 중간단계의 결과를 들고 있는 collection 을 만들지 않는다.
-
Java function interface 을 param 으로 받는 method 들에 lambda 를 넘길 수 있다.
Java functional interface 는 single abstract method 를 가진 녀석을 말하며 SAM 이라고 부른다.
-
Lambda 와 receiver 를 함께 쓰는 경우 receiver 에 대한 참조 없이 그냥 바로 method call 을 할 수 있다.
-
with 는 한 object 에 대해 여러개의 method 를 object referencing 없이 호출 할 수 있다.
apply 는 builder-style 의 object construct & initialize 하는 데 좋다.
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
[Kotlin Tutorial] Kotlin 의 Type system #2 (0) | 2017.08.22 |
---|---|
[Kotlin Tutorial] Kotlin 의 Type system - Chap6. The Kotlin type system (0) | 2017.08.18 |
[Kotlin Tutorial] 클래스, objects, 그리고 인터페이스 #2 (0) | 2017.08.14 |
[Kotlin Tutorial] 클래스, objects, 그리고 인터페이스 #1 - Chap4. Classes, objects, and interfaces (0) | 2017.08.11 |
[Kotlin Tutorial] 함수 정의하고 호출하기 #2 (0) | 2017.08.03 |
댓글