선생님, 개발을 잘하고 싶어요.

Kotlin in Action - 2부 8장 - 고차 함수: 파라미터와 반환 값으로 람다 사용 본문

일상/책 리뷰

Kotlin in Action - 2부 8장 - 고차 함수: 파라미터와 반환 값으로 람다 사용

알고싶은 승민 2022. 5. 28. 19:07

다루는 거

  • 고차 함수: 람다를 인자로 받거나 반환하는 함수
  • 인라인 함수
  • 비로컬 return, 레이블, 로컬 return
  • 무명 함수

고차함수

  • 다른 함수를 인자로 받거나 함수를 반환하는 함수
  • 코틀린으로 말하면, 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수
    • 코틀린에서 함수 변수 표현을 하기 위해서 람다, 함수 참조를 사용한다고 5장에서 말함.
  • 인자로 받은 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 같다.
  • 컴파일된 코드에서 함수 타입은 일반 인터페이스로 표시된다. Function0<R>, Function1<P1, R> …
  • 함수 타입 파라미터도 디폴트 값을 지정하거나 nullable 함수 타입을 만들 수 있다.
  • 변수, 프로퍼티, 파라미터 등을 사용해 데이터 중복을 없애는 것과 같이, 람다를 사용해 코드의 중복을 없앤다.
  • 디자인 패턴 중 전략 패턴을 구현하기 위한 인터페이스 구현 - 서브 클래싱의 구조를 탈피해서 일반 함수 타입을 사용해 전략을 표현할 수 있다.

람다 인자 타입 표현

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

람다의 부가 비용

  • 코틀린이 보통 람다를 무명 클래스로 컴파일한다. 항상 그런 것은 아니고, 변수를 포획하면 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생긴다(새로운 인스턴스 생성).
  • 따라서 무명 클래스 생성에 따른 부가 비용이 든다.

인라이닝 작동 방식

  • 함수를 호출하는 코드를 함수를 호출하는 바이트 코드가 아니고 함수 본문을 번역한 바이트 코드로 컴파일한다.
  • 이 경우 함수의 본문뿐 아니라, 인자로 전달된 람다의 본문도 함께 인라이닝된다.
  • 컴파일러는 그 람다를 함수 인터페이스를 구현하는 무명 클래스로 감쌀 필요가 없다.
  • 람다 대신 함수 타입의 변수를 넘긴다면, 인라인 함수 호출 시점에 변수에 저장된 람다 코드를 알 방법이 없으므로 무명 클래스로 감싸는 수밖에 없다.

한계

  • 람다가 본문에 직접 펼쳐지기 때문에 함수가 람다를 본문에 사용하는 방식이 한정된다.
  • 파라미터로 받은 람다를 다른 변수에 저장하고 그 변수를 추후 사용한다면, 람다를 표현하는 클래스가 어딘가에는 존재해야 하는 상황이므로 람다를 인라이닝할 수 없다.
  • 둘 이상의 람다를 인자로 받는데, 일부 람다만 인라이닝하고 싶을 때는, 인라이닝하면 안 되는 람다를 noinline 변경자를 추가한다.
  • 자바에서도 코틀린에서 정의한 인라인 함수를 호출할 수 있다. 이 경우 컴파일러가 인라이닝 하지 않고 일반 함수 호출로 컴파일한다.

컬렉션 연산 인라이닝

// 표준 라이브러리 활용
val people = listOf(Person("Alice", 29), Person("Bob", 31))
people.filter { it.age < 30 }

// 직접 구현
val result = mutableListOf<Person>()
for (person in people) {
  if (person.age < 30) result.add(person)
}
  • 이러한 코드에 대해서, 표준 라이브러리에 있는 filter가 인라인 함수이므로 위 두 구현의 바이트코드가 거의 같다.
  • 고로, 코틀린다운 연산을 안전하게 사용하고, 인라이닝을 믿고 성능에 신경 쓰지 않아도 된다.
  • filter, map 체이닝 시점에, 각 호출마다 새로운 리스트 객체를 만든다.
  • 처리할 원소가 많아지고 중간 리스트 부가 비용을 피하기 위해서 Sequence를 사용할 수 있다.
  • 하지만 Sequence는 인자로 받은 람다를 필드로 저장해서 Lazy하게 계산하므로, 인라이닝 할 수 없다.
  • 고로, 오히려 크기가 작은 컬렉션은 일반 컬렉션 연산이 더 성능이 나을 수 있다.

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

  • inline 키워드를 사용해도 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높다.
  • 일반 함수 호출의 경우 JVM은 이미 강력하게 인라이닝을 지원한다. (?!)
  • 람다를 인자로 받는 함수를 인라이닝해서 얻는 이익
    • 함수 호출 비용 줄일 수 있음
    • 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체를 만들 필요가 없음
    • JVM은 람다를 인라이닝 해주지 못함
    • 일반 람다에서는 사용할 수 없는 몇 가지 기능을 사용할 수 있음 (e.g. non-local return)

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

  • 람다로 중복을 업앨 수 있는 일반적인 패턴
    • 자원 관리 (파일, 락, 데이터베이스, 트랜잭션)
      • try/finally 문을 사용하되 try 시작 전 자원 획득 - finally에서 자원 해제
      • 자바는 try-with-resource와 같은 특별한 언어 차원 구문을 제공하지만, 코틀린은 인라인 함수와 람다 인자로 인해 언어 차원으로 제공하지 않고 라이브러리로 제공함 (use)

고차 함수 안에서 흐름 제어

  • 람다 안의 return문: 람다를 둘러싼 함수로부터 반환
  • 넌로컬 return (non-local return): 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만듬
    • 람다를 인자로 받는 함수가 인라인 함수인 경우만 가능
    • 왜 그럴까? 인라이닝되지 않는 함수는 람다를 변수에 저장할 수 있다는 뜻이고, 람다가 호출되는 시점에는 이미 람다를 생성한 함수 흐름이 끝나있을 수 있다. 고로 바깥쪽 함수를 반환시키기엔 너무 늦었을 수 있다. 그렇기에 제공될 수 없다.
  • 레이블을 사용한 return (람다 반환)
    • 물론 람다 식에서도 local return이 가능하다.
    • 로컬 return (local return): 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 이어간다.
    fun lookForAlice(people: List<Person>) {
      people.forEach labelSomething@ { // 람다 식 앞에 레이블을 붙힌다.
        if (it.name == "Alice") return@labelSomething // 앞에서 정의한 레이블 참조
      }
    }
    
    fun lookForAlice(people: List<Person>) {
      people.forEach { // 람다 식 앞에 레이블 지정 따로 안하면
        if (it.name == "Alice") return@forEach // 인라인 함수 이름을 레이블로 사용할 수 있다.
      }
    }
    
  • 무명 함수: 기본적으로 로컬 return
    • 일반 함수와 비슷해 보이지만 실제로는 람다 식에 대한 문법적 편의
    • 람다 구현 방법이나 람다 인라인 함수에 넘길 때 어떻게 인라이닝 되는지 등의 규칙은 같다.
    fun lookForAlice(people: List<Person>) {
      people.forEach(fun (person) { // 파라미터 타입 추론되었다. fun 키워드를 붙혔다.
        if (it.name == "Alice") return // 기본적으로 무명 함수가 반환된다. (가장 가까운 fun)
      })
    }
    
Comments