- Y-Combinator를 사용한 함수 고정점 소개 과 [Kotlin Pearls 8] Recursion, Tail Recursion and Y Combinator | by Uberto Barbini | ProAndroidDev 의 Y Combinator 부분 간단한 요약임
재귀함수
간단한 재귀함수를 짜보자
// 재귀 함수를 짜보자
fun factorial (n: Int): Int {
if (n == 0) return 1
return n * factorial(n - 1)
}
짜잔!
재귀는 자기 자신을 호출하므로, factorial 내부에서 factorial 을 호출하면 된다.
람다로 짜보자
val factorialLambda: (Int) -> Int = { n ->
if (n == 0) 1 else n * factorialLambda(n - 1)
}
에러가 난다.
재귀는 자기 자신을 Call 하는데, 자기 자신이라는 Identifier 가 존재 하지 않는다 (람다이기때문에..)
람다는 이름이 없어서 이름을 붙여주기 위하여 val 에 할당하면 이름이 생기는데 그 이름을 호출하기 위해서는 Body 를 완성시켜야되는데 이름이 없어서 오류가 나고 이 오류를 해결하기 위하여 호출가능한 이름을 붙여줘야 하는데 람다는 이름이 없어서 이름을 붙여주기 위하여 val 에 할당하면....
함수도 파라미터로 받을수있는 FP 의 혁신적 기법덕분에
참조 할 수 있게 파라미터 f 를 받아서 쓰자.
// 컴파일이 불가능한 Labmda factorial 재귀
val factorialLambda: (Int) -> Int = { n ->
if (n == 0) 1 else n * factorialLambda(n - 1)
}
// 참조 가능하게 F 를 받자
val factorialGenerator: ((Int)->Int) -> (Int) -> Int = { f ->
{ n -> if (n <= 1) 1 else n * f(n - 1) }
}
factorialGenerator 로 재귀 람다 함수의 본문은 정의 했다, 이제 컴파일 오류따위는 없다
이제 factorialGenerator 로 factorial 을 만들어 사용하면 된다.
val f1 = factorialGenerator(??); // 결국 여기에 뭐가 들어가야되지?
//아하! 펙토리얼 함수가 들어가야되니까 factorialGenerator 로 만들면되겠네
val f2 = factorialGenerator(factorialGenerator(??)); // 결국 여기에 뭐가 들어가야되지?
//아하! 펙토리얼 함수가 들어가야되니까 factorialGenerator 로 만들면되겠네
val f3 = factorialGenerator(factorialGenerator(factorialGenerator(??)));// 결국 여기에 뭐가 들어가야되지?
//아하! 펙토리얼 함수가 들어가야되니까 factorialGenerator 로 만들면되겠네
val f4 = factorialGenerator(factorialGenerator(factorialGenerator(factorialGenerator(??))));// 결국 여기에 뭐가 들어가야되지?
//아하! 펙토리얼 함수가 들어가야되니까 factorialGenerator 로 만들면되겠네
후략...
이젠 내가 무한루프에 같혀버렸다...
.. 사용할수가 없다... 넘겨줘야 하는 f 를 정의 할 수가 없다..
헬퍼 함수를 만들어서 쓰자..
fun closeLoop (factorialGenerator: ((Int)->Int) -> (Int)->Int): EndoMorph<Int> {
fun result (v: Int): Int {
val completeFn = factorialGenerator(::result)
return completeFn(v)
}
return ::result
}
println(closeLoop(factorialGenerator)(5)) // 120
헬퍼함수의 동작을 살펴보면,
내가 직접 넣어주는것이 아닌, RunTime result 가 들어가게 되고, result 는 다시 factorialGenerator 를 호출하므로,
결국 factorialGenerator 가 더이상 내부적으로 f 를 쓰지 않는상황 (재귀 함수 종료 조건) 이 올때까지 반복하다가 끝나게 된다, (아니면 계속 돌다가 런타임중에 스택오버플로우로 터질것이다)
이렇게 람다에서 무한한 재귀를 정의할 수 있게 하고 사용할수 있게 하는 저런 Helper 함수가 바로 Y 콤비네이터 되시겠다.
Fix Point (고정점)
Y 컴비네이터를 설명하기 전에, Fixed Point 가 무엇인지부터 알아보자
부동소수점/고정소수점할때의 Fixed Point 는 아니고 다른 개념이다.
수학에서 함수의 고정점은 함수의 정의역에 속하는 원소 중에서 함수에 의해 자기 자신으로 매핑되는 원소를 말합니다. 즉, 함수 f 가 있을 때, x = f(x) 를 만족하는 x가 고정점입니다.
그니까
fun myFormula(x: Double): Double = x * 5 - 8
fun square(x: Int): Int = x * x
fun reverse(s: String): String = s.reversed()
println("x is " + 2.0 + " then myFormula is " + myFormula(2.0))
println("x is " + 1 + " then square is " + square(1))
println("s is " + "abba" + " then reverse is " + reverse("abba"))
//x is 2.0 then myFormula is 2.0 : 2 = f(2)
//x is 1 then square is 1 : 1 = f(1)
//s is abba then reverse is abba = abba = f(abba)
myFormula 의 고정점은 2.0
square 의 고정점은 1
reverse 의 고정점은 abba , aaaa, bbbb, caac ... 등등이 되시겠다.
그리고 자기 자신이 나오기 위해서는, 당연히 Parameter 의 Type 과 Return Type 이 같아야한다.
이를 Endomorphisms 이라고 한다 (자기 사상)
// EndoMorph 은 같은 타입을 받아서 같은 타입을 리턴하는 함수
typealias EndoMorph<A> = (A) -> A
FP 에서는 함수 조차 값이기 때문에 다음과 같은 함수도 가능하다
fun takeFunction(x: (Int) -> Int): (Int) -> Int {
return x
}
fun takeEndoMorph(x: EndoMorph<Int>): EndoMorph<Int> {
return x
}
위 함수는 x 를 바로 리턴하므로, 어떤 (int) -> int 함수든 takeFunction 의 fixed Point 라고 할 수 있다
val x = { x: Int -> x + 1 }
val fixedX = takeFunction(x)
println(x == fixedX) // true
// { x: Int -> x + 1 } 라는 람다펑션은, takeFunction 의 FixedPoint
Y 컴비네이터
FixedPoint 를 찾기위한 컴비네이터가 Fixed Point 컴비네이터 이고, 그중 recursive functions 에 대하여 Fixed Point 를 찾는 컴비네이터가 바로 Y 컴비네이터 이다.
일반 함수로 정의 되어있다 손 치면, factorialGenerator 의 고정점은 다음과 같을것이다.
fun factorialFn(n: Int): Int {
return if (n <= 1) 1 else n * factorialFn(n - 1)
}
fun factorialFnGenerator(f: (Int) -> Int): (Int) -> Int {
return ::factorialFn
}
// factorialFnGenerator 의 고정점은 ::factorialFn 자체
findFix 라는 함수가 있다면 람다에서도 다음과 같이 쓸수 있을것이다.
// 불가능한 Labmda factorial 재귀
val factorialLambda: EndoMorph<Int> = { n ->
if (n == 0) 1 else n * factorialLambda(n - 1)
}
// 참조 가능하게 F 를 받자
val factorialGenerator: (EndoMorph<Int>) -> EndoMorph<Int> = { f ->
{ n -> if (n <= 1) 1 else n * f(n - 1) }
}
// findFix 를 찾자, 일반 함수 버전과 같이, 그냥 재귀함수 그 자체가 fixedPoint가 된다
val fixedPoint = findFix(factorialGenerator)
// factorialGenerator 의 Fixed Point 는 { n -> if (n <= 1) 1 else n * f(n - 1) } 이다.
val factorialFromGenerator = factorialGenerator(fixedPoint)
val value1 = factorialFromGenerator(5) // 이제 factorialGenerator 로 factorial 을 만들어서 쓸수있다!
val value2 = fixedPoint(5) // 근데 찾은 고정점 이 재귀함수 자체라 애초에 그냥 이거 써도 됨 ㄷㄷ
일단 Fixed Point 를 찾는 함수를 Kotlin 에 정의해보자
fun <A> findFix (x:(EndoMorph<A>) -> EndoMorph<A>): EndoMorph<A> {
val f = x
return f(findFix(f))
}
x = f(x) = f(f(x)) = f(f(f(x))) ...
fun closeLoop (factorialGenerator: ((Int)->Int) -> (Int)->Int): EndoMorph<Int> {
fun result (v: Int): Int {
val completeFn = factorialGenerator(::result)
return completeFn(v)
}
return ::result
}
println(closeLoop(factorialGenerator)(5)) // 120
위와 사실상 동일 하다..
즉 closeLoop == findFix == Y 컴비네이터 이다.
Lazy
이제 findFix 를 이용하여 고정점을 찾았으니 재귀함수를 람다로 정의할 수 있다!
확인해보자
이유는 우리가 방금 정의한 findFix 함수는 실행이 eager 하다
처음에 만든 closeLoop는 result 가 돌면서 -> result 를 넣고 -> 이게 돌면서 result 를 넣고 -> ... -> 종료조건되면 종료..
인데, findFix는 실행하는순간, input parameter x 가 평가되고 또 그안에서 평가하고 또 그안에서 평가하고 ... 되어, 실행하기도 전에 터져 버리는것.
해결방법은 그냥 lazy 하게 실행시키면 된다..
typealias EndoMorph<A> = (A) -> A
data class LazyFix<A>(val callIt: (LazyFix<A>) -> EndoMorph<A>)
fun <A> findFixLazy(lazyFix: LazyFix<A>): EndoMorph<A> =
lazyFix.callIt(lazyFix)
fun<A> Y(recursiveFun:(EndoMorph<A>) -> EndoMorph<A>): EndoMorph<A> =
findFixLazy(LazyFix { rec -> recursiveFun{ x -> findFixLazy(rec)(x) } })
// 참조 가능하게 F 를 받자
val factorialGenerator: (EndoMorph<Int>) -> EndoMorph<Int> = { f ->
{ n -> if (n <= 1) 1 else n * f(n - 1) }
}
fun main() {
println(Y(factorialGenerator)(5))
println(Y<Int> { f -> { n -> if (n <= 1) 1 else n * f(n - 1) } }(5)) // 와! 람다로 재귀가 된다 혁신적이다.
}
마지막으로 FixedPoint 가 맞는지 확인해보자
fun testFactorial(f: EndoMorph<Int>) {
println(listOf(1, 2, 3, 4, 5, 6, 7).map(f))
}
fun main() {
val factorial = Y(factorialGenerator)
val factorial2 = factorialGenerator(factorial)
val factorial3 = factorialGenerator(factorialGenerator(factorial))
val factorial4 = factorialGenerator(factorialGenerator(factorialGenerator(factorial)))
testFactorial (factorial)
testFactorial (factorial2)
testFactorial (factorial3)
testFactorial (factorial4)
}
//[1, 2, 6, 24, 120, 720, 5040]
//[1, 2, 6, 24, 120, 720, 5040]
//[1, 2, 6, 24, 120, 720, 5040]
//[1, 2, 6, 24, 120, 720, 5040]
고정점이 확실하다.. 재귀만 돌렸을뿐인데!
'프로그래밍 언어 노트 > JAVA | Kotlin' 카테고리의 다른 글
[Ktor] + kts 활용해서 Dynamic Endpoint 만들기 (1) | 2024.07.22 |
---|---|
Kotlin 초기화 코드 제너레이터 (0) | 2024.07.16 |
Hibernate Query Plan 으로 인한 OOM 방지 (0) | 2024.06.24 |
Kotlin ServerResponse 에 대한 DSL 을 구축해보자! (0) | 2023.03.10 |
[Spring Boot] + Kotlin 에서 Route + Beans DSL 을 사용해보자 (0) | 2023.02.25 |