뜌릅
8장 애플리케이티브 펑터 본문
애플리케이티브 펑터는 펑터의 확장버전이라고 할 수 있다.
애플리케이티브 펑터란?
애플리케이티브 펑터는 펑터가 가진 한계 때문에 등장하였다. 따라서 펑터가 가진 한계부터 살펴보는게 도움이 된다.
val product:(Int,Int) -> Int = {x:Int, y:Int -> x*y}
Just(10).fmap(product)
매개변수가 한개 이상인 변환 함수를 fmap함수에 적용하면 함수를 가진 펑터가 된다는 것을 알았다. 그리고 currying을 활용하면 이전장의 maybeProductTen.fmap { it( 5 ) }의 it(5)와 같이 함수를 매개변수로 받는 함수를 변환 함수로 사용할 수도 있다는 것도 알 수 있었다. ( it(5)을 풀어 쓰면 f: (Int) -> Int -> f(5)와 동일하다.)
그런데 it함수에 Just(5)를 적용하고 싶다면 어떻게 해야 할까? 펑터는 일반적인 함수(transform: (A) -> B)로만 매핑이 가능하기 때문에, 펑터를 입력으로 넣을 수 없다. 따라서
maybeProductTen.fmap { it( Just(5) ) }처럼 코드를 작성하게 되면 컴파일 오류가 발생하게 됩니다.
이를 적용하려면 상당히 복잡한 과정을 거쳐야 하는데, 이를 극복하기 위해서 필요한 것이 애플리케이티브 펑터이다.
Just(5)와 Just( { x -> x *2} )인 펑터가 있을 때 함수 { x -> x *2}만 꺼내서 Just(5)에 매핑하고 싶다면 어떻게 해야 할까?
일반적인 펑터를 사용한다면 Just(5).fmap(Just( { x -> x *2} ))는 컴파일 오류가 발생한다. 애플리케이티브 펑터는 먼저 Just( { x -> x * 2} )에서 { x -> x *2}를 꺼내고 5를 꺼낸뒤. 함수에 5를 적용시킨다. 그 결과인 10을 Just에 넣어서 Just(10)을 반환한다. 상자에 비유하자면 애플리케이티브 펑터는 첫 번째 상자에 담겨 있는 함수와 두 번째 상자에 담겨있는 값을 꺼내서 매핑하고, 다시 상자 안에 넣어서 반환한다.
어플리케이티브 펑터 타입 클래스는 다음과 같다.
interface Applicative<out A> : Functor<A> {
fun <V> pure(value: V): Applicative<V>
infix fun <B> apply(ff: Applicative<(A) -> B>): Applicative<B>
}
애플리케이티브에는 pure함수와 apply 함수가 있다. 기본적으로 애플리케이티브는 펑터의 확장판이다. 따라서 Functor를 상속하고 있고, fmap 함수를 사용할 수 있다.
pure 함수는 임의의 타입값을 입력으로 받아서 애플리케이티브 안에 그대로 넣고 반환한다. 이때 반환된 애플리케이티브 펑터는 최소한의 컨텍스트( 입력된 값만 포함된 상태의 애플리케이티브 펑터)이다. pure함수는 어떤 값을 받아서 가공 없이 그대로 상자에 포장하는 것으로 비유할 수 있다. pure함수는 애플리케이티브 인스턴스와 관련이 없다. 따라서 애플리케이티브가 갖는 타입인 A와는 다른 V로 선언한 이유가 그것이다. pure는 새로운 애플리케이티브 인스턴스를 생성하기 때문에 타입 클래스(Interface)와는 별도의 함수로 선언해도 된다.
apply 함수는 fmap 함수의 타입, fun <B> fmap(f: (A) -> B): Functor<B>와 비교하면 차이점과 기능을 쉽게 알 수 있다.
fmap 함수는 일반저인 함수를 받아서 펑터 안의 값을 적용하고 다시 펑터에 넣어서 반환한다. apply함수는 함수를 가진 애플리케이티브를 입력으로 받아서 펑터 안의 값을 함수에 적용하고, 적용한 결괏값을 애플리케이티브에 넣어서 반환한다.
(infix 키워드는 함수의 두 매개변수 P1, P2를 같이 호출할 수 있도록 해 준다. apply함수의 첫번째 매개변수는 this가 된다. )
메이비 애플리케이티브 펑터 만들기.
이전장에서 만든 Maybe 펑터도 적용해 봅시다.
sealed class AMaybe<out A>: Applicative<A> {
companion object {
fun <V> pure(value: V): Applicative<V> = AJust(0).pure(value)
}
override fun <V> pure(value: V): Applicative<V> = AJust(value)
abstract override fun <B> apply(ff: Applicative<(A) -> B>): AMaybe<B>
}
기본적인 기능만 정의하였다. 여기서 pure함수는 앞에서 언급한 것처럼 실제로 생성된 인스턴스와는 별개로 새로운 인스턴스를 생성하는 함수이다. 오버라이드한 pure함수는 입수는 입력받은 값value를 값 생성자인 AJust에 넣어서 반환한다. companion object의 pure 함수를 호출하면 내부적으로 오버라이드된 pure함수를 재호출해서 동작한다.
다음 코드는 Just의 값 생성자이다.
data class AJust<out A> (val value: A) : AMaybe<A>(){
override fun toString(): String = "AJust($value)"
override fun <B> apply(ff: Applicative<(A) -> B>): AMaybe<B> = when(ff){
is AJust -> fmap(ff.value)
else -> ANothing
}
override fun <B> fmap(f: (A) -> B): AMaybe<B> = AJust(f(value))
}
AJust 값을 포함한 상태다. 따라서 value를 프로퍼티로 가진다. AMaybe는 기본적으로 펑터이기 때문에 fmap 함수도 구현해야 한다. 구현 내용은 이전장에서 작성한 메이비 펑터와 완전히 동일하다.
입력받은 ff가 AJust인 경우에는 ff.value로 함수를 꺼내서 fmap함수의 변환함수를 사용한다. 결과적으로 fmap 함수에서는 value를 ff.value 함수에 적용하고, 다시 AJust로 감싼 결과를 반환할 것이다. 만약 fmap 함수를 사용하지 않고, apply를 구현한다면 AJust(f(ff.value))와 같이 작성할 수 있다. (아래 fmap을 그냥 그대로 가져다 쓴것.) ff가 ANothing이면 그대로 반환하면 된다.
object ANothing: AMaybe<kotlin.Nothing>(){
override fun toString(): String = "ANothing"
override fun <B> fmap(f: (kotlin.Nothing) -> B): AMaybe<B> = ANothing
override fun <B> apply(ff: Applicative<(kotlin.Nothing) -> B>): AMaybe<B> = ANothing
}
위의 ANothing 코드는 어떤 값이 들어오든 ANothing을 반환하게 fmap과 동일하게 apply을 작성하면 된다. 따라서 하나라도 실패하면 fmap이나 apply로 연결되는 컨텍스트는 모두 ANothing이 될 것이다.
다음은 사용 예시이다. + 어플리케이티브 스타일
println(AJust(10).fmap { it + 10 }) // AJust(20)
println(ANothing.fmap { x: Int -> x + 10 })// ANothing
println(AMaybe.pure(10))//AJust(10)
println(AJust(10) apply AJust({ x: Int -> x * 2})) // AJust(20)
println(ANothing apply AJust({ x: Int -> x * 2})) // ANothing
println(AMaybe.pure(10)
apply AJust({x : Int -> x * 2})
apply AJust({x : Int -> x + 10})) //AJust(30)
1. ANothing은 이후 어떤 펑터를 체이닝하더라도 ANothing이 나옴.
2. 어플리케이티브 펑터를 사용하면 apply 체이닝이 가능함. 펑터안에서 값을 꺼내는 번거로움이 필요없음.
만약 체이닝을 반대로 진행할 경우 :
println(AMaybe.pure({x: Int -> x * 2})
apply AJust(5)
apply AJust(10)
)// 컴파일 오류
컴파일 오류가 발생함.
왜냐하면 pure로 만든건 AMaybe<Int -> Int>인데 정의된 Applicative 타입 클래스는 타입 매개변수가 한개인 것만 허용하기 때문이다. 즉 pure의 반환값은 apply함수를 갖고 있지 않은 것이다.
애플리케이티브 스타일 프로그래밍은 컨텍스트을 유지한 상태에서 함수에 의한 연속적인 데이터 변환을 체이닝하는 방식을 말한다. 이러한 방식의 프로그래밍은 함수를 가진 어플리케이티브에서 시작해서 연속적으로 데이터를 적용하는 것이 일반적이다. 따라서 현재만든 메이비 어플리케이티브 펑터로는 어플리케이티브 스타일로 작성할 수 없다.
앞에서와 같은 방식으로 this가 함수를 포함한 펑터인 Apllicative로 정의하려면 아래와 같이 정의하면 된다.
interface Applicative<A, B> : Functor<(A) -> B>{
//생략
}
interface Applicative<out A> : Functor<A> {
//기존의 것
}
그러나 이렇게 선언하게 될시에 또 제네릭타입((A) -> B)에 공변을 적용 할 수 없으므로, 체이닝이 가능한 pure와 apply 함수를 선언할 수 없다. 따라서 상속을 활용해서 원하는 결과를 만들기는 어렵다.
8.2절에서 애플리케이티브 펑터는 펑터의 확장이라고 했다.
지금까지는 상속을 통해서 확장을 시도하였으나, 이번에는 확장 함수를 통하여 확장을 시도한다. 7장에서 만든 메이비 펑터에 pure와 apply 확장 함수를 추가해서 애플리케이티브 펑터를 만들어 보자.
sealed class Maybe<out A> : Functor<A>{
abstract override fun toString(): String
abstract override fun <B> fmap(f: (A) -> B): Maybe<B>
companion object
}
fun <A> Maybe.Companion.pure(value: A) = Just(value)
infix fun <A,B> Maybe<(A) -> B>.apply(f: Maybe<A>):Maybe<B> = when(this){
is Just -> f.fmap(value)
is Nothing -> Nothing
}
7장에서 작성한 메이비 펑터에 pure 함수를 전역으로 확장하기 위해 기존의 메이비 타입 클래스에 companion object 선언을 추가하였다. => ex: Maybe.pure(10)이 가능해짐. pure함수의 내용은 동일함.
apply 함수는 this가 (A) -> B를 포함한 Maybe라는 점에서 다르다. f가 가진 값 A를 this가 가진 함수 (A) -> B에 적용하고, 결괏값 B를 다시 Maybe에 넣어서 반환한다. 이 동작을 그대로 코드로 옮기면 f.fmap(value)가 된다.
println(Just(10).fmap { it + 10 }) //Just(20)
println(Nothing.fmap { it: Int -> it + 10 }) //Nothing
println(Maybe.pure(10)) // Just(10)
println(Maybe.pure({ x: Int -> x * 2 })) // Just((kotlin.Int) -> kotlin.Int)
println(Maybe.pure({x: Int -> x * 2}) apply Just(10))// Just(20)
println(Maybe.pure({x: Int -> x * 2}) apply Nothing)// Nothing
fmap, pure, apply 함수가 각각 의도한 대로 동작하는 것을 확인할 수 있다. 특히 apply함수는 처음에 작성한 것과 달리 함수를 포함한 Maybe에서 데이터를 적용할 수 있다.
하지만 입력 매개변수가 2개 이상인 함수를 pure 함수의 입력으로 사용하게 되면 컴파일 오류가 발생하게 된다.
apply함수는 (A) -> B를 포함한 Maybe의 확장함수이므로 매개변수가 2개인 함수에 대한 apply 함수는 없기 때문에 컴파일 오류가 발생한 것이다.
이를 해결하기 위해서 커링과 부분적용 함수를 활용한다.
fun <P1, P2, R> ((P1, P2) -> R).curried(): (P1) -> (P2) -> R = { p1: P1 ->
{ p2: P2 -> this(p1, p2) }
}
fun <P1, P2, P3, R> ((P1, P2, P3) -> R).curried(): (P1) -> (P2) -> (P3) -> R = { p1: P1 ->
{ p2: P2 -> { p3: P3 -> this(p1, p2, p3) } }
}
curried 함수는 매개변수가 여러 개인 함수를 매개변수가 한개인 함수의 체인으로 바꿔준다.
println(
Maybe.pure({ x: Int, y: Int -> x * y }.curried())
apply Just(10)
apply Just(20)
)// Just(200)
println(
Maybe.pure({ x: Int, y: Int, z: Int -> x * y + z }.curried())
apply Just(10)
apply Just(20)
apply Just(30)
)//Just(230)
트리 애플리케이티브 펑터 만들기
7장에서 이진트리를 만들어 보았으나, 애플리케이티브 펑터로 만든 트리는 이진트리가 될수 없다고 한다. 그 이유는 곱을 하게 되면 이진트리가 아니게 되기 때문인데 책의 그림 8-2(206p)에 나와있다.
7장과 달리 일반트리이며 이에 적합한 자료구조는 리스트이다.
sealed class Tree<out A> : Functor<A> {
abstract override fun <B> fmap(f: (A) -> B): Functor<B>
companion object
}
data class Node<out A>(val value: A, val forest: List<Node<A>> = emptyList()): Tree<A>(){
override fun toString(): String = "$value $forest"
override fun <B> fmap(f: (A) -> B): Node<B> = Node(f(value), forest.map {
it.fmap(f)
})}
먼저 펑터 타입 클래스의 인스턴스로 선언하고 이후 pure함수를 위해 companion object를 선언한다.
노드는 속성 값으로, 현재 트리가 가진 값 value와 하위 노드의 List forest를 가진다. 하위노드의 리스트가 비어 있으면 마지막 노드이므로 별도로 EmptyTree를 정의할 필요는 없다.
fun main(args: Array<String>) {
val tree = Node(1, listOf(Node(2), Node(3)))
println(tree.fmap { it * 2 }) // "2 [4 [], 6[]]
}
이제 일반적인 트리의 펑터를 애플리케이티브 펑터로 확장해 보자. 메이비 애플리케이티브 펑터에서 pure, apply 확장 함수를 구현하며 된다.
fun <A> Tree.Companion.pure(value: A) = Node(value)
infix fun <A, B> Node<(A) -> B>.apply(node: Node<A>): Node<B> = Node(
value(node.value),
node.forest.map {it.fmap(value)} + forest.map {it.apply(node)}
)
pure은 입력받은 값 그대로 트리에 넣어주면 된다. apply함수는 재귀구조를 이용하여 적용한다. +을 통해서 apply을 한번 더 해주는 이유는 fmap까지만 적용을 하게 되면 하위 노드들이 가진 함수들은 적용되지 않기 때문이다.
다음은 실행 코드이다.
val tree = Node(1, listOf(Node(2), Node(3)))
println(tree.fmap { it * 2 }) //2 [4 [], 6 []]
println(Tree.pure({x: Int -> x * 2}) apply tree) //2 [4 [], 6 []]
일반 펑터의 fmap 함수는 일반적인 값만 입력으로 받을 수 있지만, 애플리케이티브 펑터의 apply 함수는 트리 컨텍스트를 입력받아서 함수에 적용 할 수 있다.
println(Tree.pure({ x: Int, y: Int -> x * y}).curried()
apply Node(1, listOf(Node(2), Node(3)))
apply Node(4, listOf(Node(5), Node(6)))
) //4 [5 [], 6 [], 8 [10 [], 12 []], 12 [15 [], 18 []]]
아까 보았던 그림 8-2(206p) 예제를 적용한 것이다.
적용 방식은 대충(그림 8-3,4,5,6 210p) :
함수가 커링되어 부분 적용되면, 첫 번째 매개변수만 적용된 함수를 가진 트리가 형성됨. (1은 1 * 가되고 2는 2 * 되고 3은 3 *가 됨.)
그 이후 최상위노드(1 * )부터 입력 트리의 value값과 곱해진 value값을 받아서 트리를 완성시킨다ㅏ. 그 뒤에 원본 트리의 하위 노드인 2 *와 3 *에도 똑같이 입력 트리를 적용 시킨다.
이더 애플리케이티브 펑터 만들기.
이번 절에서는 7장에서 작성한 이더 펑터도 애플리케이티브 펑터로 적용시킨다.
이더는 성공과 실패를 모두 포함하는 컨텍스트다. 성공한 경우는 라이트가 되고, 실패한 경우는 레프트가 된다.
두 상태는 각각 다른 타입을 포함할 수 있기 때문에 이더의 타입 매개변수는 두개가 된다.
즉 2개의 타입 매개변수중 하나는 고정 나머지 하나를 적용하는 것.(성공 실패에 따라서 결정)
sealed class Either<out L, out R> : Functor<R>{//7장 코드임
abstract override fun <R2> fmap(f : (R) -> R2): Either<L, R2>
companion object
}
data class Left <out L>(val value: L): Either<L, kotlin.Nothing>() {
override fun toString(): String = "Left($value)"
override fun <R2> fmap(f: (kotlin.Nothing) -> R2): Either<L, R2> = this
}
data class Right <out R>(val value: R): Either<kotlin.Nothing, R>(){
override fun toString(): String = "Right($value)"
override fun <R2> fmap(f: (R) -> R2): Either<kotlin.Nothing, R2> = Right(f(value))
}
이제 여기에 pure와 apply 함수를 추가해 보자.
fun <A> Either.Companion.pure(value: A) = Right(value
infix fun <L,A,B> Either<L, (A) -> B>.apply(f: Either<L, A>): Either<L,B> =
when(this){
is Left -> this
is Right -> f.fmap(value)
}
pure은 동일하다.
apply의 경우 원본 Either은 Left값의 타입 L과 RIght값의 타입 (A) -> B를 가지고 있다. 애플리케이티브 펑터의 정의에 따라서 apply 함수는 입력 Either<L, A>의 A를 꺼내서 원본 Either의 함수 (A) -> B에 적용하고, 다시 Either에 넣어서 Either<L, B>를 반환한다. L은 Left값이므로 건들이지 않는다.
내부 구현 로직에서 fmap 함수와 유사하게 Left인 경우는 그대로 this를 반환하고, RIght인 경우는 입력 Either의 fmap 함수에 원본 Either의 값((A) -> B)을 입력으로 넣어서 호출한 결과를 반환한다.
Nothing이 아니라 Left인 점만 제외하면 메이비 애플리케이티브 펑터의 apply의 구현과 완전히 동일하다.
println(Either.pure(10))//Right(10)
println(Either.pure({x: Int -> x * 2})) // Right((kotlin.Int) -> kotlin.Int)
println(Either.pure({ x: Int -> x * 2}) apply Left("error"))// Left(error)
println(Either.pure({ x: Int -> x * 2}) apply Right(10))// Left(error)
println(Either.pure({x: Int, y: Int -> x * y}).curried()){
apply Left("error")
apply Right(10)
} //"Left(error)"
println(Either.pure({x: Int, y: Int -> x * y}).curried()){
apply Right(10)
apply Right(20)
}// Right(200)
Maybe의 Nothing과 마찬가지로 Either 애플리케이티브 펑터 체인의 중간에 Left가 있으면 Left가 나온다.
애플리케이티브 펑터의 법칙
펑터와 마찬가지로 애플리케이티브 펑터에도 법칙이 있다.
그건 다음과 같다.
identity: pure(identity) apply af = af 항등 법칙
Composition: pure(compose) apply af1 apply af2 apply af3 = af1 apply (af2 apply af3) 합성 법칙
Homomorphism: pure(function) apply pure(x) = pure(function(x)) 중동형 사상
Interchange: af apply pure(x) = pure(of(x)) apply af 교환 법칙
그리고 이 법칙들로 도출되는 마지막 법칙으로
pure(function) apply af = af.fmap(function)이 있다.
항등 법칙:
pure(identity) apply af = af
pure(identity) apply af = af
fun identity() = { x: Int -> x }
항등 함수에 값을 적용하는 것이 그대로 값을 반환하는 것.
val maybeAf = Just(10)
val leftMaybe = Maybe.pure(identity()) apply maybeAf
println(leftMaybe.toString() == maybeAf.toString())//true
val treeAf = Node(1, listOf(Node(2), Node(3)))
val leftTree = Tree.pure(identity()) apply treeAf
println(leftTree.toString() == treeAf.toString())//true
val eitherAf = Right(10)
val leftEither = Either.pure(identity()) apply eitherAf
println(leftEither.toString() == eitherAf.toString())//true
합성법칙 :
pure(compose) apply af1 apply af2 apply af3 = af1 apply (af2 apply af3)
fun <P1, P2, P3> compose() = {f: (P2) -> P3, g: (P1) -> P2, v: P1 -> f(g(v))}
compose를 사용하여 af1 af2 af3을 적용한것과 af1 apply (af2 apply af3)가 같다는걸 의미.
준동형 사상 법칙:
pure(function) apply pure(x) = pure(function(x))
좌변은 pure를 사용해서 function과 x값을 애플리케이티브 펑터에 넣는 것을 의미하고 우변은 function하뭇에 x를 적용한 function(x)을 애플리케이티브 펑터에 넣는걸 의미한다.
교환 법칙:
af apply pure(x) = pure(of(x)) apply af
좌변은 어떤 함수를 포함한 애플리케이티브 펑터 af와 값 x를 넣은 애플리케이티브 펑터를 적용하는 것을 의미하고 우변은 of(x)를 애플리케이티브 펑터에 넣어서 af을 적용하는걸 의미한다.
fun <T, R> of(value: T) = { f: (T) -> R -> f(value) }
of함수는 value 값을 입력으로 받아서 다른 함수의 입력 매개변수로 사용하는 람다 함수를 반환한다. of함수는 함수가 아닌값 x를 미래에 적용될 함수로 만들어 줌으로써 apply의 좌변에 있는 pure 함수의 입력으로 넣을 수 있게한다.
이 함수를 사용하면 미래에 입력받을 함수에 값 value를 적용할 함수를 만들 수 있다. 따라서 미래에 적용될 함수를 만들어 줌으로서 x를 apply 좌변에 있는 pure함수의 입력으로 넣을 수 있게 한다.
펑터와 애플리케이티브 펑터간의 관계
pure(function) apply af = af.fmap(function)
결과적으로 어떤 함수의 애플리케이티브 펑터에 값을 포함한 애플리케이티브 펑터를 적용한 결과는 함수를 펑터로 매핑한 결과와 동일하다.
apply 함수가 이 법칙에 근거해서 구현되었기 때문에 만족할 수 밖에 없다.
각 법칙은 수학적으로 증명된 이론으로 항상 기대한 동작을 한다는 것을 보장한다.
실전 응용
liftA2함수
두 개의 애플리케이티브 펑터들 사이에 이항 함수를 적용해서 애플리케이티브 스타일을 숨긴다. 일반 이항 함수를 받아서 두 개의 펑터에서 적용되는 함수로 승급했다고 볼 수 있다. 일반화라고 보면된다.
Just(10)을 Just([10])으로 변환하려면 Just(10).fmap { listOf(it) }과 같이 간단한 매핑을 사용한다.
그렇다면 3을 가진 메이비 Just(3)과 10을 넣은 리스트를 가진 메이비 Just([10])이 있을 때, 3과 10을 넣은 리스트를 가진 메이비 Just([3,10])을 liftA2함수를 사용해서 구현해보자.
fun <A, B, R> liftA2(binaryFunction: (A, B) -> R) = {
f1: Maybe<A> , f2: Maybe<B> -> Maybe.pure(binaryFunction.curried()) apply f1 apply f2
}
다음과 같이 승급함수를 만들 수 있다.
val lifted = liftA2 { x: Int, y: FunList<Int> -> FunList.Cons(x, y)}
lifted(Just(3), Just(funListOf(10)))
sequenceA 함수
이 함수는 애플리케이티브 펑터의 리스트를 받아서 각 애플리케이티브 펑터의 값들을 리스트로 가지고 있는 애플리케이티브 펑터 한개로 만들어준다.
다음은 Maybe타입 클래스의 sequenceA함수이다.
fun <T> cons() = {x: T, xs: FunList<T> -> FunList.Cons(x, xs)}
fun <T> sequenceA(maybeList: FunList<Maybe<T>>): Maybe<FunList<T>> =
when(maybeList) {
is FunList.Nil -> Just(funListOf())
is FunList.Cons -> Maybe.pure(cons<T>().curried()) apply maybeList.head
apply sequenceA(maybeList.tail)
}
sequenceA 함수는 메이비의 리스트를 입력받고 나서 메이비 안에 있는 값들의 리스트를 메이비로 포장한 결과를 반환한다.
함수 구현부에선 패턴 매칭을 사용한다.
입력 FunList가 비어있으면 빈 리스트를 포함한 메이비를 반환한다.
FunList안에 메이비가 존재하면 FunList를 maybeList,head와 tail로 분리한다.
그다음 head와 tail을 cons을 활용해서 재결합을 한다.
tail은 sequenceA을 재귀호출하면서 모든 메이비 내의 값들을 포함시킨다.
fun <T> sequenceAByFoldRight(maybeList: FunList<Maybe<T>>):Maybe<FunList<T>> =
maybeList.foldRight(Maybe.pure(funListOf()), liftA2(cons()))
5장의 foldright을 통해 구현할수 있다고 한다.