코틀린으로 함수형 프로그래밍 시작하기 #
[고차함수 : 함수를 함수에 넘기기] #
함수형 프로그램을 작성할때 기본이 되는 몇가지 주제
- 함수도 값이다.
- 함수를 변수에 대입하거나 데이터 구조에 저장하거나 함수의 인자로 넘길 수 있다.
고차함수란? 다른 함수를 인자로 받는 함수
고차함수 예제 어떤 수의 절댓값과 다른 수의 계승(팩토리얼; factorial)을 출력하는 프로그램
- 루프를 함수적으로 작성하는 방법
- n의 계승을 계산하는 함수를 추가한다.
- 재귀(recursion)를 통해 순수 함수로 루프를 작성할 수 있다.
fun factorial(i: Int): Int {
fun go(n: Int, acc: Int): Int = // <1>
if (n <= 0) acc // 루프를 종료시키려면 재귀 호출을 하지 않고, 값을 반환한다.
else go(n - 1, n * acc) // 재귀 호출
return go(i, 1) // <2>
}
i=1 go(0, 1x1) i=2 go(1, 2x1) i=3 go(2, 3x2) i=4 go(3, 4x6) i=5 go(4, 5x24)
코틀린은 이런 식의(루프로 표현할 수 있는) 재귀를 수동으로 감지하지는 않지만 함수 앞에 tailrec 변경자를 붙이도록 요구한다. tailrec이 붙은 경우 컴파일러는 재귀 호출이 꼬리 위치(tail position)인 경우에 한해 while 루프로 작성했을 때와 같은 종류의 바이트 코드를 토해낸다. => “꼬리 위치에 있는 재귀 호출을 최적화해 while 루프 형태로 변환한다.”
- 코틀린의 꼬리 호출
fun factorial(i: Int): Int {
tailrec fun go(n: Int, acc: Int): Int = // <1>
if (n <= 0) acc
else go(n - 1, n * acc) // <2>
return go(i, 1)
}
재귀적인 함수 호출을 하는 호출자가 재귀 함수 호출이 반환값을 즉시 호출하기만 하고 다른 아무일도 하지 않을때, 재귀 호출이 꼬리 위치에 있다고 말한다.
- 꼬리 위치에 있다 : go(n-1, n*acc)
- 꼬리 위치에 있지 않다 : 1 + go(n-1, n*acc)
어떤 함수의 모든 재귀 호출이 꼬리 위치에 있고 함수 앞에 tailrec 변경자가 붙은 경우 코틀린 컴파일러는 재귀를 이터레이션시 호출 스택을 소비하지 않는 반복적인 루프로 컴파일한다. => 이는 스택 오버플로우를 방지하고 메모리를 효율적으로 사용하는 데 도움이 된다.
첫번째 고차함수 작성하기
object Example {
private fun abs(n: Int): Int =
if (n < 0) -n
else n
private fun factorial(i: Int): Int { //<1> 계층함수(formatFactorial())를 추가했으므로 private로 변경
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 { //<2> 새로 추가한 함수 (결과 출력)
val msg = "The factorial of %d is %d"
return msg.format(x, factorial(x))
}
}
fun main() {
println(Example.formatAbs(-42))
println(Example.formatFactorial(7)) //<3>
}
두 함수 formatAbs, formatFactorial은 거의 같다. 이 두 함수를 일반화해서 formatResult() 함수를 만들자.
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))
}
fun main() {
println(formatResult("factorial", 7, ::factorial))
println(formatResult("absolute value", -42, ::abs))
}
고차함수
fun formatResult(name: String, n: Int, f: (Int) -> Int): String { ... }
f에 대해서도 타입을 지정한다. f가 정수를 인자로 받고 정수를 반환하는 함수라는 뜻이다.
abs()나 factorial()을 formatResult의 f 인자로 넘길 수 있다.
println(formatResult("factorial", 7, ::factorial))
println(formatResult("absolute value", -42, ::abs))
[다형적 함수 : 타입에 대해 추상화하기] #
다형적(polymorphic) 함수 : 고차 함수를 작성할때 어떤 타입이 주어지든 관계없이 동작하는 코드
예제
- 배열에서 어떤 문자열을 찾는 단형적 함수
fun findFirst(ss: Array<String>, key: String): Int {
tailrec fun loop(n: Int): Int =
when {
n >= ss.size -> -1 // <1>
ss[n] == key -> n // <2>
else -> loop(n + 1) // <3>
}
return loop(0) // <4>
}
- 위 1)번의 단형적 함수를 다형적 함수로 변환
- 술어 함수(predicate function)는 어떤 조건을 평가하여 참(true) 또는 거짓(false) 중 하나를 반환하는 함수
fun <A> findFirst(xs: Array<A>, p: (A) -> Boolean): Int { // <1> 원소 타입이 A인 배열에 작용, 배열의 각 원소에 작용하는 술어 함수를 파라미터로 받음
tailrec fun loop(n: Int): Int =
when {
n >= xs.size -> -1
p(xs[n]) -> n // <2> 술어 함수를 배열 원소에 적용하기
else -> loop(n + 1)
}
return loop(0)
}
타입 변수 A를 사용하는 위치
- 배열의 원소는 모두 A 타입이어야 한다.
xs: Array<A>
- p 함수는 A 타입의 값을 인자로 받는다.
p: (A) -> Boolean
두 위치에서 같은 타입 변수를 참조한다는 사실은 findFirst()의 두 인자에서 해당 타입이 같은 타입이어야만 한다는 사실을 암시한다. 컴파일러는 findFirst()를 호출하는 코드가 이 두 부분에서 같은 타입을 사용하도록 강제한다.
[익명 함수를 사용해 고차함수 호출하기] #
>>> findFirst(arrayOf(7, 9, 13), {i: Int -> i == 9}
{i: Int -> i == 9} 라는 구문을 함수 리터럴이나 익명 함수라고 한다.
[타입에 맞춰 구현하기] #
함수 시그니처에 의해 구현이 하나로 정해지는 예제를 살펴보자.
fun <A, B, C> partial1(a: A, f: (A, B) -> C): (B) -> C = TODO()
- 부분 적용(partial application)을 수행하는 고차함수다.
- partial1() 함수는 어떤 값과 함수(인자를 둘 받아서, 다른 결과를 내놓음)를 인자로 받는다.
- 인자를 하나만 받아서 결과를 내놓는 함수를 반환한다.
- 고차함수
f: (A, B) -> C
- 반환타입 우리가 반환해야하는 대상의 타입이다.
(B) -> C
- 인자 타입이 B인 함수 리터럴을 작성한다. partial1()의 본문 안에서 a값을 자유롭게 사용할 수 있다. 마찬가지로 b도 익명함수의 인자이므로 함수 본문에서 자유롭게 사용할 수 있다.
fun <A, B, C> partial1(a: A, f: (A, B) -> C): (B) -> C =
{ b: B -> TODO() }
- 내부 함수가 C 타입의 값을 반환해야한다. C타입의 값을 어떻게 얻을까? C 타입 값은 f 고차함수의 결괏값이다. 따라서 C 타입 값을 얻는 유일한 방법은 f 함수에 A, B 타입 값을 넘기는 것뿐이다.
fun <A, B, C> partial1(a: A, f: (A, B) -> C): (B) -> C =
{ b: B -> f(a, b) }
- 결과적으로 인자 2개를 받아서 부분적으로 적용해 돌려주는 고차함수가 생긴 것이다. => “A 그리고 A, B를 인자로 받아서 C를 생성해주는 함수가 있다면, A는 이미 인자에 있기 때문에 B만 제공한다면 C를 돌려주는 새로운 함수를 만들 수 있다.”
- 코틀린의 타입 추론 특성으로, 타입 애너테이션 생략이 가능하다.
fun <A, B, C> partial1(a: A, f: (A, B) -> C): (B) -> C =
{ b -> f(a, b) } // 여기서 타입 생략