본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[Kotlin Tutorial] 람다로 프로그래밍 하기 - Chap5. Programming with Lambdas

by 돼지왕 왕돼지 2017. 8. 16.
반응형

 [Kotlin Tutorial] 람다로 프로그래밍 하기 - Chap5. Programming with Lambdas


참조 : Kotlin in Action


::, accessing variables in scope, All, all any !, all any negate, Anonymous Class, any, apply, assequence, body, bound reference, builder pattern, builder-style, buildstring, Capture, Captured, captured variable, code block, collection, collection lambda, collection lambda chain, collection map, collection transform, Compiler, compiler generated, constructor, constructor member reference, constructor reference, Count, DSL, eager, etension function, explicit conversion of lambdas, extension function, extension function member reference, Filter, Final, Find, first mtching element, firstornull, flatmap, flatten, function type, functional apis for collections, functional interface, functional interface type, generatesequence, GroupBy, IDE, inline lambda, Interface, intermediate, intermediate operation, it, iterator, JAVA6, java8 stream, kotlin basics, kotlin tutorial, kotlin 강좌, kotlin 기초 강좌, LAMBDA, lambda programming, lambda receiver, lambda run, lambda single argument, lambda to functional interface, lambda type inference, lambda variable assign, lambda with receiver, lambdas to functional interfaces, Lazy, Lazy collection operations: Sequences, list flatten, list to map, map, mapvalues, maxby, member reference, member refernece, Method, method name conflict, method reference, Move expression into parentheses, Move lambda expression out of parentheses, nested lambda, Operation, outer class, outer scope, paralle stream, Param, parameter, passing a lambda as a parameter to a java method, Predicate, property, receiver object, Reference, Sam, sam constructor, sam interface, SEQUENCE, sequence operation, single abstract method, single lambda param, Stream, takewhile, terminal operation, this@OuterClass, tolist, top level, type inference, variable, With, zero argument, [Kotlin Tutorial] 람다로 프로그래밍 하기 - Chap5. Programming with Lambdas, 기저 functional interface, 람다, 람다 프로그래밍, 마지막 expression, 실행 결과, 초기값, 코틀린


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 하는 데 좋다.





반응형

댓글