개발 언어/코틀린

인프런 - 코틀린 고급편 (3) 함수형 프로그래밍 활용

jjiiiinn 2024. 7. 4. 08:53
728x90

13강. 고차 함수와 함수 리터럴

고차함수

  • 파라미터로 함수를 받거나 함수를 반환하는 함수

두 수를 연산하는 함수

  • 파라미터에 함수를 받고있음, 즉 고차함수
  • fun compute(num1: Int, num2: Int, op: (Int, Int) -> Int): Int { return op(num1, num2) }
  • compute를 호출하는 방법
    • 람다식, 익명함수를 함숫값 또는 함수 리터럴이라고 함
      • 리터럴: 소스 코드의 고정된 값을 나타내는 방법
fun main() {
    // 람다식
    compute(5, 3) { a, b -> a + b }

    // 익명함수
    compute(5, 3, fun(a: Int, b: Int) = a + b)
    // 익명함수 + 타입추론
    compute(5, 3, fun(a, b) = a + b)
}

반환 타입에도 함수가 들어갈 수 있음

fun opGenerator(): (Int, Int) -> Int {
    TODO("함수 구현 안됨")
}

용어 정리

  • 함숫값 / 함수 리터럴: 일반 함수와 달리 변수로 간주하거나 파라미터에 넣을 수 있는 함수
  • 람다: (프로그래밍 용어) 이름이 없는 함수
  • 람다식: (코틀린 용어) 함숫값 / 함수 리터럴을 표현하는 방법1
  • 익명함수: (코틀린 용어) 함숫값 / 함수 리터럴을 표현하는 방법2

람다식과 익명 함수의 차이

  • 람다식은 반환타입을 적을 수 없음
  • 익명함수는 반환타임을 적을 수 있음
    // 람다식
    compute(5, 3) { a, b -> a + b }

    // 익명함수
    compute(5, 3, fun(a, b): Int = a + b)
  • 람다식 안에서는 return을 쓸수 없음
  • 익명함수 안에서는 return 사용 가능
    fun iterate(numbers: List<Int>, exec: (Int) -> Unit) {
      for (number in numbers) {
          exec(number)
      }
    }
    

// 익명함수
iterate(listOf(1, 2, 3, 4, 5), fun (num) {
if (num == 3) { return }
println(num)
})

// 람다식
iterate(listOf(1, 2, 3, 4, 5), { num ->
if (num == 3) { return // 사용 불가능 }
println(num)
})


- 함수 타입 파라미터에 default parameter 적용 가능
```kotlin
fun compute1(
    num1: Int,
    num2: Int,
    op: (Int, Int) -> Int = {a, b -> a + b} // default parameter
): Int {
    return op(num1, num2)
}

fun compute2(
    num1: Int,
    num2: Int,
    op: (Int, Int) -> Int = fun (a, b) = a + b // default parameter
): Int {
    return op(num1, num2)
}

함수 파라미터 기본값 응용

  • 아래의 계산기 함수를 파라미터 기본값을 이용해 객체지향적으로 리팩토링
  • fun calculate(num1: Int, num2: Int, oper: Char): Int { return when(oper) { '+' -> num1 + num2 '-' -> num1 - num2 '*' -> num1 * num2 '/' -> { if (num2 == 0) { throw IllegalArgumentException("0으로 나눌수 없습니다.") } else { num1 / num2 } } else -> throw IllegalArgumentException("들어올수 없는 연산자입니다.") } }
  • java에서는 비슷한 구조를 위해 BiFunction 인터페이스를 사용하지만 Kotlin에서는 함수가 1급 시민이므로 함수를 바로 사용 가능함
  • enum class Operator( private val oper: Char, val calcFun: (Int, Int) -> Int ) { PLUS('+', { a, b -> a + b }), MINUS('-', { a, b -> a - b }), MULTIPLY('*', { a, b -> a * b }), DIVIDE('/', { a, b -> if (b == 0) { throw IllegalArgumentException("0으로 나눌수 없습니다.") } a / b }), }

fun calculate(num1: Int, num2: Int, oper: Operator): Int = oper.calcFun(num1, num2)

# 14강. 복잡한 함수 타입과 고차 함수의 단점

## 확장 함수
```kotlin
// Int: 수신객체 타입
// this: 수신 객체
fun Int.add(other: Long): Int = this + other.toInt()

함수 리터럴 호출하기

val add = { a: Int, b: Int -> a + b }

add.invoke(1, 2)
add(1, 2)
  • 확장 함수지만 수신객체를 첫번째 파라미터에 넣음
    val add = fun Int.add(other: Long): Int = this + other.toInt()
    

add.invoke(1, 2L)
add(1, 2L)
1.add(2L)


### Decompile 코드
- 고차함수에서 함수를 넘기면, **FunctionN** 클래스로 변환 됨
```java
compute(2, 3, (Function2)null.Instance)

public static final int compute(int num1, int num2, @NotNull Function2 op) {
    return ((Number)op.invoke(num1, num2).intValue90
}

Closure 사용

  • Closure를 사용하면 좀더 복잡해짐
  • fun main() { var num = 5 num += 1 // Closure val plusOne = { num += 1 } }
  • Decompile 코드
    • 코틀린 람다식이 외부 변수를 가리키면 Ref 객체로 감싸짐
      ```java
      final Ref.IntRef num = new Ref.IntRef();
      num.element = 5
  • +num.element

Function0 plusOne = (Function0)(new Function(){
public Object invoke() {
this.invoke();
return Unit.Instance;
}

public final invoke() {
    ++num.element;
}

})


## 고차함수 결론
- 고차함수를 사용하게되면 **FunctionN**클래스가 만들어지고 인스턴스화 되어 오버헤드가 발생
- 함수에서 변수를 포획할 경우, 해당 변수를 `Ref`라는 객체로 감싸야하기 때문에 오버헤드 발생

---

# 15강. inline 함수 자세히 살펴보기
## inline 함수 (1)
```kotlin
inline fun add(num1: Int, num2: Int): Int {
    return num1 + num2
}

fun main() {
    val num1 = 1
    val num2 = 2
    val result = add(num1, num2)
}
  • Decompile 코드
    • add함수 호출 대신 덧셈 자체가 main함수로 들어옴
public static final void main() {
    int num1 = 1;
    int num2 = 2;
    int var10000 = num1 + num2
}

inline 함수 (2)

inline fun repeat(times: Int, exec: () -> Unit) {
    for (i in 1..times) {
        exec()
    }
}

fun main() {
    repeat(2) { println("Hello World") }
}
  • Decompile 코드
    • repeat 함수가 인라이닝 됨
    • exec함수에 넣었던 println까지 인라이닝 됨
    • 자기 자신 + 파라미터로 받은 함수까지 인라이닝 됨
public static final void main() {
    int i%iv = 1;

    while(true) {
        System.out.println("Hello World")
        if (i%iv == 2) {
            return;
        }

        ++i%iv;
    }
}

단, 모든 경우 다른 함수를 인라이닝 시킬수 있는것은 아님

inline fun repeat(times: Int, exec: () -> Unit) {
    for (i in 1..times) {
        exec()
    }
}

fun main(exec: () -> Unit) {
    // 익명 함수를 파라미터로 받음
    // exec를 현재 시점에서 알수 없기 떄문에 인라이닝 되지 않음
    repeat(2, exec)
}
  • Decompile 코드
  • public static final void main(@NotNull Function0 exec) { int i%iv = 1; while(true) { exec.invoke(); // exec를 알수 없기때문에 인라이닝 되지 않음 if (i%iv == 2) { return; } ++i%iv; } }

또 다른경우 강제로 인라이닝을 막을 수 있음

  • noinline이 붙으면 인라이닝 되지 않음
  • inline fun repeat(time: Int, noinline exec: () -> Unit) { for (i in 1..time) { exec() } }
  • Decompile 코드
  • public static final void repeat(int time, @NotNull Function0 exec) { int $i$f$repeat = false; int i = 1; if (i <= time) { while(true) { exec.invoke(); // inline 되지 않음 if (i == time) { break; } ++i; } } }

non-local return

  • inline함수는 인라이닝에만 관여하지 않음 non-local return을 사용할수 있게 해줌
    • interateexec이 함께 main안으로 들어가니 return을 사용 할 수 있음
    • 단, 코드상의 return은 main함수를 return 함
      fun main() {
      iterate1(listOf(1, 2, 3)) { num ->
        if (num == 2) {
            // inline 함수에서는 `return` 사용가능
            return
        }
      }
      }
      

inline fun iterate1(numbers: List, exec: (Int) -> Unit) {
for (i in numbers) {
exec(i)
}
}


## crossinline
- inline 함수의 함수 파라미터에서 non-local return을 금지 시킴

```kotlin
fun main() {
    iterate1(listOf(1, 2, 3)) { num ->
        if (num == 2) {
            // crossinline 함수에서는 `return` 사용가능
            return
        }
    }
}

inline fun iterate1(numbers: List<Int>, crossinline exec: (Int) -> Unit) {
    for (i in numbers) {
        exec(i)
    }}

정리

  • inline 함수는 본인만 인라이닝 되는것이 아닌 함수 파라미터도 인라이닝 시키고 non-local return 역시 사용가능
  • inline 함수의 함수 파라미터를 인라이닝 하고 싶지 않다면 noinline을 사용
  • inline 함수의 함수 파라미터가 non-local return을 쓸수 없게 하고싶다면 crossinline을 사용하면 됨

inline 프로퍼티

class Person(val name: String) {
    // uppercaseName를 사용하는 곳에 `this.name.uppercase()`가 들어감
    inline val uppercaseName: String
        get() = this.name.uppercase()
}

16강. SAM과 reference

SAM

  • Single Abstract Method
    • 추상 메서드가 하나만 있는것을 의미
@FunctionalInterface
public interface Runnable {
    void run();
}
  • 자바에서는 SAM interface를 람다로 인스턴스화 할수 있음
    • 익명 클래스 또는 람다를 사용해 인스턴스화 가능
public interface StringFilter {
    abstract public boolean predicate(String str)
}

StringFilter filter = s -> s.startsWith("A") // 가능
  • 코틀린에서는 SAM을 람다식으로 인스턴스화 할수 없음
val filter: StringFilter = { s -> s.startsWith("A") } // 불가능
  • SAM 생성자를 사용하면 사용 가능
    • SAM 이름 + 람다식으로 인스턴스화 가능
val filter = StringFilter { s -> s.startsWith("A") } // 가능
  • 만약 변수에 저장하는 것이 아닌, 파라미터에 넣을거라면 바로 람다식을 사용할 수 있음
fun comsumeFilter(filter: StringFilter) {
    //
}

comsumeFilter({ s -> s.startsWith("A") }) // 가능
  • 하지만 이렇게 암시적인 SAM 인스턴스화를 할 경우 의도하지 않은 SAM이 호출될 수 있음
@FunctionInterface
public interface Filter<T> {
    abstract public boolean predicate(T t)
}

@FunctionInterface
public interface StringFilter {
    abstract public boolean predicate(String str)
}
fun <T> consumeFilter(filter: Filter<T>) {}

fun consumeFilter(filter: StringFilter) {}
    // 어떤 consumeFilter가 실행될까?
    consumeFilter({ s -> s.startsWith("A") })
  • consumeFilter 애매모호하면 더 구체적인 StringFilter가 실행됨

코틀린에서 SAM interface 만들기

  • 추상 메소드가 1개인 인터페이스 앞에 fun을 붙이면 됨
    • 그러나 코틀린만 사용할 경우 SAM interface를 사용할 일이 거의 없음
      fun interface KStringFilter {
      fun predicate(str: String): Boolean
      }

Reference

  • 기존에 존재하는 함수를 변수로 할당하기
  • 호출 가능 참조라고 부름
    fun add(a: Int, b: Int) = a + b
    

fun main() {
val add = ::add
}


- 클래스에 대한 참조는 생성자에 대한 호출가능 참조를 얻음
```kotlin
class Person (
    val name: String,
)

fun main() {
    val p = ::Person
}
  • 프로퍼티에 대한 호출가능 참조를 얻을 수도 있음
    class Person (
      val name: String,
    )
    

fun main() {
val p = Person::name.getter
}


- 인스턴화된 클래스 또는 확장함수에도 적용 가능
```kotlin
class Person (
    val name: String,
)

fun main() {
    val person = Person("jin")
    val boundingGetter = person::name.getter // 바인딩된 프로퍼티 호출가능 참조
}
fun Int.addOne(): Int {
    return this + 1
}

fun main() {
    val plus = Int::addOne // 확장 함수의 호출 가능 참조
}

java와 kotlin의 호출가능참조 차이점

  • java에서는 호출가능참조 결과값이 Consumer / Supplier와 같은 함수형 인터페이스
  • kotlin에서는 리플렉션객체임

위 내용은 인프런 코틀린 고급편강의를 시청하고 작성했습니다
인프런

728x90