개발 언어/코틀린
인프런 - 코틀린 고급편 (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
- 코틀린 람다식이 외부 변수를 가리키면 Ref 객체로 감싸짐
- +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
을 사용할수 있게 해줌interate
와exec
이 함께 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 }
- 그러나 코틀린만 사용할 경우 SAM interface를 사용할 일이 거의 없음
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