Strong 감자의 공부

Ch_10 함수 추가 사항 알아보기 본문

문법_Kotlin

Ch_10 함수 추가 사항 알아보기

ugyeong 2023. 9. 13. 13:17

목차-

1. 함수형 프로그래밍이란? 

  • a. 일급 객체 함수(first Class function)
  • b. 지연 평가 함수 실행

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

 

3. 함수의 추가 기능 알아보기

  • a. 스코프 함수
  • b. SAM 인터페이스(Single Abstract Method)

 

 


01 함수형 프로그래밍이란?

a. 일급 객체 함수(first Class function)

▪️ 함수를 변수에 할당할 수 있음

▪️ 함수를 매개변수 인자로 전달할 수 있음

▪️ 함수를 반환값으로 사용할 수 있음

▪️ 함수를 컬렉션 자료구조에 할당할 수 있음

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

fun main() {
    val add1 = fun(x: Int, y: Int): Int = x + y // 익명함수를 변수에 할당
    val add2 = { x: Int, y: Int -> x + y } // 람다표현식을 변수에 할당
    val add3: (Int, Int) -> Int = { x: Int, y: Int -> x + y } // 위 람다식에서 자료형을 추가
    println(add1(10, 20)) // 30 
    println(add2(10, 20)) // 30
    println(add3(10, 20)) // 30

    fun highfunc(sum: (Int, Int) -> Int, a: Int, b: Int): Int = sum(a, b) // 매개변수에 함수 자료형 정의
    println(highfunc({ x: Int, y: Int -> x + y }, 10, 20))  // 30, 람다 표현식을 인자로 전달

    fun refunc(): (Int, Int) -> Int { // 함수 반환자료형의 함수
        return { x: Int, y: Int -> x + y } // 람다표현식으로 반환
    }

    val rf = refunc() // 함숳 호출(객체로)
    println(rf(10, 20)) // 30


}

 

b. 지연 평가 함수 실행

함수 정의 를 필요할 경우 호출 처리 

-최상위 속성을 정의할 때 by로 속성위임에 lazy함수를 사용할 수 있다. 이 속성을 참조할 때 람다표현식이 실행돼 속성의 초깃값이 만들어진다.

-무한 시퀀스도함수인데 이 시퀀스를 만들면 실제 실행이 되지 않고 액션 toList메서드가 실행되어야 무한 시퀀스가 실행되어 take에 전달된 정수만큼만 원소를 가져와 리스트로 반환. 

-외부함수와 내부함수를 정의 후 외부함수를 실행해도 최종적으로 내부 함수가 실행되어야 함수가 종료된다.

fun main() {
    val func by lazy { { x: Int -> x } }
    println(func(100))

    val seq = generateSequence(0) { it + 100 } // 무한 시퀀스 정의
    println(seq.take(5).toList()) // 특정 시점에 값을 실행

    fun outer(x: Int): (Int) -> Int { // 부분함수 정의
        fun inner(y: Int): Int = x + y // 내부함수가 실제 연산 결과 반환
        return ::inner // 함수 참조로 반환
    }

    val out = outer(100)
    println(out(200))
}
// 결과값
100
[0, 100, 200, 300, 400]
300

무한 시퀀스 함수에 대해

더보기

public fun <T : Any> generateSequence(
    seed: T?,
    nextFunction: (T) -> T?
): Sequence<T>
Returns a sequence defined by the starting value seed and the function nextFunction, which is invoked to calculate the next value based on the previous one on each iteration.
The sequence produces values until it encounters first null value. If seed is null, an empty sequence is produced.
The sequence can be iterated multiple times, each time starting with seed.

 

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

fun main() {
    object : () -> Unit { // object표현식 정의
        override fun invoke(): Unit { // 함수 인터페이스에 실행연산자가 없어서 구현
            println("함수 객체가 만들어지고 실행")
        }
    }

    val f = AddFunc() // 객체 생성
    println(f(10, 20))

    fun func(): (Int) -> Int {
        return { x: Int ->
            println("함수 표현식 전달");
            x
        }
    }

    val c = object : () -> ((Int) -> Int) {
        override fun invoke(): (Int) -> Int {
            println("함수 반환")
            return { y: Int -> y }
        }
    }
    println(func().invoke(100))
    val funC=c()
    println(funC.invoke(200))
}

class AddFunc : (Int, Int) -> Int {
    override fun invoke(p1: Int, p2: Int): Int {
        println("클래스 실행연산자 호출")
        return p1 + p2
    }
}
// 결과값
클래스 실행연산자 호출
30
함수 표현식 전달
100
함수 반환
200

 

d. 커링함수 알아보기

하나의 함수에서 매개변수를 분리해 외부함수와 내부함수로 저장해서 처리가능. 이처럼 함수를 부분으로 나눠 처리하는 것을 커링 함수라고 한다.

fun main() {
    fun a(n: Int): (d: Int) -> Int {
        var acc = n
        return { x ->
            acc += x;
            acc // Int값 반환
        }
    }

    val a100 = a(100)
    println(a100(5)) // 105
}

 

e. 연속 호출하는 체이닝 처리

함수나 메서드가 연속으로 호출돼서 처리하는 것을 메서드 체이닝이라고 한다. 객체를 반환하면 그 내부 메서드를 연속에서 실행할 수 있다. 다만 너무 많이 사용하면 실제 코드를 이해하는데 너무 어려울수 있어 적당히 사용해야한다.

 

-함수 연속 호출

fun main() {
    fun Outer(x: Int): (Int) -> Int {
        fun inner(y: Int): Int {
            return x + y
        }
        return ::inner
    }
    println(Outer(100)(200)) // 300, 함수 연속 실행
    // 계층 처리
    val out = Outer(100)
    println(out(200)) // 300 

    val lamda = { x: Int -> { y: Int -> { z: Int -> x + y + z } } }
    println(lamda(100)(200)(300)) // 600, 함수 연속 실행
}

 

- 메서드 체인 처리

fun main() {

    val c = Car("감자", "레드")
    c.changeOwner("돌자반").repair("파랑").info() // 소유자=돌자반, 색상 =파랑
}
class Car(var ownerName: String, var color: String) {
    fun changeOwner(newName: String): Car {
        this.ownerName = newName
        return this
    }

    fun repair(newColor: String): Car {
        this.color = newColor
        return this
    }

    fun info(): Unit =
        println("소유자=$ownerName, 색상 =$color")
}

 

02 고차함수, 확장함수, 재귀함수 알아보기

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

typealias f = (Int, Int) -> Int

fun main() {
    fun one(vararg x: Int, op: f): Int {
        return x.toList().reduce(op)
    }
    println(one(1, 2, 3, 4, op = { x: Int, y: Int -> x + y })) // 10, 함수를 매개변수로 받는 고차함수

    fun add(x: Int, y: Int): Int = x + y
    println(one(1, 2, 3, 4, 5, op = ::add)) // 15

    fun two(): f {
        return { x, y -> x + y }
    }
    println(two()(10, 20)) // 30
}

 

b. 합성함수의 정의

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

- 두 함수의 매개변수 개수와 반환자료형이 같아야한다.

-두 함수를 결합한 함수도 두 함수의 매개변수와 자료형이 같아야한다.

fun main() {
    fun composeF(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int {
        return { p1: Int -> f(g(p1)) }
    }

    val f = { x: Int -> x + 2 }
    val g = { y: Int -> y + 3 }
    val composeFunc = composeF(f, g)

    println(f(g(3))) //8
    println(composeFunc(3)) //8, 3은 p1값
}

 

c. 재귀함수와 꼬리재귀 처리

재귀함수: 재귀할 때마다 함수가 메모리 스택에 올라간다. 성능상의 문제가 있다.

꼬리 재귀: tailrec 예약어를 붙인다. 내부적으로 코드를 변환해서 메모리 스택을 최소화한다.

차이점: 누적값을 매개변수로 지정해서 재귀함수와 다른 연산을 처리하지 않고 항상 함수 호출만 발생하는 것

 

fun main() {
    fun factorial(n: Int): Long {
        if (n == 1) {
            return n.toLong()
        } else {
            return n * factorial(n - 1)
        }
    }

    var result = factorial(4)
    println(result) // 24

    // 꼬리 재귀
    tailrec fun tailFactorial(n: Int, total: Int = 1): Long {
        if (n == 1) {
            return total.toLong()
        } else {
            return tailFactorial(n - 1, n * total)
        }
    }
    result = tailFactorial(4)
    println("꼬리 재귀 연산 : $result") // 24

}

 

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

a. 스코프 함수

출처 : https://medium.com/@limgyumin/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9D%98-apply-with-let-also-run-%EC%9D%80-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80-4a517292df29

1. apply

public inline fun <T> T.apply(
    block: T.() -> Unit
): T

apply는 모든 명령이 수행되고 나면 명령들이 적용되어 새로 생성된 인스턴스를 반환한다.

2. also

public inline fun <T> T.also(
    block: (T) -> Unit
): T

3. run

public inline fun <T, R> T.run(
    block: T.() -> R
): R

import java.awt.print.Book

fun main() {
    var a = Book("해로의 모험", 10000).apply {
        name = "[세일중]" + name
        discount()
    }
    var bookCost = a.run {
        println("상품명: ${name}, 가격: ${price}")
        price + 2000
    }
    println("원가는 $bookCost 입니다.") // 이 값이 반환
}

class Book(var name: String, var price: Int) {
    fun discount() {
        price -= 2000
    }
}
// 결과값
상품명: [세일중]해로의 모험, 가격: 8000
원가는 10000 입니다.

apply와의 차이점 apply는 객체를 반환하지만 run은 스코프내 실행결과값이다.

 

4. let

public inline fun <T, R> T.let(
    block: (T) -> R
): R

5. with

public inline fun <T, R> with(
    receiver: T,
    block: T.() -> R
): R

b. SAM 인터페이스(Single Abstract Method)

하나의 추상메서드만 있는 인터페이스를 보다 단순하게 처리하기 위해 함수를 정의하는 fun 예약어를 인터페이스 앞에 붙여 SAM인터페이스를 만든다.

장점: 상속하고 구현해서 처리할 수도 있고, 상속없이 재정의 해 직접 람다식으로 받아서 처리가능 

fun main() {
    val str = object : StringAble { // 인터페이스처럼 상속해서 재정의 가능
        override fun accept(s: String) {
            println(s)
        }
    }
    str.accept("object 표현식으로 익명 객체 처리")
    val consume = StringAble { s -> println(s) } // 람다표현식으로 할당
    consume.accept("바로 람다 표현식을 전달해서 재정의")
    consume.hello()
}

fun interface StringAble { // SAM인터페이스는 앞에 fun 붙임
    fun accept(s: String) // 추상메서드 한개
    fun hello() = println("일반메소드") // 일반메서드
}

//결과값
object 표현식으로 익명 객체 처리
바로 람다 표현식을 전달해서 재정의
일반메소드

 

참고) 스코프 함수- https://haero.tistory.com/21