본문 바로가기
Study/다재다능 코틀린 프로그래밍

Day 10. 람다를 사용한 함수형 프로그래밍

반응형

 

본 포스트는 지인들과 스터디한 내용을 정리한 포스트입니다

 

http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931463422

 

다재다능 코틀린 프로그래밍 - 교보문고

다양한 프로그래밍 패러다임으로 살펴보는 코틀린 기본서 | 코틀린은 멀티패러다임 프로그래밍 언어다. 코틀린은 스크립트로 사용할 수도 있고, 객체지향 코드나 함수형 코드, 비동기 프로그램

www.kyobobook.co.kr


1. 함수형 스타일

  • 함수형 스타일은 선언적 스타일에서 태어났다
  • 선언적 스타일은 개발자가 원하는 곳을 말하면 호출한 함수가 원하는 것을 처리해준다
  • 선언적 스타일에서는 레이어 아래에 캡슐화 되어있다
  • 개발자는 중요하다고 생각하고 확인해 볼 필요가 있다면 디테일을 확인할 수 있고, 그게 아니면 더 이상 개발자를 귀찮게 하지 않는다 ( 확인할 필요가 없다 )
  • 명령형 스타일 vs 함수형 스타일
    //doubleofeven.kts
    
    // 명령형 스타일
    var doubleOfEven1 = mutableListOf<Int>()
    for (i in 1..10) {
        if (i % 2 == 0) {
            doubleOfEven1.add(i * 2)
        }
    }
    
    
    // 함수형 스타일
    var doubleOfEven2 = (1..10)
        .filter { e -> e % 2 == 0 }
        .map { e -> e * 2 }​

    • 명령형 스타일은 익숙하다. 하지만 복잡하다. 익숙함 때문에 쓰기는 쉽지만 읽기가 매우 어렵다
    • 함수형 스타일은 좀 덜 친숙하다. 하지만 단순하다. 익숙하지 않기 때문에 작성하기는 어렵지만, 읽기가 쉽다
    • 하지만 매번 함수형을 쓰는 게 명령형을 사용하는 것보다 좋은 것은 아니다
    • 함수형 스타일은 코드가 연산에 집중하고 있을 때 써야 한다(변경가능성과 부작용 회피 가능)

 

2. 람다 표현식

  • 람다는 고차함수에 아규먼트로 사용되는 짧은 함수이다
  • 함수에 데이터를 전달하는 대신 람다를 이용해서 실행 가능한 코드를 함수에 전달할 수 있다
  • 람다의 구조
    • 람다 표현식은 이름이 없고 타입추론을 이용한 리턴 타입을 가지는 함수이다
    • 파라미터 리스트와 바디 두가지만 가지고 있다
    • 람다를 함수의 아규먼트 리스트로 전달할 때, 마지막 아규먼트로 전달하는 것이 좋다 ( 가독성 때문에 )
  • 람다 전달
    • 예제코드
      // lambdaexample.kts
      fun isPrime(n: Int) = n > 1 && (2 until n).none { i: Int -> n % i == 0 }​
    • none() 함수를 주목하자
    • none() 함수로 전달된 람다의 파라미터 리스트는 파라미터 i의 타입을 Int로 지정한다
    • 코틀린이 파라미터에는 타입추론을 적용하지 않기 때문에 함수에 전달되는 각 파라미터의 타입이 필요하다
    • 하지만 람다의 파라미터에는 타입을 필요로 하지 않는다 ( 메소드의 시그니처에서 추론 가능하다 )
  • 암시적 파라미터 사용
    • 람다가 하나의 파라미터만 받는다면 파라미터 정의를 생략하고 특정 이름(it)을 부여하여 암시적 파라미터를 사용할 수 있다
    • 예제코드
      // lambdaexample.kts
      fun isPrime2(n: Int) = n > 1 && (2 until n).none { n % it == 0 }​
    • "it"를 이용하면 기존에 파라미터 정의를 없앨 수 있다
    • 단 람다가 파라미터가 없는 람다인지 아니면 it를 사용하여 파라미터 하나를 취하는 람다인지 빠르게 파악하기 어렵다 ( 여러 줄로 이루어진 람다에서는 유지보수가 힘들기 때문에 사용을 자제해야 한다 )
  • 람다 받기
    • 여러 방식의 표현
      // lambdaiterate.kts
      fun walk1To(n: Int, action: (Int) -> Unit) =
          (1..n).forEach { action(it) }
      
      walk1To(5, { i -> print(i) }) // 12345
      walk1To(5) { i -> print(i) } // 12345
      walk1To(5) { print(it) } // 12345
      walk1To(5) { ::println } // 메서드 참조
    • 메서드 참조
      • 입력받는 파라미터와 전달되는 람다내부의 함수 파라미터와 일치하다면 '::'를 이용해서 메서드 참조를 이용할 수 있다

 

3. 람다와 익명함수

  • 람다의 중복코드를 피하기 위해서는 2가지 방법이 있다
    • 재사용을 위해서 람다를 변수에 담는 것
    • 람다 대신 익명 하수를 사용하는 것
  • 예제코드
    // savelambdas.kts
    val names = listOf("Pam", "Pat", "Paul", "Paula")
    
    val checkLength5 = { name: String -> name.length == 5 } // (String) -> Boolean 타입추론
    val checkLength6: (String) -> Boolean = { name -> name.length == 5 }
    val checkLength7 = fun(name: String): Boolean { return name.length == 5 } // 익명함수​

 

4. 클로저와 렉시컬 스코핑

  • 람다에는 상태가 없다
  • 외부 상태에 의존하여 람다 내부에서 사용하고 싶은 경우가 있다 ( 클로저 )
  • 람다는 스코프를 로컬이 아닌 속성과 메소드로 확장할 수 있기 때문
    // clousures.kts
    val factor = 2
    val doubleIt = { e: Int -> e * factor }
    
    println(doubleIt(3)) // 6​
    • doubleIt 메서드, 람다 시그니처를 정의하고 있는 해당 바디 부분에는 변수 또는 속성인 factor가 없다
    • 컴파일러는 factor 변수에 대한 클로저의 범위(스코프)에서 참조되는 변수를 찾지 못한다면 점차 스코프를 확장시킨다 ( 렉시컬 스코핑 )
  • 리스트 vs 시퀀스
    // mutable.kts
    var factor = 2
    val doubled = listOf(1, 2).map { it * factor }
    val doubledAlso = sequenceOf(1, 2).map { it * factor }
    factor = 0
    doubled.forEach { println(it) } // 2, 4
    doubledAlso.forEach { println(it) } // 0, 0​

    • 자세한 내용은 Chapter 11에서 다룰 예정

 

5. 비지역성(non-local)과 라벨(labeled) 리턴

  • 람다는 리턴값이 있더라도 return 키워드를 가질 수 없다 ( 익명 함수와 차이점 )
    • 람다 내부의 블록에서 return이 의미하는 바는 모호하다
    • 즉시 람다에서 빠져나와 나머지 로직을 수행하는 것인지
    • 람다를 호출하고 있는 부분에서 아에 빠져나오는 것인지
  • 라벨 리턴
    • 예제코드
      //noreturn.kts
      fun invokeWith(n: Int, action: (Int) -> Unit) {
          println("enter invokeWith $n")
          action(n)
          println("exit invokeWith $n")
      }
      
      
      fun caller() {
          (1..3).forEach { i ->
              invokeWith(i) here@ {
                  println("enter for $it")
                  if (it == 2) {
                      return@here
                  }
                  println("exit for $it")
              }
          }
          println("end of caller")
      }
      
      caller()
      println("after return from caller")
      
      //enter invokeWith 1
      //enter for 1
      //exit for 1
      //exit invokeWith 1
      //enter invokeWith 2
      //enter for 2
      //exit invokeWith 2
      //enter invokeWith 3
      //enter for 3
      //exit for 3
      //exit invokeWith 3
      //end of caller
      //after return from caller​
    • 라벨리턴은 람다의 흐름을 제어해서 라벨 블록으로 점프하기 위해서 만들어졌다 ( 람다를 빠져나가기 위함 )
    • 명령형 스타일 프로그래밍에서 반복문을 사용할 때 이후 과정을 생략하고 반복문의 마지막으로 가기 위해 사용하던 continue와 동일하다
    • @here: 명시적인 라벨
    • @{람다가 전달된 함수의 이름}: 암시적인 라벨
    • 명시적 라벨이 의도를 명확하게 보이게 하고 코드를 쉽게 이해할 수 있게 도와주므로 명시적 라벨 사용을 권장
  • 정리
    • return은 람다에서 기본적으로 허용이 안된다
    • 라벨 리턴(@ 키워드)을 사용하면 현재 동작중인 람다를 스킵할 수 있다
    • 논로컬 리턴을 사용하면 현재 동작중인 람다를 선언한 곳 바깥으로 나갈 수 있다. 하지만 람다를 받는 함수가 inline으로 선언된 경우만 사용 가능하다
    • 람다를 빠져나가기 위해서 언제든지 라벨 리턴을 사용할 수 있다
    • return을 사용할 수 있다면 람다에서 빠져나가거나 람다를 호출한 곳을 빠져나가는 것이 아니라 람다가 정의된 곳에서 빠져나간다는 사실을 기억해야한다

 

6. 람다를 이용한 인라인 함수

  • 람다는 퍼포먼스를 신경써야한다
  • 코틀린은 람다를 사용할 때 호출 오버헤드를 제거하고 성능을 향상시키기 위해서 inline 키워드를 제공한다
  • 예제코드
    // noinline.kts
    import kotlin.RuntimeException
    
    fun invokeTow(
        n: Int,
        action1: (Int) -> Unit,
        action2: (Int) -> Unit
    ): (Int) -> Unit {
        println("enter invokeTwo $n")
        action1(n)
        action2(n)
        println("exit invokeTwo $n")
        return { _: Int -> println("lambda returned from invokeTwo") }
    }
    
    fun report(n: Int) {
        println("")
        print("called with $n, ")
        val stackTrace = RuntimeException().stackTrace
        println("Stack depth: ${stackTrace.size}")
        println("Partial listing of the stack:")
        stackTrace.take(3).forEach(::println)
    }
    
    
    fun callInvokeTwo() {
        invokeTow(1, { i -> report(i) }, { i -> report(i) })
    }
    callInvokeTwo()
    
    
    //enter invokeTwo 1
    
    //called with 1, Stack depth: 31
    //Partial listing of the stack:
    //Noinline.report(noinline.kts:18)
    //Noinline$callInvokeTwo$1.invoke(noinline.kts:26)
    //Noinline$callInvokeTwo$1.invoke(noinline.kts:26)
    //
    //called with 1, Stack depth: 31
    //Partial listing of the stack:
    //Noinline.report(noinline.kts:18)
    //Noinline$callInvokeTwo$2.invoke(noinline.kts:26)
    //Noinline$callInvokeTwo$2.invoke(noinline.kts:26)
    //exit invokeTwo 1​
  • 인라인 최적화
    • inline 키워드를 이용해서 람다를 받는 함수의 성능을 향상시킬 수 있다
    • 함수가 inline으로 선언되어있으면 함수를 호출하는 대신 함수의 바이트코드가 함수를 호출하는 위치에 들어가게 된다
    • 함수 호출의 오버헤드를 제거할 수 있지만 함수가 호출되는 모든 부분에 바이트코드가 위치하기 때문에 바이트코드가 커지게 된다 ( 일반적으로 긴 함수를 인라인으로 사용하는건 좋은 생각이 아니다 )
    • 리팩토링 코드
      // inline.kts
      import kotlin.RuntimeException
      
      inline fun invokeTow(
          n: Int,
          action1: (Int) -> Unit,
          action2: (Int) -> Unit
      ): (Int) -> Unit {
          println("enter invokeTwo $n")
          action1(n)
          action2(n)
          println("exit invokeTwo $n")
          return { _: Int -> println("lambda returned from invokeTwo") }
      }
      
      fun report(n: Int) {
          println("")
          print("called with $n, ")
          val stackTrace = RuntimeException().stackTrace
          println("Stack depth: ${stackTrace.size}")
          println("Partial listing of the stack:")
          stackTrace.take(3).forEach(::println)
      }
      
      
      fun callInvokeTwo() {
          invokeTow(1, { i -> report(i) }, { i -> report(i) })
      }
      callInvokeTwo()
      
      //enter invokeTwo 1
      //
      //called with 1, Stack depth: 28
      //Partial listing of the stack:
      //Noinline.report(noinline.kts:18)
      //Noinline.callInvokeTwo(noinline.kts:26)
      //Noinline.<init>(noinline.kts:28)
      //
      //called with 1, Stack depth: 28
      //Partial listing of the stack:
      //Noinline.report(noinline.kts:18)
      //Noinline.callInvokeTwo(noinline.kts:26)
      //Noinline.<init>(noinline.kts:28)
      //exit invokeTwo 1​
    • 기존 소스와 비교해보면 Stack depth가 31 -> 28로 줄었다
    • inline이 될 함수가 매우 크거나, 많은 곳에서 호출한다면 해당 부분마다 바이트코드로 들어가기 때문에 사용하지 않을때에 비해 바이트 코드가 많아지게 된다 ( 주의사항 )
  • 선택적 noinline 파라미터
    • 어떤 이유로 람다 호출을 최적화하지 않을 수 있다
    • 람다의 파라미터를 noinline으로 표시하여 최적화를 제거할 수 있다
    • noinline 키워드는 함수가 inline인 경우에만 파라미터에 사용할 수 있다
  • 정리
    • 라벨이 없는 리턴은 항상 함수에서 발생하며 람다에서는 발생하지 않는다
    • 라벨이 없는 리턴은 인라인이 아닌 람다에서 허용되지 않는다
    • 함수명은 라벨의 기본 값이 되지만 이를 맹신해서는 안된다.
    • 라벨 리턴을 사용할 거라면 항상 라벨명을 지어야 한다
    • 일반적으로 코드 최적화를 하기 전에 성능 측정을 먼저 하라. 특히 람다를 사용하는 코드라면 성능 측정을 먼저 해야한다
    • inline은 눈에 띄는 성능 향상이 있을 때만 사용하라

 

728x90
반응형