ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin In Action] Chapter 5 - Programming with lambdas
    Dev/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은 인수 주위에 괄호가 없음

    람다 식 구문(Kotlin in Action)

    // 람다 표현식을 직접 호출할 수 있음
    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으로 나뉨

    Sequence에 각 명령들이 적용될 경우(Kotlin In Action)

    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

    댓글

Designed by Tistory.