책 정보
- 코틀린으로 배우는 함수형 프로그래밍
- yes24
0. 시작하기 전에
- 모나드는 함수형 프로그래밍을 돕는 도구이다.
- 모나드 인터페이스 자체는 간단하나, 왜 얘가 함수형프로그래밍을 돕는지 이해를 하면 좋은 것 같다. 해야한다는 아님
- 모나드 법칙을 이해 안해도 함수형 프로그래밍 잘만 할 수 있을 것 같고, 새로운 모나드도 정의만 맞춘다면 구현할 수 있을 것 같다.
- map 과 flatMap 을 지원하면 된다.
- 현업에서 모나드를 우리가 구현해야 할 일은 사실 거의 없어보이긴한데, 기회가 되면 구현해보자.
1. 모나드 타입 클래스
1
2
3
4
5
6
interface Monad<out A> : Functor<A> {
fun <V> pure(value: V): Monad<V>
override fun <B> fmap(f: (A) -> B): Monad<B> = flatMap { a -> pure(f(a)) }
infix fun <B> flatMap(f: (A) -> Monad<B>): Monad<B>
infix fun <B> leadTo(m: Monad<B>): Monad<B> = flatMap { m }
}
- Webflux Publisher 기준으로 생각해보면
- pure = of, just 같은거라고 보면 되는것 같다.
- fmap = map
- flatMap = flatMap. infix function 으로 사용가능
- leadTo = then. infix function 으로 사용가능
2. 메이비 모나드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sealed class Maybe<out A> : Monad<A> {
companion object {
fun <V> pure(value: V) : Maybe<V> = Just(0).pure(value)
}
override fun <V> pure(value: V): Maybe<V> = Just(value)
override fun <B> fmap(f: (A) -> B): Maybe<B> = super.fmap(f) as Maybe<B>
override infix fun <B> flatMap(f: (A) -> Monad<B>): Maybe<B> = when (this) {
is Just -> try { f(value) as Maybe<B> } catch (e: ClassCastException) { Nothing }
is Nothing -> Nothing
}
}
3. 모나드 법칙
- 왼쪽 항등 법 :
pure(x) flatMap f = f(x)
- 오른쪽 항등 법칙 :
m flatMap pure = m
- 결합 법칙 :
(m flatMap f) flatMap g = m flatMap { x -> f(x) flatMap g }
- 함수 합성 관점에서 모나드 법칙
1
2
3
identity compose f = f
f compose identity = f
(f compose g) compose h = f compose (g compose h)
4. IO 모나드
- 함수형에서는 IO 기능이 아주 골칫거리임. DB조회, 호출, 파일읽기쓰기
- 그냥 분리하고 격리해라
- 하스켈 예시는 스킵
5. 리스트 모나드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sealed class FunList<out T> {
companion object
}
object Nil : FunList<kotlin.Nothing>() {
override fun toString(): String = "[]"
}
data class Cons<out T>(val head: T, val tail: FunList<T>) : FunList<T>() {
override fun toString(): String = "[${foldLeft("") { acc, x -> "$acc, $x" }.drop(2)}]"
}
infix fun <T, R> FunList<T>.flatMap(f: (T) -> FunList<R>): FunList<R> = fmap(f).flatten()
infix fun <T, R> FunList<T>.fmap(f: (T) -> R): FunList<R> = when (this) {
is Nil -> Nil
is Cons -> Cons(f(head), tail.fmap(f))
}
나머지 뒷장
- 로깅
- 함수 중간에 로깅 -> 별루 -> 확장함수 withLog 붙이면? 부수효과.. 순수함수 오염시킨다
1 2 3 4 5 6 7 8 9
private fun functionalSolution2(list: FunStream<Int>) = list .fmap { addFive(it) withLog "$it + 5" } .fmap { square(it) withLog "$it * $it" } .fmap { isGreaterThan50(it) withLog "$it > 50" } private infix fun <T> T.withLog(log: String): T { println(log) return this }
- WriterMonad 이용하자
1 2 3 4 5 6 7 8 9 10 11 12 13
data class WriterMonad<out T>(val value: T, val logs: FunStream<String>) : Monad<T> { companion object { fun <V> pure(value: V) = WriterMonad(0, mempty()).pure(value) } override fun <V> pure(value: V): WriterMonad<V> = WriterMonad(value, mempty()) override fun <R> flatMap(f: (T) -> Monad<R>): WriterMonad<R> { val applied = f(this.value) as WriterMonad<R> return WriterMonad(applied.value, this.logs mappend applied.logs) } }
- 예외처리
- 메이비 모나드를 쓰면 사실 해결이 됨. Optional.empty()
- 이더 모나드로 사유 기록 가능
- 트라이 모나드 확장 가능.
- 테스팅
- 우선 테스트하기 좋은 순수함수를 만들어라. kotlintest 쓰면 알아서 string 만들어주기도 함.
- FunList 같이 직접 만든 객체 랜덤은 생성못하니, generator 를 만들어서 넣어줘야함
- 근데 우린 junit 쓸 예정.
1 2 3 4 5 6 7
class FunListTest: StringSpec({ "testReverse" { forAll { a: String -> reverse(reverse(a)) == a } } })
- 우선 테스트하기 좋은 순수함수를 만들어라. kotlintest 쓰면 알아서 string 만들어주기도 함.
- 디버깅
- 보통 고차함수 + 게으른 실행이라.. 그냥 람다 함수 중간에 break point 찍어라
함수형 프로그래밍 마무리
1장 내용인데 까먹었으니깐
- 불변성, 참조 투명성, 일급함수, 게으른 실행, 타입 안정성
- 순수 함수
- 동일 입력 동일 출력
- 부수효과가 없다.
- 예외발생도 부수효과
- 객체 상태 변경
- 공유 변수 수정
- db 조회
- 그럼 함수형 프로그래밍은 db 조회하지마?
- 격리를 해서 최대한 순수함수를 만들고, 부수효과 일으키는 애는 격리를 하자