FP(Functional Programming) - 모나드를 사용하는 것에 대한 생각(완)

cfop·2020년 10월 30일
0

모나드 그럼 대체 언제 쓰나?

제가 경험한 웹 프로그래밍은 아래 두 케이스 입니다.

  • 서버에서 데이터 받아와서 화면 구성하는 일반 web app
  • 그래픽 라이브러리를 사용한 특수한 web app

한 케이스 씩 살펴 보겠습니다.

서버에서 데이터 받아와 화면 구성하는 일반 web app

react를 예로 들면, 도대체 redux-saga는 어떻게 redux에 보내는 action을 가로챌 수 있을까 궁금해서 소스코드를 슬쩍 봤었습니다


리덕스의 applyMiddleware인데요.. 두 가지가 눈에 띕니다.
함수를 리턴 하네요.. 무려 2중 함수 입니다. 어떤 시점에 받아서 createStore를 하겠다 유추가 됩니다. 또 compose가 있네요.. 뭔가 합성해서 store.dispatch를 넘겨서 가로챌 수 있나봅니다.


redux의 createStore코드 인데요. preloadedState가 함수면 그 함수를 실행 하면서 createStore 자기 자신 함수를 넘기고 다음 파라미터로 reducer랑 preloadedState를 넘기네요. 아하! 이걸 가지고 applyMiddeleware에서 뭔가 하겠구나 짐작이 됩니다.

아하! 그렇다면 함수 합성을 하는가보다! 하고 유추 할 수가 있습니다.

그래서 redux-saga는 generator로 동작 하는 이유가 대충 유추가 됩니다. 비동기로 진행하면 함수 합성이 될 수가 없기 때문이죠. 그래서 비동기를 제어하기 위해 generator 함수로 구현 해야 된다 까지 생각 해 볼 수 있겠네요.

이런 상황에서는 모나드를 쓰는게 적절하다는 생각을 했습니다.
가령 아래와 같은 상황

서버에서 데이터가 옵니다(promise라고 하겠습니다)
데이터가 null인지 아닌지부터 봐야 겠네요
<<async 함수 안에서..>>
const result = await getData();
if(result !== null){
 어쩌구 저쩌구..
}

이런 상황에서는 Maybe 모나드를 사용하게 되면
Maybe(result)
.map(어쩌구)
.map(저쩌구)

if문이 사라졌습니다. Maybe 모나드는 값이 null이나 undefined면
다음이 실행되지 않기 때문에. 뭐랄가 더 깔끔해 진 느낌 입니다.
더해서 Either를 써서 left 상태를 알아낼 수도 있겠네요.

꼭 Maybe가 아니어도 됩니다. 
result가 배열 이라면 우리는 이미 알고 있습니다.
result.map().filter().etc..
이런 식으로 쓴다는 걸요!

** 그러나 Maybe나 Either에 담긴 데이터는 primitive는 아니라서 엄격히 말하면 mapping되는 함수는 자연스럽게 순수함수가 아니게 됩니다**

더해서 한 케이스를 더 생각 해보도록 하겠습니다.

const result = getData().then((resolve, reject) =>{
    어쩌구..
    저쩌구..
})
이거는 Task 모나드로 해결하면 이렇게 되겠네요
Task((err, ok) =>{
 getData().then((resolve)=> ok(resolve))
}).map(어쩌구..).map(저쩌구..)

resolve된 데이터로 어쩌구.. 를 실행하고.. 그 결과를 가져다 또 저쩌구..를 실행한다고
코드를 보면 더 편하게 인식 할 수 있습니다.(제너릭 타입까지까지 잘 줬으면 더욱 실수할 일이 없겠죠)

그럼 redux-saga의 예를 들면 그 안에서는 함수 합성을 해도 관계 없겠구나!? 하고 결론을 내렸습니다. 어쩌면 더 안정적인 프로그래밍을 할 수 있을지 몰라.. 희망을 가지며 말이죠

그래픽 라이브러리를 사용한 특수한 web app

webGL관련 THREE.js를 예로 들겠습니다. 이런 경우에는 큰 장애물이 존재 합니다.
이런 그래픽 관련 라이브러리는 아래와 같은 특징이 있습니다.

  • 이미 객체지향적으로 구조화가 다 되어 있다.

이 말은 무슨 의미를 갖고 있나 생각 해보면, 객체 지향적으로 짯을때 가장 안정성이 높다는
결론에 다다릅니다. 함수형으로 억지로~억지로~ 할 수 있지만 코드가 엄청 장황해 집니다.

 const sphere = new THREE.Mesh(geometry, material);
 sphere.rotateOnAxis(new THREE.Vector(0, 1, 0), Math.PI / 2);

구를 만들고 y축 기준으로 90도 회전하라는 코드 입니다. 이걸 모나드로 어떻게 할 수 있을까요?

const sphere = new THREE.Mesh(geometry, material);
Maybe(sphere.rotation)
.map((d) => d.y = 90)
sphere.needsUpdate = true;
흠.. 이런 코드를 적으면서도 부자연 스러움이 물씬 느껴집니다.

누가 봐도 이상합니다. 번거롭습니다... 한가지 예를 더 보겠습니다.

게임 같은 경우는 객체 간에 상호작용이 많죠.
A 캐릭터가 B 캐릭터를 공격하면 B 캐릭터의 체력에서 A 캐릭터의 공격력만큼 깎는 케이스를
생각 해보겠습니다.

class A : 공격력 10, 체력 100
class B : 공격력 10, 체력 100

state 모나드를 써서..

const afterHP = State(A의 공격력)
.chain((value: A공격력)=> state에서 A공격력 빼서 state modify)
.evalState(B의 체력) // B의 체력 return

B.체력 = afterHP

이렇게 하면 B의 체력이 state가 되고 A공격력 따라서 state를 업데이트 하기 때문에 좌우지간 계산은 될 겁니다. 차라리 그냥 교전 판정 함수에서 처리하면 더 낫지 않을까요?

function judge(attacker: class, target: class){
	계산
}

이런 사례들을 고려 해봤을때. 이런 케이스에서는 모나드를 활용 하는게 떠오르는 방법이 몇가지 없습니다.. (저의 좁은 식견으로는) 그래서 부적절하지 않을까. 하는 생각이 들었습니다.

이러나 저러나 순수함수는 안되는거 아닌가?

네 맞습니다. 위의 케이스는 모두 순수함수가 아니죠. 순수 함수로 하게 되면 코드가 더 장황해 질겁니다. 이를 해결하는 방법은 여러개가 있겠네요

불변성 지키기

  • immutable.js같은 라이브러리 도움 받기
  • 1 depth 객체만 이용하여 Object.freeze하기
  • 매 리턴 시마다 deep copy를 하기

불변성을 지키는 방법 입니다. 그럼 자연스럽게 순수함수가 되죠. 정의역이 어떤 이유에서는 바뀌지 않을테니까요(보기만 해도 성능은 엄청 좋을 것 같지는 않네요)

적당히 합의 보기

모나드 보다는 함수 합성(pipe, go)에 더욱 집중하는 케이스 입니다.
그래 객체는 어쩔 수 없는 일이야.. 이 객체는 다른데서 바꿀 리가 없어 라고 하며 진행하는 것 입니다. 뭐.. 프로그램이 크고 복잡해지면.. 문제가 생기긴 하겠죠.. 이런 케이스라면 아마 프로그램 아주 일부분에만 소극적으로 도입 될 것 같습니다.

부족한 글 재주로 글을 여기까지 썻네요. 언제나 뭔가 연구할 게 있다는건 즐거운 것 같습니다.
인내를 갖고 읽어주신 분들께 사죄(?)와 감사의 말씀을 드립니다.
항상 건강하시고 행복 하세요

profile
dog발자

0개의 댓글