본 포스팅은 유튜브 naver d2 채널의 Monad란 무엇인가? 영상을 보고 작성하였습니다.
Functor
그럼 값을 꺼낼 수도 없고, 할 수 있는 일이라고는 map()
메소드로 값을 변경하는 것 뿐인 군더더기 같은 Functor를 왜 쓰는 걸까?
Functor를 이용하면 일반적으로 모델링할 수 없는 상황을 모델링할 수 있다.
Functor를 이용하면 함수들을 손쉽게 합성할 수 있다.
class FOptional<T> implements Functor<T, FOptinoal<?>> {
private final T valueOrNull;
private FOptinal(T valueOrNull) {
this.valueOrNull = valueOrNull;
}
public <R> FOptinal<R> map(Function<T, R> f) {
if (valueOrNull == null) // 값이 비어있으면 empty() 호출하고, f 함수는 호출하지 않음
return empty();
else
return of(f.apply(valueOrNull));
}
public static <T> FOptional<T> of(T a) {
return new FOptional<T>(a);
}
public static <T> FOptional<T> empty() {
return new FOptinal<T>(null); // 값이 비어있는 경우 null을 값으로 가지는 Functor를 반환
}
실제 사용
FOptional<String> optionStr = FOptional(null);
FOptional<Integer> optionInt = optionStr.map(Integer::parseInt);
null
값이 들어있음에도 일반 문자열이 있는 것처럼 map
함수를 적용할 수 있고 parseInt
함수를 전달할 수 있다.
이것은 값이 들어있는 아래 케이스와 완전히 동일한 로직이다.
FOptional<String> optionStr = FOptional("1");
FOptional<Integer> optionInt = optionStr.map(Integer::parseInt);
사용하는 쪽에서 null
check가 불필요하며, null
인 경우 그냥 로직이 실행되지 않는다.
이것이 타입 안정성을 유지하면서 null
을 인코딩 하는 방법이다.
Promise<Customer> customer = // ...
Promise<byte[]> bytes = customer.map(Customer::getAddress) // return Promise<Address>
.map(Address::street) // return Promise<String>
.map((String s) -> s.substring(0, 3)) // return Promise<String>
.map(String::toLowerCase) // return Promise<String>
.map(String::getBytes); // return Promise<byte[]>
Promise<Customer>
는 아직 Customer
값을 가지고 있지 않다. 하지만 FOptional
과 동일하게 map
메소드를 적용할 수 있다. 문법적으로나 의미적으로 완전히 동일하다.
비동기로 이루어지는 연산들인데 동기로 이루어지는 것 처럼 작성할 수 있다.
비동기 연산들의 합성이 가능하다.
List도 일종의 Functor
인데 값이 하나가 아니고 List인것이다.
class FList<T> implements Functor<T, FList<?>> {
private final ImmutableList<T> list; // 단순히 Functor가 담고 있는 값이 List임
FList(Iterable<T> value) {
this.list = ImmutableList.copyOf(value);
}
@Override
public <R> FList<?> map(Function<T, R> f) {
ArrayList<R> result = new ArrayList<R>(list.size());
for (T t : list) {
result.add(f.apply(t)); // List의 모든 원소에 함수 f를 적용
}
return new FLIst<>(result);
}
}
parseInt
메소드를 List의 각 원소에 적용
List<String> num = Arrays.asList("1", "2", "3", "4", "5");
List<String> collect1 = num.stream().map(Integer::parseInt).collect(Collectors.toList());
Scala의 대부분의 Collection에는 대부분 map
함수를 가지고 있다.
Functor는 잘 알겠는데, 그럼 Monad는 뭔데 !
Monad는 Functor에
flatMap()
을 추가한 것
Monad는 Functor의 문제점을 보완하기 위해 나온 것이다. Functor의 문제점이 무엇인지 알아보자.
FOptional<Integer> tryParse(String s) {
try {
final int i = Integer.parseInt(s);
return FOptional.of(i); // 여기서 이미 Functor를 반환
} catch (NumberFormatException e) {
return FOptional.empty();
}
}
앞에서 다룬 예제는 위와 같다.
map
에 Integer
를 넘겨서 반환값이 FOptinal<Integer>
이다.
여기서는 문제가 발생하지 않는다.
map
에 FOptinal<Integer>
을 넘기므로 반환값은 FOptional<FOptional<Integer>>
가 된다.
즉, Functor가 Functor에 감싸져 있으면 함수의 합성과 체이닝을 저해한다는 문제점이 발생한다.
FOptional<Integer> num1 = // ...
FOptional<FOptional<Integer>> num2 = // ...
FOptional<Date> date1 = num1.map(t -> new Date(t));
FOptional<Date> date2 = num2.map(t -> new Date(t)); // 컴파일 에러
Functor가 두 번 감싸짐으로 인해 제 기능을 하지 못한다.
But 앞에서 본 tryParse
메서드처럼 반환값이 Functor인 함수는 매우 일반적이라는 것이다..
이러한 상황을 위해
flatMap
이 도입되었다.
interface Monad<T, M extends Monad<?,?>> extends Functor<T,M> {
M flatMap(Function<T,M> f); // 변형함수 f의 타입인자인 M을 반환
}
flatMap
도 결국 함수를 파라미터로 받는 녀석인데, 그 함수가 T
를 받아서 M
을 반환한다.
그럼 이 flatMap
도 M
을 반환하는 구조이다.
map
과 비교해보면 큰 차이를 느낄 수 있다.
interface Funtor<T> {
<R> Functor<R> map(Funciton <T,R> f);
}
map
은 Functor<R>
를 반환한 반면, flatMap
은 그냥 M
을 반환한다.
이것만 차이이다.
FOptional<String> num = FOptional.of("42"):
// tryParse의 반환값: FOptinal<Integer>
FOptional<Integer> answer = num.flatMap(this::tryParse);
FOptional<Date> date = answer.map(t -> new Date(t)); // 합성 가능
이것이 flatMap
이 가지는 진정한 의미이다.
num.flatMap(this::tryParse)
.map(t -> new Date(t));
.flatMap
.
.
flatMap
을 하고 나면 그 결과에 map
을 할 수도 있고, flatMap
을 할 수도 있다.
(주르륵 할 수 있게 되는 것이다)
이러한 합성을 할 수 있게 하는 것이 Monad가 가지는 강점이라고 할 수 있다.
- Monad는 값을 담는 컨테이너의 일종
- Functor를 기반으로 구현되었음
flatMap()
메소드를 제공함- Monad Laws를 만족시키는 구현체
- 값이 없는 상황이나, 값이 미래에 이용해질 상황 등 일반적으로는 할 수 없는 여러 상황을 모델링 할 수 있다.
- 비동기 로직을 동기 로직으로 구현하는 것과 동일한 형태로 구현하면서도, 함수의 합성 및 완전한 non-blocking plpeline을 구현할 수 있다.