Lambda

람다 식은 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각이다. 쉽게 말해 변수에 담거나 인자로 넘길 수 있는 작은 함수를 뜻한다. 람다도 식이므로 식의 마지막에서 리턴 문 없이 반환할 수 있다.

Usage

// declare & execute
val lambda = {arg: Int -> 
  ...
  arg * 2 // return arg * 2
}
val ret = lambda(1) // 2

// 익명 람다 실행: run
run {
  prinln("Anonymous lambda")
}

// use with function
val people = listOf(Person("aaa", 26), Person("bbb", 27))
val oldest = people.maxBy { it.age }      // 프로퍼티 참조
val youngest = people.minBy(Person::age)  // 멤버참조

외부 변수 포획

자바 메소드 안에서 무명 내부 클래스를 정의한 경우 메소드의 로컬 변수 중 불변(final) 변수만 접근할 수 있지만 코틀린은 모든 로컬 변수에 접근하고 또 수정할 수 도 있다. 람다에서 접근하는 로컬 변수를 람다가 포획한 변수라 한다.

함수 내의 로컬 변수는 함수가 종료되면 같이 사라진다. 하지만 변수를 포획한 람다를 함수가 반환하더라도 로컬 변수에 접근 할 수 있다. 함수가 종료되더라도 메모리에 살아있는 것이 아니라 불변 변수는 람다 내로 복사하고 일반 변수는 특별한 래퍼 객체로 감싸 인스턴스의 참조를 람다 내에 저장하는 방식으로 작동한다.

Sequence

자바에서는 자바8에서 람다를 통한 컬렉션 처리를 위해 지연 연산으로 작동하는 스트림을 도입했다. 코틀린은 기본적으로 표준 함수에서 컬렉션을 다루는 확장함수를 제공하므로 스트림이 필요하지 않을 것 같지만, 표준 함수는 지연 연산이 아닌 즉시 연산으로 작동한다. 대신 스트림을 대신하는 시퀀스를 제공한다.

시퀀스는 스트림과 마찬가지고 지연 연산으로 작동한다. 차이점은 스트림은 병렬 처리를 지원하지만 시퀀스는 지원하지 않는다. 병렬 처리를 필요로 한다면 스트림을 사용하자.

컬렉션을 시퀀스로 만들기 위해서는 확장 함수 Type.asSequence() 메소드 또는 generateSequence 메소드를 사용한다. 기본적인 사용법은 스트림과 유사하다

listOf(1,2,3,4).asSequnce() // 시퀀스 화
  .map { it * it }          // 중간 연산 1
  .filter { it % 3 == 0 }   // 중간 연산 2
  .toList()                 // 최종 연산

지연 연산에 따른 성능 향상 때문에 항상 시퀀스를 사용하고 싶을 수 도 있다. 그러나 지연 연산의 효과로 연산의 양을 줄이는 경우가 아니라면 일반적인 연산에서는 매우 큰 컬렉션에 시퀀스를 사용할 때만 성능 향상이 있고 작은 컬렉션에는 오히려 느릴 수 있다는 것에 유의.

Functional Interface

자바에서는 메소드를 한개만 가진 인터페이스를 함수형 인턴페이스(Functional Interface) 또는 SAM(Single Abstration Method) 인터페이스라고 한다. 자바에서는 이런 인터페이스를 활용하는 경우가 많은데 코틀린은 함수형 인터페이스를 인자로 취하는 자바 메소드를 호출할 때 람다를 넘길 수 있게 해준다.

자바 코드로 컴파일 시 람다 코드를 함수형 인터페이스를 구현하는 무명 클래스로 만들고 람다가 메소드가 되는 방식으로 동작한다. 따라서 람다의 인자의 개수와 인자 타입과 반환 타입이 구현하는 메소드와 같아야 한다.

// 자바 코드
interface Runnable { void run(); }
void postponeComputation(int delay, Runable computation);

// 무명 클래스 사용
postponeComputation(0, object: Runnable {
  override fun run() {
    println("anonymous class")
  }
})

// 일반 람다 사용
postponeComputation(1) { println("lambda") }

SAM 생성자

인자로 람다를 넘기면 코틀린 컴파일러가 알아서 함수형 인터페이스의 인스턴스로 바꿔주지만 함수형 인터페이스를 반환해거나 변수에 저장해야 한다면 일반 람다로는 불가능하다. 이 때 람다를 함수형 인터페이스의 인스턴스로 변환하는 생성자를 SAM 생성자라 한다. SAM 생성자는 람다 앞에 함수형 인터페이스의 이름을 명시하여 사용한다.

val ruunableInst = Runnable {
  ...
}

fun createRunnable() {
  return Runnable {
    ...
  }
}

수신 객체 지정 람다

수신 객체 지정 람다는 수신 객체를 명시하지 않고 람다의 본문에서 객체의 메소드를 호출할 수 있도록 하는 특수한 람다이다.

with

with(object, lambda)는 수신 객체를 넘기고 람다의 결과를 반환하는 함수 이다.

fun alphabet() = with(StringBuilder()) {
  for (letter in 'A'..'Z') append(letter)
  toString()  // return this.toString()
}

apply

T.apply(lambda): T는 람다를 적용한 수신 객체를 반환하는 함수다.

fun alphabet() = StringBuilder().apply {
  for (letter in 'A'..'Z') append(letter)
}.toString()

주의

만약 수신 지정 람다를 사용하는 함수가 어떤 클래스의 메소드이고 클래스의 메소드의 이름이 수신 객체의 메소드 이름과 동일한 경우 기본적으로 수신 객체의 메소드를 호출한다. 만약 바깥 클래스의 메소드를 호출하고 싶다면 this@OuterClass.method()를 사용한다

Higher Order Function

고차함수(Higher Order Function)란 다른 함수를 인자로 받거나 함수를 반환하는 함수를 말한다. 코틀린 표준 라이브러리가 제공하는 filter, map 등은 람다 식를 인자로 받으므로 고차함수다.

Function Type

자바에서는 고차함수를 만들기 위해 Function 인터페이스를 사용하지만 코틀린에서는 함수가 타입으로 존재한다.

val sum: (Int, Int) -> Int = {x, y -> x + y}

함수타입은 매개변수 타입을 괄호 안에 작성하고 화살표와 리턴 타입을 명시하여 정의한다. 위의 코드는 타입을 명시했지만 타입추론이 작동하기 때문에 함수 타입을 명시하지 않고 람다 내에서 타입을 표기해도 된다.

매개변수의 타입만 작성해도 되지만 코드의 가독성을 위해 매개변수 이름도 같이 표기해도 된다. 하지만 람다 식를 작성할 때 매개변수의 이름과 타입 파라미터 이름과 같지 않아도 된다.

fun foo((x: Int, y: Int) -> Int) {
  ...
}

foo {a, b -> ... }

Inline Function

람다는 보통 무명 클래스로 컴파일된다. 매번 무명 클래스의 인스턴스를 생성하진 않지만 람다가 외부 변수를 포획한 경우 람다를 실행할 때마다 무명 객체를 생성한다. 이런 경우 인스턴스 생성에 따른 오버헤드가 발생한다.

inline 변경자를 함수 앞에 붙이면 컴파일러가 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔준다. 인자로 넘겨받은 람다도 인라이닝하므로 일반 함수의 경우 성능 향상이 적지만 매번 인스턴스를 생성해야하는 람다의 경우 성능상 이점을 가져온다.

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
  lock.lock()
  try {
    return action()
  } finally {
    lock.unlock()
  }
}

// 함수 실행
synchronized(lock, { println("action") })

// 컴파일 시 함수 실행 코드
synchronized(lock: Lock){
  lock.lock()
  try {
    println("action")   // 람다 인라이닝
  } finally {
    lock.unlock()
  }
}

한계

단순히 넘겨받은 람다를 실행한다면 람다를 인라이닝 할 수 있다. 문제는 람다를 변수에 저장하고 나중에 변수를 사용한다면 람다의 인스턴스가 필요하다. 이러한 경우 인라이닝이 불가능하다.

fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
  return TransformingSeqeunce(this, transfrom)
}

위의 함수와 같이 시퀀스에 대해 동작하는 메소드 중에는 람다를 받아서 시퀀스 원소에 람다를 적용하고 새 시퀀스 객체를 반환하는 경우가 많다. 위의 함수는 시퀀스 생성자에 람다를 넘기고 람다를 내부 프로퍼티로 저장한다. 이러한 경우 인라이닝을 사용할 수 없는 것이다.

둘 이상의 람다를 인자로 넘기는데 특정 람다가 코드량이 많고 인라이닝을 하면 안되는 코드라고 가정하자. 이러한 경우 파라미터 앞에 noinline 변경자를 붙이면 인라이닝을 하지 않는다.

inline fun <T, R> Sequence<T>.map(noinline transform: (T) -> R): Sequence<R> {  // 람다를 인라이닝 하지 않음
  return TransformingSeqeunce(this, transfrom)
}

Flow Control in Higher Order function

non-local return

어떤 함수 내의 일반 루프 안에서 return을 호출하면 루프를 중지하고 값을 반환하며 함수를 종료한다. 인라인 함수 내의 람다에서 return을 호출하면 같은 결과과 나온다. 이 때의 리턴 방식을 non-local Return 이라 한다. 인라인 함수는 람다까지 인라이닝하므로 당연한 결과이다.

fun nonlocal(): String {
    (1..10).forEach {
        if (it == 5) return "stop"
        println(it)
    }
    return "nonlocal"
}

prinln(nonlocal())
// 출력: 1 2 3 4 "stop"

forEach는 인라인 함수이므로 넌로컬 리턴되어 중간에 함수가 중지되는 것을 확인할 수 있다.

local return

인라인 함수가 아니라면 람다의 return에서 컴파일 에러가 발생한다. 이러한 경우 local return을 사용해야한다. 로컬 리턴은 인라인 함수에서도 사용할 수 있다. 넌로컬 리턴과 다르게 로컬 리턴은 람다 내에서 일종의 break처럼 작동한다. 즉 감싸고 있는 외부 함수를 중단시키지 않는다. 로컬 리턴은 return 뒤에 @와 함수이름을 붙여 사용한다. 예를 들어 forEach 내에서 로컬 리턴은 return@forEach 와 같이 작성한다. 또는 람다 블록 앞에 레이블@를 붙이고 명명한 레이블 이름을 함수 이름 대신 사용해도 된다.

fun local(): String {
    (1..10).forEach {
        if (it == 5) return@forEach
        println(it)
    }
    return "local"
}

fun forEach(callback: () -> Unit): String {
    callback()
    return "noInline"
}

println(local())
// 출력 1 2 3 4 5 6 7 8 9 10 "local"

println(forEach {
  for (i in range 1..10) {
    if (i == 5) return@forEach
    println(i)
  }
})
// 출력 1 2 3 4 "noInline"

주의할 점은 로컬 리턴은 외부의 for 문과 관계 없이 람다 내에서 break를 건다는 의미이다. 즉 람다 밖을 반복문으로 감싸고 있어도 람다 실행만 멈출 뿐 밖의 반복문은 영향을 받지 않는다는 것이다.

'Language > Kotlin' 카테고리의 다른 글

[Kotlin] 코루틴의 이해와 사용  (0) 2022.12.14
[Kotlin] 코루틴과 비동기  (0) 2022.12.10
[Kotlin] BigDecimal  (0) 2022.12.02
[Kotlin] Function & Class  (0) 2022.11.17
복사했습니다!