코틀린 인 액션 8장

존스노우·2023년 4월 6일
0

코틀린

목록 보기
8/10

고차 함수: 파라미터와 반환값으로 람다 사용

  • 람다를 인자로 받거나 반환하는 고차 함수를 만드는 방법을 다룸

고차함수 정의

함수 타입

  • 람다를 인자로 받는 함수를 정의하려면? 타입을 어떻게 선언할지 알아 보자.

    // 컴파일러에서 두 개를 함수 타입으로 추론을 함.
    // 각각 Int unit 반환하는걸로 인식.
    val sum = (x: Int, y: Int) => x + y
    val action = { println(42) }
    
  • 함수타입을 정의하려면 -> 뒤에 반환 타입을 지정하면 됨.

  • unit 타입 경우 생략해도 되지만 함수 타입! 을 선언할땐 반환타입 반드시 명시해야됨! Unit빼먹지말기.

// 널이 될수 있는 함수타입 변수도 가능.
var canReturnNull: ((Int, Int) -> Int)? = { x, y -> null }

인자로 받는 함수 호출

  • 고차함수를 구현해보자

     // Declare a function type parameter for operation that takes two Int arguments and returns an Int
     fun twoAndThree(operation: (Int, Int) -> Int) {
         // Call the operation function with arguments 2 and 3 and store the result in the result variable
         val result = operation(2, 3)
         // Print the result string
         println("The result is $result")
     }
    
     // Call the twoAndThree function with a lambda expression that adds two numbers
     twoAndThree { a, b -> a + b } // The result is 5
    
     // Call the twoAndThree function with a lambda expression that multiplies two numbers
     twoAndThree { a, b -> a * b } // The result is 6
  • 단순한 문법소개 함수 이름 뒤에 괄호를 넣고 원하는 인자넣은뒤 콤마로 분리!

  • Filter함수를 구현해 보자.

 // filter 함수를단순하게만든버전구현하기
fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}
println("ablc".filter { it in 'a'..'z' }) // Output: "abc"

자바에서 코틀린 함수 타입 사용

  • 컴파일된 코드 안에선 함수타입은 일반 인터페이스로 바뀜
  • 즉 펑셔널 인터페이스 구현 객체로 저장을한다.
  • 함수타입을 사용하는 코틀린 함수를 자바는 쉽게 호출함 자바 8 람다를 넘기면 자동으로 함수타입값 반환

디폴트값을지정한함수타입파라미터나널이될수있는함수타입파라미터

  • 길다 제목 참..

  • 파라미터로 넘기는 함수를 디폴트값을 지정할 수 있다.

    fun <T> Collection<T>.joinToString(
       separator: String = ", ",
       prefix: String = "",
       postfix: String = ""
    ): String {
       val result = StringBuilder(prefix)
       for ((index, element) in this.withIndex()) {
           if (index > 0) result.append(separator)
           result.append(element)
       }
       result.append(postfix)
       return result.toString()
    }
    
  • 예시 코드 result.append(element) 는 암시적이게 대해 toString 메서드 호출

  • 허나 toString 메서드로만 고정되 문자열로 반환. 따라 서 다른 방법도필요

  • 허나 파라미터를인자로 넘겨주면 불편할 수도 있으니 고정값을 사용하는법을 하자

  fun <T> Collection<T>.joinToString(
      separator: String = ", ",
      prefix: String = "",
      postfix: String = "",
      transform: (T) -> String = { it.toString() }
  ): String {
      val result = StringBuilder(prefix)
      for ((index, element) in this.withIndex()) {
          if (index > 0) result.append(separator)
          result.append(transform(element))
      }
      result.append(postfix)
      return result.toString()
  }

  val letters = listOf("Alpha", "Beta")
  
  // Example usage of joinToString function
println(letters.joinToString()) // default conversion
println(letters.joinToString(transform = { it.toLowerCase() })) // lowercase transformation
println(letters.joinToString(separator = "! ", postfix = "! ", transform = { it.toUpperCase() })) // uppercase transformation with custom separator and postfix

// Invoke 예제.
// Define a function-like object using a lambda expression
val myFunction: (String) -> Unit = { str -> println(str) }

// Call the function using the invoke method
myFunction.invoke("Hello, world!") // Output: "Hello, world!"

// Alternatively, call the function using the shorthand syntax
myFunction("Hello again!") // Output: "Hello again!"
  • 간단히 말해서 'invoke'는 개체를 함수처럼 호출할 수 있는 프로그래밍의 특수 메서드입니다. Kotlin이나 Java와 같은 프로그래밍 언어로 함수형 객체를 만들면 함수형 인터페이스의 인스턴스로 구현됩니다. 이 기능적 인터페이스에는 함수의 인수를 가져와 그 결과를 반환하는 'invoke'라는 단일 메서드가 있습니다.
    일반 함수 호출 구문을 사용하여 이 함수와 유사한 객체를 호출하면 'invoke' 메서드가 함수를 실행하기 위해 백그라운드에서 자동으로 호출됩니다.
  • Kotlin에서 함수가 하나만 있는 객체를 정의하면 객체의 invoke 메서드를 명시적으로 지정하지 않고 약식 구문을 사용하여 호출할 수 있습니다.
fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = "",
    transform: ((T) -> String)? = null
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        val str = transform?.invoke(element) ?: element.toString()
        result.append(str)
    }
    result.append(postfix)
    return result.toString()
}

함수를 함수에서 반환

  • 함수가 함수를 인자로 받아야 할 필요가 있는경우가 많음.
enum class Delivery {
    STANDARD, EXPEDITED
}

class Order(val itemCount: Int)

fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        return { order -> 6 + 2.1 * order.itemCount }
    }
    return { order -> 1.2 * order.itemCount }
}

val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
println("Shipping costs: ${calculator(Order(3))}")
Shipping c o s t s 12.3
  • 함수를 반환하는 예제 배송방법에 따른 배송비 계산을 다르게 해야될 때 를 예제로 들고 있음..
  • 굳이 람다로..? 으음.. 이러면 좀 더 간결 한가?

람다를 활용한 중복 제거

data class SiteVisit(
  val path: String,
  val duration: Double,
  val os: OS
)

enum class OS {
  WINDOWS, LINUX, MAC, IOS, ANDROID
}

val log = listOf(
  SiteVisit("/", 34.0, OS.WINDOWS),
  SiteVisit("/", 22.0, OS.MAC),
  SiteVisit("/login", 12.0, OS.WINDOWS),
  SiteVisit("/signup", 8.0, OS.IOS),
  SiteVisit("/", 16.3, OS.ANDROID)
)

// 확장함수로 간결하게
fun List<SiteVisit>.averageDurationFor(os: OS) =
  filter { it.os == os }
      .map(SiteVisit::duration)
      .average()

val averageWindowsDuration = log.filter { it.os == OS.WINDOWS }
  .map(SiteVisit::duration)
  .average()

println(averageWindowsDuration) // Output: 23.0

println(log.averageDurationFor(OS.MAC)) // Output: 22.0

// 고차함수를 이용해 간결하게
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
  filter(predicate)
      .map(SiteVisit::duration)
      .average()

println(log.averageDurationFor {
  it.os in setOf(OS.ANDROID, OS.IOS)
}) // Output: 12.15

println(log.averageDurationFor {
  it.os == OS.IOS && it.path == "/signup"
}) // Output: 8.0
  • 여기선 마지막에 함수타입으로 코드 중복을 줄일 때 함수 타입이 도움이 된다하는데.
  • 크게 와닿지는 않았다 그냥 어디에다 필터를 정의하냐 차이인데.. 확장함수가 실무에선 도움이 가장 되는듯 하다.
  • 그래도 도움이되는건 전략패턴? 괜찮은거 같다

    인라인 함수: 람다의 부가 비용 없애기

  • 5장에선 람다를 무명 클래스로 컴파일 한다고 설명.
  • 람다가 변수를 포획할때마다 람다가 생성 시점 마다 새로운 무명 클래스 객체 생성
  • 이런경우 무명 클래스 생길때마다 부가 비용이 듬.
  • 그래서? 반복 코드를 별도의 라이브러리 함수로 빼내어 컴파일러가 자바 일반 명령분 처럼 효율적인 코드 생성되게?
  • inline 견경자를 사용하면 함수에 붙일시 컴파일러는 그 함수를 호출하는 모든 문장을
  • 함수 본문에 해당하는 바이트 코드로 바꿔치기 해줌.

인라이닝 작동하는 방식

  • inline 함수에 사용시 함수의 본문이 인라인이 됨.

  • 함수 본문을 번역해서 바이트코드로 컴파일 함.

  • 그러나 람다를 변수에 저장하거나 나중에 사용하는 경우 인라이닝 불가능

    val anotherLambda: () -> Unit
    
    if (someCondition) {
        anotherLambda = myLambda
    } else {
        anotherLambda = { println("Performing another action") }
    }
    
    inlineFunction(anotherLambda) // In this case, inlining is not possible
    		```
    

언제 사용되야 좋을까?

  • 함수 호출과 관련된 오버헤드를 줄여서 코드 성능을 최적화 할때.
  • 특히 람다를 인수로 허용하는 작고 자주 호출되는 함수나 고차원 함수일때..
  1. 작은 함수: 작은 함수를 인라인하면 함수 호출 및 반환의 오버헤드를 줄일 수 있으므로 특히 함수가 자주 호출되는 경우 성능이 크게 향상될 수 있습니다.
  2. 고차 함수: 람다 식을 고차 함수에 전달할 때 인라인을 사용하면 런타임에 익명 클래스나 함수 개체가 생성되지 않도록 방지할 수 있습니다. 람다 식의 코드가 호출 함수에 직접 삽입되므로 성능이 향상되고 메모리 사용량이 감소할 수 있습니다.
  3. 루프 최적화: 경우에 따라 인라인을 사용하면 컴파일러가 루프 언롤링 또는 루프 퓨전과 같은 루프 최적화를 수행하여 코드 성능을 더욱 향상시킬 수 있습니다.
  • 그러나 인라이닝은 컴파일된 코드의 크기를 증가해 항상 유익한건 아님.

  • 과도한 사용은 코드 부풀림 발생 -> 캐시미스 유발 컴파일 코드 효율성 떨어 트림..

       inline fun add(a: Int, b: Int): Int {
           return a + b
       }
    
       inline fun processList(list: List<Int>, action: (Int) -> Int): List<Int> {
           return list.map(action)
       }
    
       inline fun square(x: Int): Int {
           return x * x
       }
    
       fun main() {
           // Small function example
           val sum = add(3, 4)
           println("Sum: $sum")
    
           // Higher-order function example
           val doubled = processList(listOf(1, 2, 3)) { it * 2 }
           println("Doubled: $doubled")
    
           // Loop optimization example
           println("Squared:")
           for (i in 1..5) {
               println(square(i))
           }
       }
    Sum: 7
    Doubled: [2, 4, 6]
    Squared:
    1
    4
    9
    16
    25
    	```

인라인 함수의 한계

  • 람다를 변수에 저장했다가 나중에 사용하면 인라인 할 수 없음!
  • 특정 람다의 인라인 방지를위해 Noinline 예약어도 있음.

컬렉션 연산 인라이닝

  • 컬렉션에 작용하는 코틀린 표준 라이브러리 성능 알아보기

            //람다 구현 
      data class Person(val name: String, val age: Int)
    
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    
    println(people.filter { it.age < 30 }) // [Person(name=Alice, age=29)]
    
          //람다 구현 X
    data class Person(val name: String, val age: Int)
    
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    
    val result = mutableListOf<Person>()
    for (person in people) {
        if (person.age < 30) result.add(person)
    }
    
    println(result) // [Person(name=Alice, age=29)]
    
                            
  • 코틀린의 Filter함수는 인라인 함수 임.

  • filter 함수가 호출될 때, 람다 표현식의 바이트 코드와 함께 호출 위치에 직접 복사된다는 것을 의미합니다. 이로 인해 성능 면에서 우수한 최적화된 코드가 생성 됨

  • 람다 함수를 컬렉션과 함께 인라인으로 사용되는 경우가 많으며

  • 이런식으로 성능이 향상이 됨.

  • 그래서 일반함수를 사용할때 필터 대신 사용하면 해당 함수를 별도로 호출해서

  • 오버헤드가 추가되고 성능상 안좋을 수 있음

      data class Person(val name: String, val age: Int)
    
      fun main() {
          val people = listOf(Person("Alice", 29), Person("Bob", 31))
    
          // using lambda function
          val lambdaResult = people.filter { it.age < 30 }
          println(lambdaResult)
    
          // using regular function
          val regularResult = getYoungerThan30(people)
          println(regularResult)
      }
    
      fun getYoungerThan30(people: List<Person>): List<Person> {
          val result = mutableListOf<Person>()
          for (person in people) {
              if (person.age < 30) {
                  result.add(person)
              }
          }
          return result
      }
  • 여기 예제에서도 결과는 같지만 일반 함수는 함소 호출이 추가 되므로 오버헤드발생.

  • 책에서는 시퀀스(stream) 사용시 인라인 하지 않기 때문에 작은 크기 컬렉션은 일반 컬렉션 연산이 더나음

  • 대략적으로 10-100개 미만의 요소가 있는 컬렉션의 경우 성능 차이가 크지 않을 수 있음 실무에선 .. 그냥 람다써도 될 듯?

함수를 인라인으로 선언해야 하는 경우

  • 인라인을 여기저기 써봤자 람다를 인자로 받는 함수만 성능이 좋아 질 가능성이 높음..
  • JVM은 이미 일반 함수 호출의 경우 강력한 인라이닝 지원.
  • JVM은 코드 실행을 분석해 가장 이익이 되는 방향으로 호출을 인라이닝함
  • JVM은 Just in time 컴파일 기능을 가지고 있어 런타임에 코드를 분석해
  • 더 나은 성능을 위해 최적화 하는 것을 의미. 하나의 예로 함수 인라인화!
  • 코틀린에서 인라인 함수는? 컴파일시 함수 호출을 실제 함수 코드로 대체
  • 이는 코드 중복과 잠재적으로 큰 컴파일 코드 크기 초래.
  • 그래서 코틀린 인라인 함수 사용시 잠재적인 성능향상이 잠재적인 단점보다 우세한지 고려해서 사용..
                              
  • 책에선 람다를 인자로 받는 함수를 인라이닝시 이익이 더 많다 (아까 Filter?)
  1. 인라이닝을 통해 없앨 수 있는 부가 비용이 많다.
  • 함수 호출 비용 줄고 / 람다 표현 클래스와 람다 인스턴스에 해당하는 객체 만들 필요 X
  1. JVM은 함수 호출과 람다를 인라이닝 해줄 정도로 똑똑하지 못함
  2. 인라이닝 사용시 일반 람다에서는 사용할 수 없는 몇가지 기능 사용 가능.

예제 코드

  // 함수 호출 비용 절감
  fun add(a: Int, b: Int): Int {
      return a + b
  }

  // Lambda function
  val addLambda: (Int, Int) -> Int = { a, b -> a + b }

  // Usage
  val result1 = add(2, 3) // Function call
  val result2 = addLambda(2, 3) // Lambda call
  
  
  
  // 개체 생성 방지:
  // 이 예에서 performAction 함수는 람다를 인수로 사용합니다. 
  // 람다 호출을 사용하여 호출하면 람다에 해당하는 객체가 생성되지 않습니다. 
  // 반대로 개체 식을 사용하여 호출하면 람다 인터페이스를 구현하는 익명 클래스가 생성됩니다.
  fun performAction(action: () -> Unit) {
      action()
  }

  // Usage
  performAction { println("Hello World") } // Lambda call

  // Equivalent code using object expression
  performAction(object : () -> Unit {
      override fun invoke() {
          println("Hello World")
      }
  })
  
  
  // 비로컬 제어 흐름
  // 비로컬 제어 흐름은 중간 코드를 건너뛰고
  // 프로그램의 한 지점에서 다른 지점으로 제어를 전송하는 기능
inline fun performCalculation(numbers: List<Int>, operation: (Int) -> Int): Int {
    numbers.forEach {
        val result = operation(it)
        if (result < 0) return result // Non-local return
    }
    return 0
}

  // Usage
  val numbers = listOf(1, 2, 3, -4, 5)
  val result = performCalculation(numbers) {
      if (it < 0) return@performCalculation -1 // Labelled return
      it * 2
  }
  println(result) // -1
  • 첵에서는 인라인 변경자 함수에 붙일 때 주의 사항이 있다고 한다.
  • 함수가 큰 경우? 본문에 해당하는 바이트 코드를
  • 모든 호출 지점에 복사해 넣고나면 바이트 코드가 아주 커지기 때문.
  • 이럴 경우 람다 인자와 무관한 코드를 비인라인 함수로 빼내자.

자원 관리를 위해 인라인된 람다 사용

  • 자원을 획득하고 작업을 마친후 자원을 해제하는 자원관리?

  • 자바에서는 try-with-resource를써 해당객체를 획득하고 자동으로 닫아준다.

  • 코틀린에선 use라는 예약어가있음

    fun readFirstLineFromFile(path: String): String {
        BufferedReader(FileReader(path)).use { br ->
            return br.readLine()
        }
    }
  • 새로운사실! use는 함수를 닫을 수 있는(Closeable)자원에 대한 확장 함수다! (람다인자로받음)

  • use는 또 인라인 함수임.

  • 람다 본문안에 사용한 Return은 넌로컬 return 이문은 람다가 아니라 readFristLineFromFile 함수를 끝내면서 값 반환.

    넌로컬 reutrn?

  • 외부 함수나 클로저에서 반환하는 반환문을 의미

  • 비로컬 반환문은 여러 수준의 중첩된 함수나 클로저에서 빠져나올 수 있도록 해줌

  • 책에서는 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른블록을 반환하게 만드는 것을 말한다.

  • 책에 나온내용이 더이해하기 쉽네..

  • 바깥쪽에 함수를 반환시킬수 있는.. 음음.. 람다를 인자로 받는함수가 인라인함수인경우뿐

    람다로부터 반환 : 레이블을 사용한 Return

  • 람다식의 로컬 Return 사용 가능 , 이는 break랑 비슷함

    fun lookForAlice(people: List<Person>) {
      people.forEach label@{
          if (it.name == "Alice") return@label
      }
      println("Alice might be somewhere")
    }
    
  • label을 붙여서 표시함.

profile
어제의 나보다 한걸음 더

0개의 댓글