function programming - Monad

남자김용준·2024년 9월 19일
0

현재 개발하고 있는 프로젝트는 nextjs를 사용하여 개발을 진행하고 있다. 평소에 함수형 프로그래밍에 관심이 있었고 실제 업무에도 적용하고 싶은 마음이 있던 찰나에 compose 패턴에 관련한 글을 읽었고, 해당 패턴이 굉장히 직관적이고 가독성이 뛰어나다고 생각되어 프로젝트에 적용하게 되었다.

다만, 현재 개발중인 프로젝트의 대부분의 로직은 api를 작성하는 부분이 포함되어 있었다.
form data 를 input으로 받아서 api param에 맞게 input을 정제하고, input을 validation 한 뒤, api 를 호출한다. ( 그 뒤의 로직도 존재하긴 하지만 다음 로직은 각 동작마다 다르다)

처음에는 input을 정제하는 부분만 사용하여 별 문제가 없었다. 하지만 점차 input validation 과정과, api를 호출하는 부분, 그 후에 post logic에 대해서도 적용하고 싶은 생각이 들었다. 그만큼 compose를 통한 함수 조립은 그 형태/가독성이 매력적이었다. 다만 해당 케이스에 적용하기에는 문제가 있었다. 함수형 프로그래밍의 기본적인 논조는 [side effect가 발생하지 않는다] 이다. 하지만 지금 적용하려는 input validation이나 api 호출에는 side effect가 무조건적으로 발생한다. input validation의 과정에서 input이 맞지 않는 형태라면 에러메세지 혹은 에러를 뱉어내야 하고, api 실패에 대해 대비해야했다. 결국 compose를 통해서 조립한 함수들 중 중간에서 멈추고 흐름을 끊어야 하는 경우가 있었다.

이런 점을 보안하기 위해서 어떤 패턴이 있는 지 찾아보았고, 그 중 monad 라는 개념이 있어 공부를 하였다.

In functional programming, a monad is a structure that combines program fragments (functions) and wraps their return values in a type with additional computation. In addition to defining a wrapping monadic type, monads define two operators: one to wrap a value in the monad type,
and another to compose together functions that output values of the monad type (these are known as monadic functions).
ref. 위키백과

함수형 프로그래밍에서 모나드는 프로그램 조각들과 그 결과값들을 추상화한 추상적 개념이다.

  • 모나드 type으로 랩핑하는 것
  • 값을 출력하는 함수를 구성하는 것 (모나딕)

위의 설명을 보면 모나드는 특정 기능이 아닌 자료구조라고 볼 수 있다. 굉장히 단순하다고 볼 수 있는 이 구조가 어떻게 함수형 프로그래밍에서 side-effect가 추가되어도 문제없게끔 하는 것일까?

이 문제에 대한 해답은 굉장히 간단하다. 함수의 실행 결과를 항상 모나드로 반환하여 순수함수 성질을 잃지 않도록 하는 것이다. side-effect의 결과 자체를 side-effect가 아닌 본래 예상했던 결과로 생각한다고 느꼈다. 의도하지 않은 결과를 의도한 결과로 한 단계 wrapping한다. 굉장히 간단해 보인다.

좀 더 모나드 패턴을 자세하게 살펴보자.

functor

functor는 단순히 map함수를 들고 있는 인터페이스이다.
그렇다면 map함수는 어떤 것일까? 처음 해당 개념을 접할때는 javascript array map method로 받아들여서 배열을 순회하면서 뭘 하는건가? 싶었다.
하지만 map 은 단순히 monad 의 값에 콜백함수로 받은 함수를 실행시키는 걸 의미한다. (실제로 array의 map도 functor의 개념이다)

function map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
  
interface Functor<T> {
	map<R>(func:(value: T) => R): Functor<R | null>;
}

functor를 상속하여 구현한 class는 map함수를 구현해야한다.

let number?: Int? = 42
let addedThree: Int? = number.map { (number: Int) -> Int in
    return number + 3
}

print(addedThree)    //Optional(45)

위의 예시는 Optional context에 담겨있는 숫자를 map method를 통해서 3을 더해주는 함수를 적용한 반환값을 다시 optional context에 담아 반환하는 코드이다.

ref. https://velog.velcdn.com/images/mazol/post/a0675ea1-3201-4c14-8e86-b202dbb58785

반대로 context 내부에 값이 존재하지 않을 수도 있다. 그렇게되면 map은 콜백 함수를 실행하지 않고 빈 값을 context로 포장하여 반환한다.

하지만 map 함수에는 한 가지 문제가 있다. map은 U 타입을 가지는 함수를 전달받아 U?를 반환하는 함수이다. 그렇기 때문에 Optional<Optional>와 같이 type wrapping이 중첩될 수 있다..!

해당 문제를 해결하기 위해서 나온 개념이 flatMap이다.

func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?

flatMap은 U? 타입을 가지는 함수를 전달받아서 최종적으로 U?타입을 반환하는 함수이다. 이렇게 되면 위에서 말한 map함수의 문제를 해결할 수 있다.

이런 flatMap을 구현한 객체를 monad 라고 한다.

monad

그럼 모나드를 왜 사용할까? 위에서 말했던 것 처럼 monad를 사용하게 되면 side-effect를 제거할 수 있다.
로직에서 side-effect가 없어진다? 그 말은 즉, 함수형 프로그래밍이 등장할 수 있는 배경이 만들어진단 의미이다.
결국 모나드는 side-effect가 있는 함수를 일급 함수로 만들어주고 연속적인 체이닝 함수의 합성을 가능하게 한다.

실제 프로젝트에서는 Either monad를 사용하여 함수 진행중의 에러 상황과 정상동작 상황을 구분하여 compose pattern을 이용하여 로직을 구성하였다. 그 결과 보다 직관적인 로직을 구성할 수 있었고, 팀원들도 모두 만족하는 방향으로 개발이 진행되었다.

정리

업무중에 문제 상황을 해결하기 위해서 접한 monad 라는 개념을 정리해보았다. 오랜만에 글을 써서 그런 지 정리가 잘 되지도 않았던 거 같고, 개념 자체도 설명하기에 난이도가 있어 글이 중구난방으로 진행된 거 같지만 단순히 한 가지로 기억하면 되지 않을까 싶다.

monad란 side-effect를 예측할 수 있는 결과로 만들어 side-effect를 발생시키지 않는 개념 (곧 추상화)
뭔가 글을 너무 오랜만에 다시 써서 두서없이 진행된 거 같다.

reference

profile
frontend-react

0개의 댓글