본문 바로가기
코틀린/개발자를 위한 코틀린 프로그래밍

[chapter10] 함수 추가사항 알아보기

by 측면삼각근 2023. 9. 16.
728x90
반응형

들어가기 전에

본 포스팅은 개발자를 위한 코틀린 프로그래밍의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다.

이전 포스팅 ⬇️

[chapter09] 추상 클래스, 인터페이스 알아보기

 

[chapter09] 추상 클래스, 인터페이스 알아보기

들어가기 전에 본 포스팅은 개발자를 위한 코틀린 프로그래밍의 chapter단위로 공부하고, 정리, 부족한 내용의 추가 학습내용을 정리하는 블로깅입니다. 이전 포스팅 ⬇️ [chapter08] 컬렉션 알아보

messycode.tistory.com


01 함수형 프로그래밍이란

1.1 순수함수와 일급 객체 함수

함수는 참조 투명성(지역변수만 사용)을 갖춰야 항상 동일한 입력에 동일한 결과를 반환하는 순수 함수를 만들 수 있지만, 이 방식을 사용하면 다양한 기능을 다 처리할 수 없다. 특별한 경우를 제외하고 순수함수로 작성해야 많은 예외 방지 할 수 있다.

순수함수(pure function) 조건

  • 동일한 인자로 실행하면 항상 동일한 값을 반환한다.
  • 함수 내부에서 반환값 이외의 결과로 부수효과가 발생하지 않는다.

부수효과(side effect)

함수가 실행되는 과정에서 함수 외부의 데이터를 사용 및 수정하거나, 다른 기능을 사용하는 것을 말한다.

  • 함수가 전역변수를 사용하거나 수정
  • 함수가 표준 입출력을 사용해서 키보드 입력과 화면 등에 출력
  • 함수가 파일을 읽고 쓰는 작업 수행
  • 함수를 사용해서 데이터베이스에 연결

순수함수예시

순수함수: 매개변수로 전달된 인자를 가지고 항상 동일한 결과를 반환하는 함수

fun plus(a: Int, b: Int): Int { // 순수함수
    return a + b // 입력되는 인자에 의해 결정된다.
}

fun printPlus(a: Int, b: Int): Unit { // 순수함수가 아니다.
    println(a + b) // 표준 출력을 수행(부수효과 존재) 하기 때문에 순수함수가 아님
}

var a = 0 
fun plus2(b: Int): Int {
    return a + b
}

fun main() {
    println(plus(1, 2)) // 3
    println(plus(1, 2)) // 3
    // 인자가 같으면 항상 같은 값을 반환한다.
    
    println(plus2(1)) // 1
    // 외부 변수에 의존하고 있기 때문에 순수함수가 아니다.
    a = 2
    println(plus2(1)) // 3
    // 함수를 호출할 때마다 결과가 달라진다.
}

일급 객체 함수(first class function)

함수를 일급 객체로 만든 것은 함수도 정수나 문자열처럼 객체로 사용할 수 있는 것을 말한다.

  • 함수를 변수에 할당할 수 있다.
  • 함수를 매개변수의 인자로 전달할 수 있다.
  • 함수를 반환값으로 사용할 수 있다.
  • 함수를 컬렉션 자료구조에 할당할 수 있다.

일급 객체 함수(first class function) 처리

val anonymousFunc = fun(a: Int, b: Int): Int = a + b // 익명함수 (anonymous function)
// (kotlin.Int, kotlin.Int) -> kotlin.Int

val lambdaFunc: (Int, Int) -> Int = { a: Int, b: Int -> a + b } // 람다식
// (kotlin.Int, kotlin.Int) -> kotlin.Int

// 함수를 인자로 전달
fun highFunc(sum: (Int, Int) -> Int, a: Int, b: Int): Int {
    return sum(a, b)
}
// (kotlin.Int, kotlin.Int) -> kotlin.Int

// 함수를 반환
fun returnFunc(): (Int, Int) -> Int {
    return { a: Int, b: Int -> a + b }
}
// 변수에 해당 자료형 할당 (kotlin.Int, kotlin.Int) -> kotlin.Int

일급 함수는 자료구조에 저장이 가능하다.

val map = HashMap<String, (Int, Int) -> Int>()
map["add"] = { a, b -> a + b }
map["sub"] = { a, b -> a - b }
map["mul"] = { a, b -> a * b }
map["div"] = { a, b -> a / b }

val x = '*'
val result = when (x) {
    '+' -> map["add"]?.invoke(1, 2)
    '-' -> map["sub"]?.invoke(1, 2)
    '*' -> map["mul"]?.invoke(1, 2)
    '/' -> map["div"]?.invoke(1, 2)
    else -> null
}

평가 방법

표현식을 코드로 작성하면 로딩할 때 즉시평가 즉 표현식을 계산해 값으로 변형한다. 이러한 방식을 즉시 평가라고 한다.
이와 반대로 사용하는 시점에 평가되는 것을 지연평가라고 한다.

  • 즉시 평가(eager): 함수를 정의하고 호출처리
  • 지연 평가(lazy): 함수를 정의하고 필요할 경우 호출 처리

지연 평가 함수 실행

특정 시점에 평가되는 지연평가를 처리하는 방법은 lazy함수, 시퀀스함수, 부분함수 등이 있다.

val lazyFun by lazy {
    { x: Int -> x }
}
println(lazyFun(1))

val seq = generateSequence(0) { it + 1 } // 시퀀스 정의
println(seq.take(10).toList()) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// 특정 시점에 값을 실행

fun outer(x:Int): (Int) -> Int {
    fun inner(y:Int)  = x + y
    return ::inner
}
val f = outer(10) // f는 inner 함수를 가리키는 함수
// 내부함수 실행
println(f(20)) // 30

1.2 함수와 실행 객체 비교

코틀린은 함수, 람다표현식, 익명함수를 지원하며 모두 실행 연산자를 가지고 실행된다.

보통 함수 자료형은 리터럴 표기법으로 제공하지만, 리플렉션에 있는 함수 인터페이스로도 처리할 수 있다. 함수 자료형의 리터럴 표기법과 함수 인터페이스를 상속해서 실행연산자를 재정의하면 실제 함수 객체가 만들어진다.

함수 인터페이스를 상속해서 실행연산함수 구현

fun funB() = println("funB")

object B : () -> Unit {
    override operator fun invoke(): Unit {
        println("B")
    }
}


class AddFunc : (Int, Int) -> Int {
    override operator fun invoke(p1: Int, p2: Int): Int {
        println("실행연산자 호출")
        return p1 + p2
    }
}

fun main() {
    funB() // funB
    B() // B
    // fun 과 object로 호출한 자료형 모두 함수이다.
    println((::funB) is Function<Unit>) // true
    println(B is Function<Unit>) // true

    val addFun = AddFunc() //객체 생성
    println(addFun(1,2)) // 실행연산자 호출, 3
    println(addFun.javaClass.kotlin) // class AddFunc
    // 함수가 클래스로 인해 객체로 만들어진다. (함수로 일급 객체이다)

1.3 커링함수 알아보기

함수를 반환하는 함수를 커링함수(currying funciton)이라고 한다.

보통 커링함수를 만들 때는 클로저 환경이 구성된다. 또한 메서드도 클래스 내부에 정의된 함수이므로 커링함수를 구성할 수 있다.

커링함수(currying function)

fun add(a: Int, b: Int) = a + b
fun add1(a: Int): (Int) -> Int = { b -> a + b }
fun outer(a: Int): (Int) -> Int {
    fun inner(b: Int): Int = a + b
    return ::inner
}

object Add{
    fun add(a:Int):Function1<Int,Int>{
        return object :Function1<Int,Int>{
            override fun invoke(p1: Int): Int {
                return a+p1
            }
        }
    }
}

Add.add(1)(2)

내부에 누적하는 상태를 보관하는 커링함수 만들기

fun a(n: Int): (d: Int) -> Int { // 부분 함수 정의
    var accumulator = n; // 지역변수로 누적값 관리
    return { d: Int -> accumulator += d; accumulator } // 내부함수 반환: 클로저 발생
}

val a100 = a(100)
println(a100(10)) // 110
println(a100(10)) // 120

확장함수로 커링 함수 처리

함수 자료형은 함수 인터페이스와 동일하게 처리된다.
확장함수는 인터페이스, 추상 클래스, 클래스에 정의할 수 있고, 또한 커링함수도 확장함수로 정의할 수 있다.

fun ((Int, Int) -> Int).partial(x: Int): (Int) -> Int { // 함수 자료형(인터페이스) 부분함수 추가
    return { y: Int -> this(x, y) }
}

val add = { x: Int, y: Int -> x + y }
val p = add.partial(10) // 부분함수 생성

println(p(20)) // 30

// 동일한 함수 자료형의 다른 람다표현식을 사용하여 partial 확장 함수를 계속 사용할 수 있다.
val mul = { x: Int, y: Int -> x * y }
val q = mul.partial(10) // 부분함수 생성
println(q(20)) // 200

fun Function2<Int, Int, Int>.partial2(x: Int): (Int) -> Int {
    var accumulator: Int = 0
    return { y: Int -> accumulator += y; this(x, accumulator) }
}
val p2= add.partial2(100) // 부분함수 실행
println(p2(20)) // 120
println(p2(30)) // 150

1.4 연속 호출하는 체이닝 처리

모든 것은 객체이기에 객체를 반환하면 그 내부 메서드를 연속해서 사용할 수 있다.
하지만 너무 많이 사용하면 실제 코드를 이해하는데 어려움을 겪을 수 있으므로 적절하게 사용하는 것이 좋다.

fun Outer(x: Int): (Int) -> Int {
    fun inner(y: Int): Int = x + y
    return ::inner
}

Outer(1)(2) // 연속 함수 실행

메서드 체인 처리

메서드도 함수이므로 연속해서 호출할 수 있다.
다른 점은 객체에 의해 메서드가 호출되므로 함수를 반환할 때 해당 객체를 반환한다. 또한, 메서드 연속 호출을 중단하는 메서드도 정의해야 하며 마지막으로 연속처리를 중단할 때는 호출해서 더 이상 객체가 반환되지 않아야 한다.

class Car(var ownerName: String, var color: String) {
    fun changeOwner(newOwner: String): Car {
        ownerName = newOwner
        return this // 연속 호출을 위해 객체 반환
    }

    fun repaint(newColor: String): Car {
        color = newColor
        return this // 연속 호출을 위해 객체 반환
    }

    fun info(){
        println("ownerName: $ownerName, color: $color")
    }
}

val car = Car("John", "red")
car.info()
// ownerName: John, color: red
car.changeOwner("Jane").repaint("blue").info()
// ownerName: Jane, color: blue

확장함수 내에서 this라는 리시버 객체를 사용하면, 클래스 내부에서 메서드를 정의하는 것과 동일하게 체인을 구성할 수 있다.

fun Car.changeOwnerAndRepaint(newOwner: String, newColor: String): Car {
    ownerName = newOwner
    color = newColor
    return this
}
728x90

02 고차함수, 합성함수, 재귀함수 알아보기

2.1 고차함수 정의

고차함수(highorder function)는 함수를 객체로 생각해서 인자로 전달되거나 반환값으로 처리되는 함수 패턴을 말한다.

고차함수의 구성

  • 함수의 매개변수에 함수 자료형을 정의하고 함수를 호출할 때 인자로 다른 함수를 받는다.
  • 함수 내부에서 반환값으로 다른 함수를 반환한다.
  • 인자와 반환값으로 함수, 익명함수, 람다표현식으로 처리할 수 있다.
    함수일 경우, 함수 이름이 아닌 함수 참조로 처리해야 한다.
typealias f2 = (Int, Int) -> Int

fun higherOrderFunction(vararg x: Int, f: f2): Int {
    return x.reduce { acc, i -> f(acc, i) }
}

// 합산을 하는 람다 표현식 전달
val answer = higherOrderFunction(1, 2, 3, 4, 5) { acc, i -> acc + i }
// 15

fun add(x: Int, y: Int) = x + y
higherOrderFunction(1, 2, 3, 4, 5, f = ::add) // 함수 참조 전달

// 람다 표현식을 반환하는 함수
fun higOrder(): f2 {
    return { x, y -> x + y }
}
higOrder()(1, 2) // 3

고차함수로 함수 일반화하기

고차함수를 정의해서 사용하는 이유는
행위를 하는 함수연산을 하는 함수를 분리해서 구성할 수 있기 때문이다.

fun agg(nums: IntArray, op: f): Int {
    var result = nums.firstOrNull() ?: return 0
    nums.forEachIndexed { index, i ->
        if (index == 0) return@forEachIndexed
        result = op(result, i)
    }
    return result
}

println(agg(intArrayOf(1, 2, 3, 4, 5)) { a, b -> a + b })

fun sum(nums: IntArray): Int = agg(nums) { a, b -> a + b }
fun max(nums: IntArray): Int = agg(nums) { a, b -> if (a > b) a else b }

2.2 합성함수 정의

두 함수를 하나의 함수로 연결한 것을 합성 함수(composite function)이라고 한다.
함수를 구성하려면 함수의 매개변수와 자료형이 일치해야 하므로 함수를 합칠 때는 주의해야 한다.

typealias F = (Int) -> Int
typealias G = (Int) -> Int
typealias FG = (Int) -> Int
typealias GF = (Int) -> Int

// 첫번째 함수의 확장함수를 지정
infix fun F.compose(g: G): FG = { x -> this(g(x)) }
// 입력받은 함수를 내부에서 실행, 첫번째 함수도 실행

// 첫번째 함수의 인자로 다른 함수의 인자를 받음
infix fun F.then(g: G): GF = { x -> g(this(x)) }
// 입력받은 함수를 내부에서 실행, 입력받은 함수도 실행

val plus2: F = { it + 2 }
val times3: G = { it * 3 }
val plus2times3 = plus2 compose times3
println(plus2times3(3)) // 11 -> (3 + 2) * 3

val times3plus2 = plus2 then times3 // 역방향 함수 결합
println(times3plus2(3)) // 15 -> (3 * 3) + 2

2.3 재귀함수 정의

재귀함수 구성 방식

재귀함수를 구성할 때 주의할 점은 무한 반복을 방지하고 함수가 너무 많이 생기면 함수 스택에 오버플로우가 생겨서 작동이 중단되는 것을 방지하는 것이다.

  • 무한 순환이 발생하지 않도록 함수의 종료 시점을 반드시 작성한다.
  • 함수 실행의 마지막 부분에 자기 자신을 호출하고 호출된 인자의 값을 조정한다.
  • 꼬리 재귀(tail recursion)는 함수가 다른 변수와 함수 호출 결과와의 연산도 함수의 인자로 전달되도록 작성한다.
  • 꼬리 재귀로 처리하면 코틀린은 내부적으로 함수 스택을 추가로 만들지 않는다.

재귀함수 호출과 실행 방식

  • 종료 시점까지 함수를 호출해서 함수 스택을 계속 구성한다.
  • 종료 시점을 만나면 특정 값이 반환되어 함수 스택의 역방향으로 함수가 실행된다.
    마지막까지 모두 실행되면 최종 결과를 반환한다.

재귀함수와 꼬리 재귀처리

꼬리재귀는 추가 연산 없이 스스로 재귀 호출을 하다가 값을 리턴하는 함수를 의미한다.

  • 일반적인 재귀함수는 함수를 메모리 스택에 올려 처리하므로, 성능상 문제가 있다.
    그래서 꼬리 재귀를 처리하면 내부적으로 코드를 변환해서 메모리 스택을 최소화한다.
fun factorial(n: Int): Int {
    if (n == 1) return 1
    return n * factorial(n - 1)
}
val num = 4;
println(factorial(num)) // 24

tailrec fun factorial2(n: Int, acc: Int = 1): Int {
    if (n == 1) return acc // 마지막 로직 처리
    return factorial2(n - 1, acc * n) // 꼬리재귀를 위해 함수에 변경 값을 전달
}
println(factorial2(num)) // 24

 

메모이제이션 처리

재귀함수를 계속 사용하면 성능에 문제가 발생할 수 있다.
그래서 재귀함수를 처리할 때마다 데이터를 메모리에 저장했다가 다시 재귀함수를 호출할 때 기존에 계산된 것이 있으면 조회해서 종료하면 매번 재귀함수를 호출하지 않아도 된다.

그래서 메모이제이션 기법을 재귀함수와 같이 사용하면 성능상 문제 등을 해결할 수 있다.

val map = mutableMapOf<String, Int>()
fun factorial(n: Int): Int {
    if (n == 1) {
        map[n.toString()] = 1
    }
    if (!map.containsKey(n.toString())) {
        map[n.toString()] = n * factorial(n - 1)
    }
    return map[n.toString()]!!
}
println(factorial(5)) // 120
println(map) // {1=1, 2=2, 3=6, 4=24, 5=120}

03 함수의 추가기능 알아보기

3.1 람다 표현식에 수신 객체 반영

수신 객체를 함수 자료형에 붙인 다음 람다 표현식을 받으면 수신 객체를 람다표현식 내에서 사용할 수 있다.
이는 람다 표현식을 메서드처럼 사용할 수 있게 한다.

람다 표현식에 수신 객체 반영

// 함수 자료형에 특정 자료형을 수신 객체로 지정
val op: Int.(Int) -> Int = { other -> this + other }
println(1.op(2)) // 3
op(1, 2) // 3
// 수신 객체의 전달은 실제 함수의 인자가 추가된 것과 같다.

// 두개의 인자를 받는 함수 자료형
val op1: (Int, Int) -> Int = { a, b -> a + b }
println(op1(1, 2)) // 3

// 반환 자료형을 수신 객체 함수 자료형으로 지정
fun add(): Int.(Int) -> Int = { other -> this + other }
val adop = add()
println(1.adop(2)) // 3
println(adop(1, 2)) // 3

매개변수에 리시버 함수 자료형 처리

함수의 매개변수에도 함수 자료형에 리시버 클래스를 붙여서 정의할 수 있다. 이때 람다 표현식을 전달받으면 동일하게 수신객체를 사용이 가능하다.

fun agg(nums: IntArray, op: Int.(Int) -> Int): Int {
    var result = 0
    for (num in nums) {
        result += result.op(num)
    }
    return result
}

println(agg(intArrayOf(1, 2, 3, 4)) { it })
// 1 + 2 + 3 + 4

fun agg2(nums: IntArray, op: (Int, Int) -> Int): Int {
    var result = 0
    for (num in nums) {
        result = op(result, num)
    }
    return result
}
println(agg2(intArrayOf(1, 2, 3, 4)) { a, b -> a + b }) // 람다 표현식으로 전달
println(agg2(intArrayOf(1, 2, 3, 4), Int::plus)) // 플러스 메서드로 전달

3.2 스코프 함수

[코틀린 공식문서] Scope functions

 

[코틀린 공식문서] Scope functions

들어가기 전에 해당 블로깅은 코틀린 공식문서의 Scope functions를 번역하며 학습한 내용입니다. 학습 중임에 따라 이해하는데 도움이 되는 부분들을 추가되고 의역된 부분이 있습니다. 혹시 잘못

messycode.tistory.com

스코프 함수 차이점

스코프 함수는 내부에서 제공하는 스코프를 객체인 this로 사용할 것인지, 아니면 매개변수인 it을 사용할 것인지가 중요하다.
그리고 반환값이 함수 결과인지를 잘 이해해야 한다.

스코프 함수는 코드를 더 간결하게 만들 수 있지만, 지나치게 사용할 경우 가독성이 떨어지고, 오류를 유발할 수 있다.

  • this vs it
    • this: run, with, apply
    • it: let, also
  • 반환값 처리
    • apply: this 반환
    • also: it반환
    • let, run, with: 람다 결과식 반환

스코프 함수

  • let
    • context object: it
    • return: lambda result
    • null이 아닌 값을 포함하는 코드 블록을 사용하는데 자주 사용된다.
    • 제함 범위의 함수를 도입해서 코드 가독성을 높일 수 있다.
val length = str?.let { 
    println("let() called on $it")        
    processNonNullString(it)      // OK: 'it' is not null inside '?.let { }'
    it.length
}
  • with
    • context object: this
    • return: lambda result
    • 확장 함수가 아니다. 
    • 반환된 결과를 사용할 필요가 없는 경우,
      콘텍스트 객체에서 함수를 호출하는 데 사용하는 것이 좋다.
    • 코드에서 이 개체를 활용하여 다음을 수행한다.로 읽음
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)
  • run
    • context object: this
    • return: lambda result
    • run은 확장함수와 일반 함수(비 확장 함수; non-extension funciton)로 호출할 수 있다.
    • 확장함수의 경우 run은 with와 동일한 역할을 수행한다.
val service = MultiportService("https://example.kotlinlang.org", 80)

val result = servce.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// the same code written with let() function:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

비 확장함수의 경우 콘텍스트 객체가 없지만, 여전히 람다 결과를 반환한다.
비확장함수로 사용하면 표현식이 필요한 여러 문의 블록을 사용할 수 있다.

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
    println(match.value)
  • apply
    • context object: this
    • return: 객체 자신
    • 콘텍스트 자체를 반환하므로 값을 반환하지 않고, 주로 수신자 객체의 멤버에 대한 코드를 작성하는 데 사용하는 것이 좋다.
    • 객체 구성시 주로 사용한다.
    • 코드로 "객체에 다음 할당을 적용한다"로 읽을 수 있다.
val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)
  •  also
    • context object: it
    • return: 객체 자신
    • 콘텍스트 개체를 인수로 사용하는 작업을 할 때 유용하다.
    • 해당 속성 함수 및 개체에 대한 참조가 필요한 작업에 사용하거나, 외부 범위에서 참조를 숨기고 싶지 않을 때 사용한다. (외부의 this)
    • 코드로 "그리고 객체로 다음도 수행한다"로 읽을 수 있다.
val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")
    
// The list elements before adding new one: [one, two, three]

3.3 SAM 인터페이스

추상 메서드가 하나만 잇는 인터페이스를 Funcational interface 혹은 SAM(Single Abstract Method)라고 한다.
SAM 인터페이스는 여러 개의 비추상(일반) 멤버가 있을 수 있지만 추상 멤버는 하나만 있을 수 있다. 

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

// SAM 변환을 사용하지 않는 경우
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

// Kotlin의 SAM 변환 활용
// Creating an instance using lambda
val isEven = IntPredicate { it % 2 == 0 }
SAM 변환은 JAVA 인터페이스에서도 사용할 수 있으며, 추상 클래스의 경우 단일 추상 메서드만 있는 경우에도 작동한다.

04 인라인 함수와 인라인 속성 알아보기

https://kotlinlang.org/docs/inline-functions.html

 

Inline functions | Kotlin

 

kotlinlang.org

코차함수나 재귀함수 등을 사용하다 보면 성능상의 문제가 발생한다.
그래서 호출된 곳에 함수를 코드로 삽입해서 문제가 일어나는 것을 방지할 수 있다.

이런 방식으로 처리하는 것을 인라인 함수(inline function)라고 한다. 또한 속성에도 인라인을 붙여서 인라인 속성(inline property)을 사용할 수 있다.

성능상 효율은 좋아지지만, 코드가 증가하게 된다. 그러나 합리적인 방법으로 수행하면(큰 함수는 인라인 처리하지 않음) 루프 내부의 호출에서 성능이 향상된다.

4.1 인라인 함수와 인라인 속성

인라인을 사용하면 이를 호출하는 모든 곳에 서컴파일러가 인라인으로 지정된 함수나 속성을 삽입해서 처리한다.

인라인 고차함수 호출

inline fun compose(a:Int, action:(Int) -> Int, block:(Int) -> (Int)):Int = action(a) + block(a)

fun main() {
    fun callingHigherOrderFunction() {
        val result = compose(1, { it + 1 }, { it * 2 })
        println(result)
    }
    callingHigherOrderFunction()
    compose(1, { it + 1 }, { it * 2 })
}

인라인 속성

최상위 속성과 클래스 속성 등이 많을 경우 게터와 세터가 모두 만들어진다.
인라인 함수처럼 인라인 속성을 사용하면 컴파일 타임에서는 사용하는 곳에 코드가 삽입된다.

인라인 속성은 현재 클래스가 아닌 호출되는 곳에 바이트 코드로 처리되므로, 작성할 때 배킹필드는 사용하지 않는다. (배킹필드를 사용하지 않을 때만 사용 가능)

val foo: Foo
    inline get() = Foo()

var bar: Bar
    get() = ...
    inline set(v) { ... }

// 두 접근자를 모두 인라인 처리
inline var bar: Bar
    get() = ...
    set(v) { ... }

4.2 노인라인 처리하기

인라인으로 호출한 전달된 람다표현식 등을 호출한 곳에 코드를 삽입하지 않으려면 예약어 noinline을 지정해야 한다.

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }

인라인 함수 내에 매개변수로 전달되는 람다표현식으로 코드 삽입을 금지할 수 있다.

4.3 크로스라인 처리하기

인라인 처리된 함수는 호출되는 곳에 코드를 삽입한다.

fun foo() {
    ordinaryFunction {
        return // ERROR: cannot make `foo` return here
    }
}


fun foo() {
    inlined {
    // 람다에 있지만, 바깥쪽 함수를 종료하기때문에 비 지역 반환(non-local function)이라고 함
        return // OK: the lambda is inlined
    }
}

그래서 람다 표현식으로 전달할 때도 return 문을 사용할 수 있다. 이를 non-local return(비지역 반환)이라고 한다.
이것은 실제 코드에 지정될 때 문제가 발생할 수 있다.

inline fun higherOrderFunction(block: ()-> Unit){
    println("Before block")
    block()
    println("After block") // 람다식에서 반환 처리되어 실행되지 않음
}

fun main() {
    fun callingHigherOrderFunction() {
        higherOrderFunction {
            println("In block")
            return // 람다식에서 반환 처리되어 실행되지 않음
        }
    }
    callingHigherOrderFunction()
    // Before block
    // In block
}

이러한 비지역 반환을 방지하기 위해 크로스 인라인 예약어를 매개변수에 표기한다.

inline fun higherOrderFunction(crossinline aLambda: ()-> Unit){
    println("인라인 함수")
    println("Before lambda")
    aLambda()
    println("After lambda")
}


fun main() {
    fun callingHigherOrderFunction() {
        higherOrderFunction {
            println("In lambda")
            // return // 'return' is not allowed here
        }
    }
    callingHigherOrderFunction()
    /*
    인라인 함수
    Before lambda
    In lambda
    After lambda
    */
}

 

반응형