Monad

모나드의 개념과 활용을 Functor를 기반으로 설명하며, 비동기 처리 및 일반적인 모델링이 어려운 상황을 해결하는 방법을 알아봅니다.

  1. 들어가기 전에 DB 에 데이터를 저장하는 예시를 들어 보면, IO 과정에서 비동기 처리가 이루어지게 된다. 이러한 Process 가 Blocking 형태로 처리되게 되면 이 작업은 한 쓰레드 전체를 점유하게 된다. Non-Blocking 형식으로 처리하는 방법으로는 여러가지가 있지만, Javascript 의 경우에는 Callback 함수, Promise, Async 방식을 이용한다. 이 예시에서, Javascript 의 Promise 는 Monad 이다. Java 의 Optional Class 또한 Monad 의 일종이다.
Optional.ofNullable(response.getCart()).ifPresent(cart -> {
    Optional.ofNullable(c.getProduct()).ifPresent(p -> 
        System.out.println(p.getName()));
})


  1. Monad 란 무엇인가 ?

Monad는 값을 담는 컨테이너의 일종이다. Functor 를 기반으로 구현되어있다. flatMap() 메소드를 제공한다. Monad Laws 를 만족시키는 구현체를 말한다.


import java.util.function.Function;

interface Functor<T> {
    <R> Functor<R> map(Function<T, R> f);
}


map() 의 진정한 의미는 ~~컬렉션의 원소를 순회하는 방법~~ T 타입의 Functor 를 R 타입의 Functor 로 바꾸는 것이다.

List<String> numberStrings = Arrays.asList("1", "2", "3", "4", "5");
List<Integer> numbers = numberStrings.stream()
                                        // <String> Functor -> <Integer> Functor
                                        .map(Integer::parseInt)
                                        .collect(Collectors.toList())


  1. Functor 은 왜 쓸까 ?
class FOptional<T> implements Functor<T, FOptional<?>> {
    private final T valueOrNull;
    private FOptional(T valueOrNull) {
        this.valueOrNull = valueOrNull;
    }
    public <R> FOptional<R> map(Function<T, R> f) {
        if(valueOrNull == null) {
            return empty();
        }
        else {
            return of(f.apply(valueOrNull));
        }
    }
    public static <T> FOptional<T> of(T t) {
        return new FOptional<T>(t);
    }
    public static <T> FOptional<T> empty() {
        return new FOptional<T>(null);
    }
}

...
// null 값이지만, 모나드를 이용해서 합성 함수를 만들 수 있다.
// -> 사용하는 쪽에서 null 체크가 불필요 
// (타입안정성을 지키면서 null 인코딩 가능)
FOptional<String> optionStr = FOptional(null);
FOptional<Integer> optionInt = optionStr.map(Integer::parseInt);


// sudo code 
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[]>


class FList<T> implements Functor<T, FList<?>> {
    // 단순히 Functor 가 담고 있는 값이 list 임.
    private final ImmutableList<T> 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) {
            // list 의 모든 원소에 함수 f를 적용
            result.add(f.apply(t));
        }
        return new FList<>(result);
    }
}


  1. 그럼 Monad 는 뭘까 ?

Functor 에 flatMap() 이 추가된 형태이다.

FOptional<String> num = FOptional.of("42");
FOptional<Integer> answer = num.map(this::tryParse); // num 이 FOptional<String> 이어서 chaining 불가능


때로는 Functor 자체를 반환하는 함수도 많은데, Functor\<Functor\<T>> 의 구조가 되면, 정상적인 함수 chaning 이 불가능해진다. Functor 를 반환하는 함수는 매우 일반적이다. 그래서 FlatMap 이 정의된다.

interface Monad<T, M extends Monad<?, ?>> extends Functor<T, M> {
    M flatMap(Function<T, M> f);
}
FOptional<String> num = FOptional.of("42");
FOptional<Integer> answer = num.flatMap(this::tryParse); // num 은 FOptional<String>
FOptional<Date> date = answer.map(t -> new Date(t)); // 합성 가능


여기서, 위에서 설명했던 Monad 의 의미를 다시 읽어보면 조금 더 이해가 잘될 수 있다.

Monad는 값을 담는 컨테이너의 일종이다. Functor 를 기반으로 구현되어있다. flatMap() 메소드를 제공한다. Monad Laws 를 만족시키는 구현체를 말한다.

  1. Monad 의 의의

이것도 읽어보세요