[코틀린 인 액션] CH8 고차 함수: 파라미터와 반환 값으로 람다 사용

0

코틀린 인 액션

목록 보기
9/13
post-thumbnail

[코틀린 인 액션] CH8 고차 함수: 파라미터와 반환 값으로 람다 사용

이 포스팅은 <<Kotlin in Action>>, 드미트리 제메로프 & 스베트라나 이사코바, 에이콘출판사(2017)을 읽고 개인 학습용으로 정리한 글입니다.

8.1 고차 함수 정의

  • 고차 함수: 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수

8.1.1 함수 타입

  • 함수 타입 정의
    (파라미터 타입 목록) -> 반환 타입

  • 그냥 함수를 정의하는 경우 Unit 반환 타입 지정 생략 가능
    함수 타입을 정의하는 경우 반환 타입 반드시 명시해야

  • 널이 될 수 있는 함수 타입 가능

var canReturnNull : (Int, Int) -> Int? = {x,y -> null}
var funOrNull: ((Int, Int) -> Int))? = null
  • 함수 타입에서 파라미터 타입 지정 가능
    -> 파라미터 이름은 타입 검 사시 무시됨
    -> 이 함수 타입의 람다를 정의할 때 파라미터 이름이 꼭 함수 타입 선언의 파라미터 이름과 일치하지 않아도 됨

8.1.2 인자로 받은 함수 호출

  • 인자로 받은 함수를 호출하는 구문 = 일반 함수를 호출하는 구문

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

  • 컴파일된 코드안에서 함수 타입은 일반 인터페이스로 바뀐다

  • 코틀린 표준 라이브러리는 함수 인자의 개수에 따라 인터페이스 제공
    -> ex. 인자가 없는 함수 Function0<<R>>, 인자가 하나인 함수 Function1<<R>>
    -> 각 인터페이스에는 invoke 메서드 정의 -> invoke를 호출하면 함수 실행

  • 함수 타입의 변수 = FunctionN 인터페이스를 구현하는 객체 저장
    -> 그 클래스의 invoke 메서드 본문 = 람다의 본문

  • 자바에서 함수 타입을 사용하는 코틀린 함수 호출할 때
    -> 자바 8:람다를 넘기면 자동으로 함수 타입 값으로 변환됨
    -> 자바 8 이전: 필요한 FunctionN 인터페이스의 invoke 메서드를 구현하는 무명 클래스 넘김
//코틀린 함수 선언
fun processTheAnswer(f: (Int) -> Int){
	println(f(42))
}
//자바 8
processTheAnswer({number -> number + 1});

//자바 8 이전
processTheAnswer(
    new Function1<Integer, Integer>(){
        @Override
        public Integer invoke(Integer number){
            return number+1;
        }
    }
);
  • 자바에서 코틀린 표준 라이브러리가 제공하는 람다를 인자로 받는 확장 함수 호출할 때
    -> 수신 객체를 확장 함수의 첫 번째 인자로 명시적으로 넘겨야
CollectionsKt.forEach(strings, s->{ //strings는 확장 함수의 수신 객체
    System.out.println(s);
    return Unit.INSTANCE; //Unit 타입의 값 명시적으로 반환해야
});

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

  • 널이 될 수 있는 함수 타입으로 함수를 받으면
    -> 그 함수 직접 호출할 수 X (NPE 발생 가능성이 있어 컴파일되지 X)
    -> null 여부 명시적으로 검사해야

  • 함수 타입이 invoke 메서드를 구현한 인터페이스라는 사실 이용하여 안전 호출
    -> 널이 될 수 있는 함수?.invoke()

8.1.5 함수를 함수에서 반환

  • 함수를 반환하려면 return식에 람다/멤버 참조/함수 타입의 값 등을 넣기

8.1.6 람다를 이용한 중복 제거

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

  • 코틀린은 보통 람다를 무명 클래스로 컴파일
    -> 람다식을 사용할 때마다 새로운 클래스가 만들어지지는 X
    -> 람다가 변수를 포획하는 경우: 람다가 생성되는 시점마다 새로운 무명 클래스 객체 생성

8.2.1 인라이닝이 작동하는 방식

  • 어떤 함수를 inline으로 선언하면 그 함수 본문이 inline된다
    -> 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기
inline fun <T> synchronized(lock: Lock, action: () -> T) : T{
    lock.lock()
    try{
        return action()
    }
    finally{
        lock.unlock()
    }
}

fun foo(l: Lock){
    println("Before sync")
    synchronized(l){
        println("Action")
    }
	println("After sync")    
}

fun main(){
	val l = Lock()	
	foo(l)
}
  • foo를 컴파일한 버전:
fun foo(l: Lock){
    println("Before sync")
    l.lock()
    try{
        return println("Action")
    }
    finally{
        l.unlock()
    }
	println("After sync")    
}
  • synchronized의 본문 뿐만 아니라 synchronized에 전달된 람다의 본문도 함께 인라인
    -> 코틀린 컴파일러는 그 람다를 무명 객체로 감싸지 X

  • 인라인 함수를 호출하면서 람다를 넘기는 대신 함수 타입의 변수를 넘기는 경우
    -> 람다 본문 인라이닝 되지 않고 synchronized 함수의 본문만 인라이닝

8.2.2 인라인 함수의 한계

  • 파라미터로 받은 람다를 다른 변수에 저장하고 나중에 그 변수를 사용하는 경우
    -> 람다를 표현하는 객체 존재해야
    -> 람다 인라이닝할 수 X

  • 둘 이상의 람다를 받는 인라인 함수에서 일부 람다만 인라이닝 하고 싶은 경우
    -> noinline 변경자 파라미터 이름 앞에 붙이기

inline fun foo(inlined: ()->Unit, noinline notInlined: () -> Unit){ ... }
  • 자바에서 코틀린에서 정의한 인라인 함수 호출 가능
    -> 이 경우 컴파일러는 인라인 함수를 인라이닝하지 않고 일반 함수 호출로 컴파일

8.2.3 컬렉션 연산 인라이닝

  • filter와 map은 인라인 함수
    -> 두 함수의 본문은 인라이닝 됨 -> 추가 객체/클래스 생성 X
    -> 하지만 결과를 저장하는 중간 리스트 생성

  • asSequence를 통해 리스트 대신 시퀸스 사용
    -> 중간 시퀸스는 람다를 필드에 저장하는 객체로 표현됨
    -> 최종 연산은 중간 시퀸스에 있는 여러 람다를 연쇄 호출
    -> 시퀸스는 람다를 저장해야하므로 람다 인라인 X

  • 시퀸스 연산에서는 람다가 인라이닝되지 않기 때문에
    크기가 작은 컬렉션은 오히려 일반 컬렉션 연산이 성능이 좋을 수도

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

일반 함수를 인라인하는 경우 👎

  • 일반 함수의 경우 JVM은 코드 실행을 분석해 가장 이익이 되는 방향으로 호출 인라이닝
    -> 이 과정은 바이트코드를 기계어 코드로 번역하는 과정(JIT)에서 일어남

  • 코틀린 인라인 함수는 바이트 코드에서 각 함수 호출 지점을 함수 본문으로 대치
    -> 코드 중복 발생
    -> 함수를 직접 호출하면 스택 트레이스 더 깔끔함

람다 함수를 인라인하는 경우 👍

  • 함수 호출 비용 줄일 수 O, 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체 만들 필요 X

  • 인라이닝을 사용하면 일반 람다에서는 사용할 수 없는 몇 가지 기능 사용 가능
    -> ex. 넌로컬(nonlocal) 반환

  • 하지만 인라이닝하는 함수가 큰 경우 바이트 코드 매우 커질 수 있음

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

  • 자원(resource): 파일, Lock, 데이터베이스 트랜잭션 등

  • 자원 관리 패턴
    -> 보통 사용하는 방법: try 블록 시작 직전에 자원 획득, finally 블록에서 자원 해제

  • 코틀린 withLock 함수: Lock 인터페이스의 확장 함수

fun<T> Lock.withLock(action: () -> T):T{
    lock()
    try{
        return action()
    }finally{
        unlock()
    }
}
  • 코틀린 use 함수: 자바 try-with-resource와 같은 기능
    -> 닫을 수 있는(closable) 자원에 대한 확장 함수 (인라인 함수)
    -> 람다를 인자로 받아 람다를 호출한 다음 자원 닫아줌

8.3 고차 함수 안에서 흐름 제어

8.3.1 람다 안의 return문: 람다를 둘러싼 함수로부터 반환

  • 람다 안에서 return을 사용하면 람다로부터만 반환되는 것 X, 그 람다를 호출하는 함수가 실행 종료되고 반환

  • 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return
    -> 넌로컬 return

  • 인라이닝되지 않는 함수에 전달되는 람다 안에서 return 사용 X

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

  • 람다 식에서 로컬 리턴 사용 가능
    -> for루프의 break와 비슷한 역할

  • 로컬 return과 넌로컬 return 구분을 위해 레이블 사용

    • return으로 실행을 끝내고 싶은 람다 식 앞에 레이블(label@)을 붙이고
      return 키워드 뒤에 그 레이블(@label) 추가
    • 람다를 인자로 받는 인라인 함수의 이름을 return 뒤에 붙임
  • this의 레이블에도 같은 규칙 적용됨

    • 수신 객체 지정 람다 앞에 레이블(sb@)을 붙이고
      this 뒤에 그 레이블(@sb) 추가
    • 람다를 인자로 받는 인라인 함수의 이름을 this 뒤에 붙임

8.3.3 무명 함수: 기본적으로 로컬 return

  • 무명 함수
    -> 함수 이름이나 파라미터 타입 생략 가능
    -> 반환 타입 지정 규칙 일반 함수와 동일
    (블록이 본문인 경우 반환 타입 명시, 식이 본문인 경우 생략 가능)

  • 무명 함수 안에서 레이블이 붙지 않은 return 식: 무명 함수 자체 리턴

  • return에 적용되는 규칙: fun 키워드를 사용해 정의된 가장 안쪽 함수 반환

📌참고자료

함수의 인터페이스 구현으로 객체를 넘겨주는 경우
-> 메서드를 호출할 때마다 새로운 객체 생성됨
함수의 인터페이스 구현으로 람다를 넘겨주는 경우
-> 람다에 대응하는 무명 객체 메서드를 호출할 때마다 반복 사용
-> but 람다가 주변 영역의 변수를 포획하는 경우 반복 사용 X, 새로운 인스턴스 생성됨

profile
Be able to be vulnerable, in search of truth

0개의 댓글