이전에 설명한 functor 글을 읽어보시면 더욱 도움이 됩니다.
여기서도 샘플 코드는 kotlin으로 작성합니다.
오류에 대한 정정이나 의견은 언제든지 환영합니다.
Option 타입은 일반적으로 Some, None의 두 타입으로 정의됩니다.
Some은 값이 존재할 경우의 타입이고 None은 값이 존재하지 않을 경우의 타입입니다.
sealed class Option<A> {
data object None : Option<Nothing>()
data class Some(val value: A) : Option<A>
}
Java의 Optional은 위에서 적은 것과 같이 타입으로 구분되지 않고, isPresent(), isEmpty() 같은 함수를 통해 Some, None을 구분한다고 이해할 수 있습니다.
val t = Optional.ofNullable<Int>(null)
assert(t.isPresent() == false)
assert(t.isEmpty() == true)
val k = Optional.ofNullable<Int>(1)
assert(t.isPresent() == true)
assert(t.isEmpty() == false)
Optional도 map 이라는 함수를 지원하는데, 이 map 함수는 Optional의 구조를 보존해주지 않습니다.
이 말은 Optional이 Some 이었다가 None으로 변할 수도 있다는 뜻입니다.
예를 들어 보면 아래와 같습니다.
fun <T> returnNull(value: T?): T? {
return null
}
fun myDouble(value: Int?): Int? {
return value?.let { it * 2 }
}
val nullableValue = Optional.ofNullable<Int?>(123)
println(nullableValue.isPresent()) // true == Some
val doubledValue = nullableValue.map(::myDouble)
println(doubledValue.isPresent()) // true == Some
val finalValue = nullableValue.map(::returnNull)
println(finalValue.isPresent()) // false == None
이전의 functor 글에서는 설명하지 않았지만, 카테고리 이론에서의 functor는 대상과 사상을 모두 포함하여 매핑하는 개념이고, 만약 f라는 함수가 A -> B로 변환하는 함수라면 functor를 통해 매핑된 특정 카테고리에 있는 사상인 F(f)는 F(A) -> F(B)로 F(A)와 F(B)를 연결하면서 그 구조를 그대로 유지해줘야 합니다.
이 법칙은 identity law(fa.map(::identity) == fa)를 통해서도 유추할 수 있습니다.
하지만 위의 Optional 코드에서는 map 함수를 통해 매핑을 수행하면 마지막 finalValue에서 사실상 Some에서 None으로 타입이 바뀌고, 따라서 그 구조를 그대로 유지해주지 않습니다.
Optional이 Functor로서 기능한다고 이해하고 코드를 짜고 있었다면 이는 버그를 유발하게 되고, 이는 인지하기 어려운 오류를 발생시킬 가능성이 있습니다.
사실 Optional은 functor의 개념을 고려하지 않고 본질적으로 "null" 방지를 목적으로 개발된 타입이라서 이러한 개념을 적용할 만한 대상은 아닙니다.
null 방지를 목적으로 사용할 때는 사용해도 되지만 그렇지 않다면 Java의 Vavr이나 Kotlin의 Arrow.kt, Scala의 Option을 사용하여야 합니다.