2.1 고차 함수

함수형 프로그래밍에서는 함수도 값처럼 다룰 수 있다.

Int, String, List.. 처럼 함수 역시 변수에 대입하거나, 데이터 구조(Collections)에 저장하거나, 함수의 파라미터나 반환타입으로 사용할 수 있다는 뜻이다. 

고차 함수는 파라미터나 반환타입으로 함수를 사용하는 함수를 말한다. 

예제

이제 고차함수를 작성하는 예제를 보자.

어떤 정수의 대해 절대값과 팩토리얼을 계산해서 "The absolute value of -7 is 7" 이런 문자열을 반환해주는 함수이다. 

처음에는 고차함수를 사용하지 않은 형태를 보곘다. 

object Exam2_2 {
    private fun abs(n: Int) =
        if (n < 0) -n
        else n

    private fun factorial(i: Int): Int {
        fun go(n: Int, acc: Int): Int =
            if (n <= 0) acc
            else go(n - 1, n * acc)
        return go(i, 1)
    }

    fun formatAbs(x: Int): String {
        val msg = "The absolute value of %d is %d"
        return msg.format(x, abs(x))
    }

    fun formatFactorial(x: Int): String {
        val msg = "The factorial of %d is %d"
        return msg.format(x, factorial(x))
    }
}

fun main() {
    println(Exam2_2.formatAbs(-42))
    println(Exam2_2.formatFactorial(7))
}

factorial 함수는 내부 함수를 정의해서 재귀적으로 해결하였다. 
변수를 하나 두고 값을 누적계산할 수 도 있지만, 순수 함수로 만드려면 값을 변이할 수 없다. 

재귀 함수를 통해 factorial을 순수함수로 작성할 수 있다. 

 private fun factorial(i: Int): Int {
    tailrec fun go(n: Int, acc: Int): Int =
        if (n <= 0) acc
        else go(n - 1, n * acc)
    return go(i, 1)
}

factorial 함수는 return에서 함수를 호출하기만 하므로 꼬리 재귀로 콜스택을 줄일 수 있다. 
코틀린에서는 tailrec이라는 키워드를 사용해 꼬리 재귀함수라고 컴파일러에게 알려줄 수 있다.

다시 순수함수 얘기로 돌아가자.

포맷팅된 문자열을 반환해주는 formatAbs와 formatFactorial은 거의 같다. 두 함수를 일반화 해보자.

fun formatResult(name: String, n: Int, f: (Int) -> Int): String {
    val msg = "The %s of %d is %d."
    return msg.format(name, n, f(n))
}

이렇게 f라는 함수를 인자로 받는 함수를 정의할 수 있다. 이제 abs, factorial모두 이 함수를 통해 포맷팅된 문자열을 반환받을 수 있다. 

사용은 함수 인자 부분에 Callable Reference(자바의 메소드 참조)나 람다를 넘겨 사용할 수 있다.

fun main() {
    println(Exam2_2.formatResult("absolute value", -42, Exam2_2::abs))
    println(Exam2_2.formatResult("factorial", 7, Exam2_2::factorial))
}

연습문제 2.1

피보나치 수를 지역적인 꼬리재귀함수를 사용하여 작성하라.

fun fib(i: Int): Int {

    tailrec fun go(beforeValue: Int, currentValue: Int, currentIndex: Int, maxIndex: Int): Int =
        if (currentIndex > maxIndex) beforeValue
        else go(currentValue, beforeValue + currentValue, currentIndex + 1, maxIndex)

    return go(0, 1, 1, i)
}

 

 

2.2 다형적 함수: 타입 추상화

fun formatResult(name: String, n: Int, f: (Int) -> Int): String

앞에서 이런 고차함수를 만들었었다. 

f는 Int값을 받고 Int값을 리턴한다. 오로지 Int 타입에 대해서만 작동하는 함수이다.
이런 함수를 단형적 함수라고 한다. 

특정 타입에 제한된 것이 아닌 어떤 타입에도 작동하는 함수를 다형적 함수라고 한다. 

여기서 얘기하는 다형(polymorphism)은 객체지향의 상속을 통한 다형성이 아닌 Generics(List<T>...)에 가깝다.

예제

단형적 함수를 다형적 함수로 바꾸는 예제를 살펴보자.

fun findFirst(ss: Array<String>, key: String): Int {
    tailrec fun loop(n: Int): Int =
        when {
            n >= ss.size -> -1
            ss[n] == key -> n
            else -> loop(n + 1)
        }
    return loop(0)
}

배열에서 특정 문자열의 index를 찾는 함수이다. 

함수를 보면 배열이 String, Int이던, 내가 정의한 클래스이던 같은 로직을 사용할 수 있을 것 처럼 보인다. 

다형적 함수로 변경해보자.

fun <A> findFirst(xs: Array<A>, p: (A) -> Boolean): Int {
    tailrec fun loop(n: Int): Int =
        when {
            n >= xs.size -> -1
            p(xs[n]) -> n
            else -> loop(n + 1)
        }
    return loop(0)
}

이렇게 타입 파라미터를 사용하여 정의할 수 있다.

findFirst(arrayOf(1,2,3,4,5,6)) {
    it == 5
}
findFirst(arrayOf("A", "B", "C", "D", "E")) {
    it == "A"
}

이런 식으로 사용하면 된다.

연습문제 2.2

isSorted함수를 구현하라. 확장 프로퍼티인 head와 tail을 사용하라.

fun main() {
    val list = listOf(1, 2, 3, 4, 5)
    val list2 = listOf(1, 2, 3, 5, 4)
    val isSorted = isSorted(list2) { a1, a2 ->
        a1 <= a2
    }
    println(isSorted)

}

val <T> List<T>.tail: List<T>
    get() = drop(1)

val <T> List<T>.head: T
    get() = first()

fun <A> isSorted(aa: List<A>, order: (A, A) -> Boolean): Boolean {
    val isOrdered = order(aa.head, aa.tail.head)
    return if (isOrdered.not()) {
        false
    } else if (aa.tail.size <= 1) {
        isOrdered
    } else {
        isSorted(aa.tail, order)
    }
}

 

 

2.3 타입에 맞춰 구현하기

위에서 타입으로 A를 받아서 다형적 함수를 구현하였다. 

다형적 합수 안에서 A 타입 객체에 사용할 수 있는 연산은 A 타입에 대해 작용하는 연산 뿐이다. 
예를 들어 A 타입으로 Int를 받아 나누기 연산을 할 수 있겠지만, String 타입에 연산자 오버라이딩을 하지 않고는 나누기 연산을 할 수 없다. 

그리고 주어진 타입에 따라 구현할 수 있는 코드가 하나로 정해지는 경우도 있다.

partial application 을 수행하는 고차함수를 보자. 

어떤 값과 함수를 인자로 받고, 인자를 하나만 적용해서 결과를 내놓는 함수이다.

fun <A, B, C> partial1(a: A, f: (A, B) -> C): (B) -> C

partial 함수 정의는 이렇게 한다. 

타입 파라미터 A, B, C 세가지를 받고 f는 A, B 타입 인자를 받아 C타입의 결과를 리턴한다. 
그리고 partial1 함수는 (B) -> C타입을 반환한다. 

이 함수는 한 가지 구현만 갖는다. 우선 리턴 타입인 (B) -> C를 리턴하는 함수를 정의하자.

fun <A, B, C> partial1(a: A, f: (A, B) -> C): (B) -> C = { b: B ->
	TODO()
}

이렇게 반환 함수를 만들 수 있다. 

이제 (B) -> C함수에서 C를 반환해야한다. C는 f를 이용해 도출할 수 있다. 

fun <A, B, C> partial1(a: A, f: (A, B) -> C): (B) -> C = { b: B ->
	f(a, b)
}

인자 두개를 받아 부분적으로 적용해 돌려주는 고차 함수를 정의했다. 

의문점

{ b -> C() }를 할 수는 없을까?

Generics 타입의 인스턴스 생성은 할 수 없다. 

이렇게 간단한 한 줄짜리 함수를 작성하였다. 이런 코드가 실제 세계의 큰 코드에서도 적용할 수 있을까?

답은 가능하다. 다형성 고차함수는 특정 도메인을 다루지 않고 다양한 문맥에서 발생할 수 있는 전형적인 패턴만 추상화한다.  

예제 2.3

currying 함수를 작성하라

fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C = { a ->
    { b -> f(a, b) }
}

예제 2.4

curry 변환의 역변환인 uncurry함수를 작성하라

fun <A, B, C> unCurry(f: (A) -> (B) -> C): (A, B) -> C = { a, b ->
    f(a)(b)
}

예제 2.5

두 함수를 합성하는 고차함수를 작성하라.

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C = { a ->
    f(g(a))
}

+ Recent posts