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

ㅎㄱㅎ·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개의 댓글