애플리케이티브 펑터
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
interface Applicative<out A> : Functor<A> {
fun <V> pure(value: V): Applicative<V>
infix fun <B> apply(ff: Applicative<(A) -> B>): Applicative<B>
}
- 함수를 가진 펑터가 다른 펑터의 값을 적용할 때 컨텍스트 안에서 처리할 수 없는 한계를 극복하기 위한 펑터.
- 값이
Just({ x -> x * 2})
인 펑터와 Just(5)
, 함수 { x- > x * 2 }
만 꺼내서 Just(5)
에 적용하려면:
- 일반적인 펑터:
Just(5).fmap(Just({ x -> x * 2 }))
- 펑터는 일반적인 함수
f: (A) -> B
로만 매핑할 수 있으므로 컴파일 에러가 발생한다.
- 애플리케이티브 펑터:
- 첫 번째 펑터
Just({ x -> x * 2 })
에서 { x -> x * 2 }
를 꺼낸다.
- 두 번째 펑터
Just(5)
에서 5
를 꺼낸다.
- 첫 번째 펑터에서 꺼낸 함수에 두 번째 펑터에서 꺼낸 값을 적용한다:
5 -> 5 * 2
- 마지막으로 적용한 결과를
Just
에 담아 Just(10)
을 반환한다.
- 펑터의
fmap
과 애플리케이티브 apply
를 비교하면:
fun <B> fmap(f: (A) -> B): Functor<B>
infix fun <B> apply(ff: Applicative<(A) -> B>): Applicative<B>
메이비 애플리케이티브 펑터
sealed class ApplicativeMaybe<out A> : Applicative<A> {
companion object {
fun <V> pure(value: V): Applicative<V> = ApplicativeJust(0).pure(value)
}
override fun <V> pure(value: V): Applicative<V> = ApplicativeJust(value)
abstract override fun <B> apply(ff: Applicative<(A) -> B>): ApplicativeMaybe<B>
}
data class ApplicativeJust<out A>(val value: A) : ApplicativeMaybe<A>() {
override fun toString(): String = "ApplicativeJust($value)"
override fun <B> apply(ff: Applicative<(A) -> B>): ApplicativeMaybe<B> = when (ff) {
is ApplicativeJust -> fmap(ff.value)
else -> ApplicativeNothing
}
override fun <B> fmap(f: (A) -> B): ApplicativeMaybe<B> = ApplicativeJust(f(value))
}
object ApplicativeNothing : ApplicativeMaybe<kotlin.Nothing>() {
override fun toString(): String = "ApplicativeNothing"
override fun <B> apply(ff: Applicative<kotlin.Nothing>): ApplicativeMaybe<B> = ApplicativeNothing
override fun <B> fmap(f: (kotlin.Nothing) -> B): ApplicativeMaybe<B> = ApplicativeNothing
}
- 애플리케이티브 펑터도 기본적으로 펑터이기 때문에
fmap
함수를 정상적으로 실행해야 한다:ApplicativeJust(5).fmap { it * 2 }
ApplicativeNothing.fmap { x: Int -> x + 10 }
pure
함수는 입력받은 값을 그대로 컨텍스트에 넣어서 반환한다:ApplicativeMaybe.pure(10)
apply
함수는 함수를 가진 메이비를 받아 값을 적용한 뒤 메이비에 넣어서 반환한다:ApplicativeJust(5) apply ApplicativeJust({ x: Int -> x * 2 })
ApplicativeNothing apply ApplicativeJust({ x: Int -> x * 2 })
- 체이닝도 가능하다:
ApplicativeMaybe.pure(5)
apply ApplicativeJust({ x: Int -> x * 2 })
apply ApplicativeJust({ x: Int -> x + 10 })
- 애플리케이티브 스타일 프로그래밍은 컨텍스트를 유지한 상태에서 함수에 의한 데이터 변환을 체이닝하는 방식이다.
- 만약
ApplicativeMaybe.pure({ x: Int -> x * 2 })
컨텍스트에서 체이닝을 시도하면 컴파일 에러가 발생한다.
Applicative
타입 클래스가 타입 매개변수가 한 개인 메이비만 허용하기 때문이다.
- 매개변수를 여러 개 전달하려면 커링을 사용한다:
fun <P1, P2, R> ((P1, P2) -> R).curried(): (P1) -> (P2) -> R =
{ p1: P1 -> { p2: P2 -> this(p1, p2) } }
ApplicativeMaybe.pure({ x: Int, y: Int -> x * y }.curried())
apply ApplicativeJust(10)
apply ApplicativeJust(20)
- 상속으로 펑터를 확장해 애플리케이티브 펑터를 만드는 대신 확장 함수를 사용할 수도 있다:
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
}
애플리케이티브 펑터의 법칙
- 모든 애플리케이티브 펑터가 지켜야하는 법칙이 있다.
- 수학적으로 증명된 법칙들이기 때문에 법칙을 만족한다면 항상 기대 동작을 보장한다.
- 수학적으로 자세한 내용은 프로그래머를 위한 카테고리 이론 참고.
항등 법칙
pure(identity) apply af = af
합성 법칙
pure(compose) apply af1 apply af2 apply af3 = af1 apply (af2 apply af3)
- 좌변:
pure
를 사용해 합성함수 compose
를 넣고 애플리케이티브 펑터 af1
, af2
, af3
를 적용.
- 우변: 애플리케이티브 펑터
af2
, af3
를 적용한 애플리케이티브 펑터를 af1
에 적용.
- 이때 좌변과 우변이 같아야 한다.
ApplicativeMaybe
가 합성 법칙을 만족하는지 보면:fun <P1, P2, P3> compose() = { f: (P2) -> P3, g: (P1) -> P2, v: P1 -> f(g(v)) }
val af1 = ApplicativeJust({ x: Int -> x * 2 })
val af2 = ApplicativeJust({ x: Int -> x + 1 })
val af3 = ApplicativeJust(30)
ApplicativeMaybe.pure(compose<Int, Int, Int>().curried()) apply af1 apply af2 apply af3
== af1 apply (af2 apply af3)
준동형 사상 법칙
pure(function) apply pure(x) = pure(function(x))
교환 법칙
af apply pure(x) = pure(of(x)) apply af
- 좌변: 애플리케이티브 펑터
af
와 값 x
를 넣은 애플리케이티브 펑터를 적용.
- 우변:
of(x)
를 애플리케이티브 펑터에 넣어 af
를 적용.
- 이때 좌변과 우변이 같아야 한다.
of
는 x
를 다른 함수의 매개변수로 제공하는 함수다:fun <T, R> of(value: T) = { f: (T) -> R -> f(value) }
value
값을 받아 다른 함수((T) -> R
)의 입력 매개변수로 사용하는 람다 함수를 반환한다.
- 이를 통해 미래에 입력받을 함수에 값
value
를 적용할 함수를 만들 수 있다.
- 값
x
를 미래에 적용될 함수로 만듦으로써 pure
함수의 입력으로 넣을 수 있게 해준다.
ApplicativeMaybe
가 교환 법칙을 만족하는지 보면:val af = ApplicativeJust({ a: Int -> a * 2 })
val x = 10
af apply ApplicativeMaybe.pure(x) == ApplicativeMaybe.pure(of<Int, Int>(x)) apply af
펑터와 애플리케이티브 펑터의 관계 법칙
pure(function) apply af = af.fmap(function)
이 문서를 인용한 문서
- 함수형 프로그래밍
- 펑터
-
이는 애플리케이티브 펑터를 사용해 해결해야 한다.
- 모나드
-
애플리케이티브 펑터는 apply
함수를 이용해 문제를 해결한다.
-
ApplicativeFunctor
가 아닌 Functor
를 상속한 이유는 이전에 애플리케이티브 펑터를 확장 함수로 구현했기 때문이다.