[컨퍼런스] NAVER D2 - 모나드(Monad)란 무엇인가?

어느 개발자·2022년 2월 2일
0

컨퍼런스

목록 보기
2/2

본 포스팅은 유튜브 naver d2 채널의 Monad란 무엇인가? 영상을 보고 작성하였습니다.

Functor
그럼 값을 꺼낼 수도 없고, 할 수 있는 일이라고는 map() 메소드로 값을 변경하는 것 뿐인 군더더기 같은 Functor를 왜 쓰는 걸까?

이유 1

Functor를 이용하면 일반적으로 모델링할 수 없는 상황을 모델링할 수 있다.

  • 값이 없는 케이스
  • 값이 미래에 준비될 것으로 예상되는 케이스

이유 2

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는 뭔가요?

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 함수를 가지고 있다.

그럼 Monad는 뭔데?

Functor는 잘 알겠는데, 그럼 Monad는 뭔데 !

Monad는 Functor에 flatMap() 을 추가한 것

Monad는 Functor의 문제점을 보완하기 위해 나온 것이다. 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();
    }   
}


앞에서 다룬 예제는 위와 같다.
mapInteger를 넘겨서 반환값이 FOptinal<Integer> 이다.
여기서는 문제가 발생하지 않는다.


mapFOptinal<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 이 도입되었다.

flatMap()

interface Monad<T, M extends Monad<?,?>> extends Functor<T,M> {
	M flatMap(Function<T,M> f); // 변형함수 f의 타입인자인 M을 반환
}

flatMap 도 결국 함수를 파라미터로 받는 녀석인데, 그 함수가 T를 받아서 M을 반환한다.
그럼 이 flatMapM을 반환하는 구조이다.

map과 비교

map 과 비교해보면 큰 차이를 느낄 수 있다.

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

mapFunctor<R> 를 반환한 반면, flatMap 은 그냥 M을 반환한다.
이것만 차이이다.

flatMap의 적용

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의 정의

  • Monad는 값을 담는 컨테이너의 일종
  • Functor를 기반으로 구현되었음
  • flatMap() 메소드를 제공함
  • Monad Laws를 만족시키는 구현체

Monad의 의의

  • 값이 없는 상황이나, 값이 미래에 이용해질 상황 등 일반적으로는 할 수 없는 여러 상황을 모델링 할 수 있다.
  • 비동기 로직을 동기 로직으로 구현하는 것과 동일한 형태로 구현하면서도, 함수의 합성 및 완전한 non-blocking plpeline을 구현할 수 있다.

0개의 댓글