-
kotlin) 복잡한 함수형 크로그래밍kotlin 2024. 7. 20. 02:50
고차 함수와 함수 리터럴
고차함수
- 하나 이상의 함수를 인자로 받거나, 함수를 반환하는 함수
- 고차 함수는 함수형 프로그래밍의 핵심 개념 중 하나로, Kotlin은 이를 언어 차원에서 지원
- 고차 함수를 통해 코드를 더 유연하고 재사용 가능하게 만들 수 있음
//고차함수 X fun add(num1: Int, num2: Int): Int{ return num1 + num2 } //op: (Int,Int) -> Int 를 통해 함수를 인자로 받음 , 고차함수 O fun compute(num1: Int, num2: Int, op: (Int,Int) -> Int): Int{ return op(num1, num2) }
반환 타입에도 함수가 들어갈 수 있다.
//Int,Int 를 받아 Int 함수를 반환하는 함수 fun opGenerator(): (Int, Int) -> Int { }
사용법
fun compute(num1: Int, num2: Int, op: (Int,Int) -> Int): Int{ return op(num1, num2) } //람다식 //덧셈 연산 fun main() { val sumResult = compute(10, 5) { a, b -> a + b } //마지막 파라미터의 람다식()은 바깥으로 뺄 수 있다. println("Sum: $sumResult") // 출력: Sum: 15 } //뺄셈 연산 fun main() { val subtractResult = compute(10, 5) { a, b -> a - b } println("Subtract: $subtractResult") // 출력: Subtract: 5 } //익명 함수 //덧셈 연산 fun main() { val sumResult = compute(10, 5, fun(a: Int, b: Int): Int { return a + b }) //val sumResult = compute(10, 5, fun(a, b) = a + b)// 파라미터 타입까지 축약할 수도 있다. println("Sum: $sumResult") // 출력: Sum: 15 } //뺄셈 연산 fun main() { val subtractResult = compute(10, 5, fun(a: Int, b: Int): Int { return a - b }) println("Subtract: $subtractResult") // 출력: Subtract: 5 }
함수 리터럴(Function Literal)
- 함수 리터럴은 이름이 없는 함수로, 코드에서 직접 정의되어 사용
- Kotlin에서는 두 가지 형태의 함수 리터럴을 갖고있음
- 람다 식(lambda expression) : 람다 식은 간결한 함수 리터럴 표현
- 익명 함수(anonymous function) : 익명 함수는 이름이 없는 함수로, fun 키워드를 사용하여 정의
함숫값(Function Value)
- 함숫값은 변수에 저장하거나, 함수의 인자로 전달하거나, 함수에서 반환할 수 있는 함수
- 함수 자체를 값처럼 취급
- Kotlin에서는 함수 타입을 명시하여 함숫값을 사용할 수 있음
fun add(a: Int, b: Int): Int { return a + b } fun main() { val sumFunction: (Int, Int) -> Int = ::add //sumFunction은 add 함수의 참조를 담고 있는 변수 println(sumFunction(3, 5)) // 출력: 8 add 구문은 add 함수에 대한 참조를 나타냄 }
람다 식과 익명 함수의 차이점
- 구문: 람다 식은 간결한 구문을 제공하며, 익명 함수는 fun 키워드를 사용하여 좀 더 명시적
- 반환 타입: 람다 식은 추론된 반환 타입을 사용하고, 익명 함수는 반환 타입을 명시할 수 있음
- 명확성: 복잡한 로직이 필요한 경우 익명 함수가 더 읽기 쉬울 수 있음
요약
- 함숫값: 함수 자체를 값처럼 취급하여 변수에 저장하거나 전달할 수 있음
- 함수 리터럴: 이름이 없는 함수로, 람다 식과 익명 함수가 있음
- 람다 식
- 간결한 함수 리터럴 표현,
- 람다식 안에는 return 을 쓸 수 없다.
- 익명 함수:
- fun 키워드를 사용하여 정의되는 함수 리터럴
- 익명 함수 안에는 return 을 쓸 수 있다.
- 람다 식
고차함수를 이용한 간단한 계산기
fun compute(num1: Int, num2: Int, op: (Int, Int) -> Int): Int { return op(num1, num2) } fun calculator(num1: Int, num2: Int, operator: String): Int { return when (operator) { "+" -> compute(num1, num2) { a, b -> a + b } "-" -> compute(num1, num2) { a, b -> a - b } "*" -> compute(num1, num2) { a, b -> a * b } "/" -> compute(num1, num2) { a, b -> a / b } else -> throw IllegalArgumentException("Invalid operator") } }
복잡한 함수 타입과 고차 함수의 단점
고차 함수의 타입 살펴보기
//화살표는 반환타입 //(Int,Int,(Int,Int -> (반환타입)Int) -> (반환타입)Int fun compute(num1: Int, num2: Int, op: (Int,Int) -> Int): Int{ return op(num1, num2) } //() -> (Int, Int) -> Int fun opGenerator(): (Int, Int) -> Int { return { a, b -> a + b} } //괄호 앞에 수신객체 타입이 붙는다. //Int.(Long) -> Int fun Int.add(other: Long): Int = this + other.toInt()
함수 리터럴 호출하기
- 람다 식 정의: 람다 식은 이름 없는 함수로, { a: Int, b: Int -> a + b }와 같은 형태로 정의
- 함수 타입: 람다 식은 함수 타입 (Int, Int) -> Int를 가짐
- invoke 메서드: 함수 타입의 객체를 호출할 때 사용할 수 있는 특별한 메서드로, 람다 식이나 함수 타입 객체를 실행
- 직접 호출: 함수 타입 객체는 () 연산자를 사용하여 직접 호출할 수 있으며, 이는 invoke 메서드를 호출하는 것과 동일
fun main(){ //두 개의 Int 값을 받아 그 합을 반환하는 람다 식 val add = { a: Int, b: Int -> a+b} //두개의 차이점은 없다. add.invoke(1,2) //invoke 함수는 함수 타입을 가진 객체에서 호출할 수 있는 특별한 함수 add(1,2) //직접 함수 호출 5.add(3) //확장함수를 호출하는것과 동일하게 사용할 수도 있다. }
Decompile 코드
//고차함수에서 함수를 넘기면, FunctionN 클래스로 변환 compute(2,3,(Function2) null.INSTANCE); public static final int compute(int num1, num2, @NotNull Function2 op){ return ((Number) op.invoke(num1, num2)).intValue(); }
FunctionN
- 함수를 변수처럼 사용할때마다 FunctionN 객체가 만들어진다
- FunctionN 인터페이스는 이러한 함수 타입을 나타내는 기본 인터페이스
- 여기서 N은 함수가 받을 수 있는 매개변수의 수를 나타냄
- 예를 들어, Function2는 두 개의 매개변수를 받는 함수 타입을 나타냄
fun main() { // compute 함수를 호출하여 2와 3을 더한 결과를 result 변수에 저장합니다. // 익명 객체를 사용하여 Function2<Int, Int, Int> 인터페이스를 구현하고, // 두 Int 값을 받아 더한 값을 반환하는 invoke 메서드를 정의합니다. val result = compute(2, 3, object : Function2<Int, Int, Int> { override fun invoke(p1: Int, p2: Int): Int { // 두 매개변수 p1과 p2를 더한 값을 반환합니다. return p1 + p2 } }) // 결과를 출력합니다. 출력값은 5입니다. println(result) // 출력: 5 } // compute 함수는 두 Int 값을 받고 Function2 인터페이스를 구현한 함수 타입을 매개변수로 받습니다. fun compute(num1: Int, num2: Int, op: Function2<Int, Int, Int>): Int { // op.invoke(num1, num2)를 호출하여 num1과 num2를 더한 값을 반환합니다. return op.invoke(num1, num2) }
Closure 와 연동
fun main(){ var num = 5 num += 1 val plusOne: () -> Unit = { num += 1} //밖의 변수 num 의 정보를 미리 포획함 = Closure }
inline 함수 자세히 살펴보기
inline 함수
- 함수를 호출하는 쪽에 함수 본문을 붙여넣게 됨
- 함수 호출을 함수 본문으로 대체하여 호출 오버헤드를 줄이고 성능을 최적화할 수 있도록 하는 함수
- inline 함수를 사용하면 람다 식이나 고차 함수에서 함수 호출 비용을 줄일 수 있음
// inline 키워드를 사용하여 함수 호출 오버헤드를 줄입니다. inline fun compute(num1: Int, num2: Int, op: (Int, Int) -> Int): Int { // op 함수를 호출하여 num1과 num2를 인자로 넘겨 계산 결과를 반환합니다. return op(num1, num2) } fun main() { // inline 함수 compute를 호출합니다. val sumResult = compute(2, 3) { a, b -> a + b } println("Sum: $sumResult") // 출력: Sum: 5 // 다른 연산을 수행하는 람다 식을 전달합니다. val productResult = compute(2, 3) { a, b -> a * b } println("Product: $productResult") // 출력: Product: 6 } //자바의 디컴파일 public static void main(String[] args) { // compute 함수의 본문이 이곳에 직접 인라인 됩니다. // 첫 번째 호출 int sumResult = (2 + 3); System.out.println("Sum: " + sumResult); // 출력: Sum: 5 // 두 번째 호출 int productResult = (2 * 3); System.out.println("Product: " + productResult); // 출력: Product: 6 }
non-lical return
fun main(){ iterate(listOf(1,2,3,4,5)){num-> if(num == 3){ return //inline 의 경우 main() 에 내부 함수를 그대로 가져오기때문에 return 을 사용할 수 있다. //다만 해당 return 의 경우 main() 함수를 종료하는 return 이다. } println(num) } } inline fun iterate(numbers: List<Int>, exec: (Int) -> Unit){ for(num in numbers){ exec(num) } }
SAM과 reference
SAM(Single Absctract Method)
- SAM 변환은 하나의 추상 메서드만을 갖는 인터페이스나 추상 클래스에 대해, 해당 인터페이스나 추상 클래스의 인스턴스를 람다 식으로 표현할 수 있도록 하는 기능
- Java에서는 이러한 인터페이스를 함수형 인터페이스라고 부름.
@FunctionalInterface public interface Runnable{ //추상 메소드이고 하나만 존재 public abstract void run(); } //람다식으로 변환 public class Main { public static void main(String[] args) { // Runnable 인터페이스를 람다식으로 구현 Runnable runnable = () -> System.out.println("Running"); // Thread에 runnable을 전달하여 실행 Thread thread = new Thread(runnable); thread.start(); } }
Reference
- 객체의 메모리 주소를 가리키는 변수
- Reference는 객체의 실제 데이터를 포함하지 않고, 객체가 위치한 메모리 주소만을 보유
강한 참조 (Strong Reference)
- 기본적인 참조 형태로, 객체가 가비지 컬렉션(Garbage Collection)되지 않도록 유지
String str = new String("Hello, World!");
약한 참조 (Weak Reference)
- 객체가 가비지 컬렉션의 대상이 될 수 있도록 허용하는 참조입니다. WeakReference 클래스가 사용
- 약한 참조는 객체의 생명주기를 강하게 연장하지 않으며, 객체가 더 이상 강하게 참조되지 않으면 가비지 컬렉션
import java.lang.ref.WeakReference; String str = new String("Hello, World!"); WeakReference<String> weakRef = new WeakReference<>(str);
소프트 참조 (Soft Reference)- 가비지 컬렉터가 메모리가 부족할 때만 객체를 수거하도록 허용하는 참조입니다. SoftReference 클래스가 사용
- 소프트 참조는 메모리가 충분할 때는 가비지 컬렉션되지 않지만, 메모리가 부족하면 가비지 컬렉션
import java.lang.ref.SoftReference; String str = new String("Hello, World!"); SoftReference<String> softRef = new SoftReference<>(str);
팬텀 참조 (Phantom Reference)
- 객체가 finalize된 후, 가비지 컬렉터에 의해 수거되기 전 액션을 수행할 수 있도록 합니다. PhantomReference 클래스가 사용
- 팬텀 참조는 객체가 실제로 가비지 컬렉션되기 전의 마지막 단계에서 사용
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; String str = new String("Hello, World!"); ReferenceQueue<String> queue = new ReferenceQueue<>(); PhantomReference<String> phantomRef = new PhantomReference<>(str, queue);
함수 참조 (Function Reference)
- 함수 참조는 기존의 함수를 참조하여 이를 변수에 저장하거나 다른 함수에 전달
- 함수 참조는 :: 연산자를 사용하여 표현
- Java 에서는 호출 가능 참조 결과값이 Consumer / Supplier 같은 함수형 인터페이스이지만,
Kotlin 에서는 리플렉션 객체이다.
fun add(a: Int, b: Int): Int { return a + b } fun main() { // 함수 참조를 변수에 저장 val addFunction: (Int, Int) -> Int = ::add // 함수 참조를 사용하여 함수 호출 val result = addFunction(2, 3) println(result) // 출력: 5 }
프로퍼티 참조 (Property Reference)
- 프로퍼티 참조는 특정 프로퍼티(변수)를 참조하여 이를 변수에 저장하거나 다른 함수에 전달할 수 있게 함
- 프로퍼티 참조 역시 :: 연산자를 사용하여 표현
var name: String = "Kotlin" fun main() { // 프로퍼티 참조를 변수에 저장 val nameReference = ::name // 프로퍼티 참조를 사용하여 값 읽기 println(nameReference.get()) // 출력: Kotlin // 프로퍼티 참조를 사용하여 값 설정 nameReference.set("Kotlin Language") println(name) // 출력: Kotlin Language }
'kotlin' 카테고리의 다른 글
kotlin) 어노테이션과 리플렉션 (2) 2024.07.24 kotlin) 제네릭 (0) 2024.07.21 kotlin) Kotlin 의 지연과 위임 (0) 2024.07.18 kotlin) 추가적으로 알아두어야 할 코틀린 특성 (0) 2024.07.13 kotlin) 코틀린에서의 FP (0) 2024.07.09