펑터
class Functor f where
fmap :: (a -> b) -> f a -> f b
interface Functor<out A> {
fun <B> fmap(f: (A) -> B): Functor<B>
}
- 펑터는 ‘매핑 가능한’ 행위를 선언한 타입 클래스:
- 펑터를 다시 정리하면:
- 리스트와 같은 컨테이너형 타입의 값을 꺼내서
- 인자로 받은 함수를 적용한 다음
- 함수의 결과값을 컨테이너형 타입에 넣어 반환
- 하는 행위를 선언한 타입 클래스다.
- 펑터 자체는 추상화된 타입이기 때문에
List<T>
와 같은 일반화된 타입을 가진다:
- 따라서 한 개의 매개변수를 받는 타입 생성자다.
메이비 펑터
sealed class Maybe<out A> : Functor<A> {
abstract override fun toString(): String
abstract override fun <B> fmap(f: (A) -> B): Maybe<B>
}
data class Just<out A>(val value: A) : Maybe<A>() {
override fun toString(): String = "Just($value)"
override fun <B> fmap(f: (A) -> B): Maybe<B> = Just(f(value))
}
data class Nothing<out A>(val value: kotlin.Nothing) : Maybe<A>() {
override fun toString(): String = "Nothing"
override fun <B> fmap(f: (kotlin.Nothing) -> B): Maybe<B> = Nothing
}
이더 펑터
sealed class Either<out L, out R> : Functor<R> {
abstract override fun <R2> fmap(f: (R) -> R2): Either<L, R2>
}
data class Right<out R>(val value: R): Either<Nothing, R>() {
override fun <R2> fmap(f: (R) -> R2): Ether<Nothing, R2> = Right(f(value))
}
data class Left<out L>(val value: L): Either<L, Nothing>() {
override fun <R2> fmap(f: (Nothing) -> R2): Ether<L, R2> = this
}
단항 함수 펑터
data class UnaryFunction<in T, out R>(val g: (T) -> R) : Functor<R> {
override fun <R2> fmap(f; (R) -> R2): UnaryFunction<T, R2> =
UnaryFunction { x: T -> f(g(x)) }
fun invoke(input: T): R = g(input)
}
- 함수도 펑터로 만들 수 있다.
- 함수는 여러 개의 매개변수를 받을 수 있지만,
Functor
타입 클래스는 하나의 매개변수만 가진다:
- 이 문단에서는 매개변수가 하나인 단항 함수에 대한 펑터로 제한한다.
- 단항 함수는 입력 하나, 출력 하나를 가지므로 타입 매개변수 두 개가 필요하다.
- 이때 변경되는 값은 출력 뿐이므로 입력 값은 고정할 수 있다.
- 따라서 펑터의 타입은
Functor<R>
이다.
fmap
메서드:
- 펑터 안의 함수
g
를 인자로 전달된 함수 f
에 적용하고 결과를 UnaryFunction
에 넣어 반환한다.
- 체이닝을 위해
UnaryFunction<T, R2>
타입을 반환한다.
invoke
메서드:
- 펑터 안의 함수
g
를 호출한 결과를 그대로 반환한다.
- 실제로 사용해보면 이렇다:
val f = { a: Int -> a + 1 }
val g = { b: Int -> b * 2 }
val fg = UnaryFunction(g).fmap(f)
fg.invoke(5)
펑터의 법칙
- 펑터가 되기 위해서는 두 가지 법칙을 만족해야 한다.
- 펑터를 통해 항등 함수를 매핑하면 반환되는 펑터는 원래의 펑터와 같다.
- 두 함수를 합성한 함수의 매핑은 각 함수를 매핑한 결과를 합성한 것과 같다.
- 펑터의 법칙을 만족하면 펑터의
fmap
이 매핑 동작 외에는 어떤 것도 하지 않는다는 것을 보장할 수 있다.
펑터 제1법칙
fmap(identity()) == identity()
펑터 제2법칙
fmap(f compose g) == fmap(f) compose fmap(g)
- 함수
f
와 g
를 합성한 결과를 fmap
의 입력으로 넣어서 얻은 결과는 각 함수를 따로 fmap
의 입력으로 넣어 얻은 결과를 합성한 결과와 같아야 한다.
Maybe
펑터가 제2법칙을 만족하는지 보면:infix fun <F, G, R> ((F) -> R).compose(g: (G) -> F): (G) -> R =
{ gInput: G -> this(g(gInput)) }
val f = { a: Int -> a + 1 }
val g = { b: Int -> b * 2 }
val left = Nothing.fmap(f compose g)
val right = Nothing.fmap(g).fmap(f)
left == right
val left = Just(5).fmap(f compose g)
val right = Just(5).fmap(g).fmap(f)
left == right
compose
는 입출력 함수이기 때문에 Maybe
로는 체이닝이 불가능하다:
- 그래서
Nothing.fmap(f) compose Nothing.fmap(g)
처럼하면 컴파일 에러가 난다.
- 이는 애플리케이티브 펑터를 사용해 해결해야 한다.
- 하지만 여기에선
fmap
을 체이닝하는 것으로 대체한다.
이 문서를 인용한 문서
- 함수형 프로그래밍
- 모나드
-
펑터는 (A) -> B
, (B) -> C
함수를 합성해 (A) -> C
함수를 만든다.