[FP 스터디] 5장 - 복잡성을 줄이는 디자인 패턴 [1/2]

horiz.d·2023년 8월 13일
0

복잡성을 줄이는 디자인 패턴 [1/2]

FP 스터디 - Functional-programming-in-javascript


중요 키워드 정리

  1. 함수자 (Functor): 래퍼 안으로 값을 승급하고, 이후 수정이 필요할 때 수정 후 다시 값을 래퍼에 넣을 목적을 염두에 둔 함수 매핑이 가능한 자료구조

  2. 모나드: 컨테이너 내부로 값을 승급하고, 어떤 규칙을 정해 통제한다는 생각으로 자료형을 생성


Why functor & monad for handling erros?

루프와 조건문을 함수로 추상했던 것 처럼 에러처리도 어떤 식으로든 추상할 필요가 있습니다.

필자는, 기존 명령형 프로그램의 try-catch 에러 처리의 몇가지 단점을 짚으며 에러처리를 추상화 할 것을 권장한다.

  1. 다른 함수형 장치처럼 합성이나 체이닝을 할 수 없다.
  2. 예외를 던지는 행위는 함수 호출에서 빠져나갈 구멍을 찾는 것으로, 단일한, 예측 가능한 값을 지향하는 참조 투명성 원리에 위배된다.
  3. 예기치 않게 스택이 풀리면 함수 호출 범위를 벗어나 전체 시스템에 영향을 미치는 부수효과를 일으킨다.
  4. 에러를 조치하는 코드가 호출한 지점과 떨어져있어 비지역성 원리에 위배된다. 즉, 에러가 날 경우 함수는 지역스택과 환경에서 벗어난다 = 스코프에 따른 문제를 지적하는 것으로 이해했다.

제이슨(@leejaeseung)은 명령형 에러 처리의 문제점을 더 깊이있게 조사했고, 어제 대면 스터디에서 짧지않은 시간을 할애하여 이를 공유해주었다.

그 결과, FP에서 명령형 에러처리를 사용했을 때 가장 문제가 될 수 있다고 진단한 부분은 아래와 같았다.

함수가 Either 로 반환되면 개발자는 이 Either 를 처리할 때 예외에 대한 처리를 할 수 밖에 없다. (객체 내부 데이터를 가져오려면) 함수가 try-catch 였다면 예외처리가 필수가 아니게 된다.

제이슨은 이에 대해 자신이 작업하고 있는 코드 일부를 예로 들어 보여주었는데, try catch에 의해 throw error를 처리하는 경우, 던져진 error는 main에서 마저 throw되며 사실상 아무런 에러처리 없이 모든 곳에서 책임 없이 던져졌다.

하지만 FP에서 모나드 & Functor를 활용하는 Either를 사용한 에러처리의 경우, 반드시 어떤 요소는 이를 열어(by identity)봐야 하므로 마지막까지 에러처리를 신경쓰지 않을 수는 없었다. 필요한 값의 조회를 위해서라도 반드시 누군가는 열어봐야 했고, 이것이 에러였을 경우에 대한 처리 또한 필요한 것.

명령형 에러 처리의 문제점



2. 더 나은 방안 functor(함수자)

FP에서는 위험한 코드를 감싼다는 개념은 그대로 가져가되, try-catch`블록은 제거합니다. 함수형 자료형을 사용하여 불순함과의 분리를 일급 시민으로 만드는 것이지요.

값을 감싼다, 즉 컨테이너화하는 것은 함수형 프로그래밍의 기본 디자인 패턴이라고 한다.
필자가 제시한 functor의 constructor에 의한 컨테이너화이든, 모나드의 단위함수 of이든 이들은 값을 감싼다.

이렇게 감싼 값에 접근하는 유일한 방법은 연산을 컨테이너에 매핑 하는 것이다.

그리고 이에 더해, FP를 배우기 전 그저 익숙하게만 사용해왔던 List 뿐 아니라, FP 패러다임 상에서 구현된 그 모든 자료구조가 사실 Fuctor, 모나드 라는 사상 위에서 구현되어 있었다(어제 제이슨과 까봤다). 그러므로 여러 다양한 체이닝, 구조를 주무르고 빚고 할 수 있었던 것.



다시 돌아와서 컨테이너화 시킨 값(승급된 값)은 오직 아래와 같이 identityfmap에 매핑하여 조회할 수 있다.

const wrappedValue = wrap('Get Functional'); // 값을 승급, 컨테이너화

wrappedValue.map(R.identity); //-> 'Get Functional' 

wrappedValue.map(toUpper); //-> Wrapper(5)    :   값을 함수에 따라 수정, 저장
wrappedValue.map(R.identity) //-> 'GET FUNCTIONAL'

즉, 이는 객체지향 프로그래밍 처럼 값을 캡슐화했다고 이해된다.
다만, 객체지향 프로그래밍의 경우 추상화단계가 낮은 (메서드와 기타등등이 포함된) 어떤 덩어리에 대한 값을 캡슐화하는데 반해,

FP는 그저 값에 대한, (심지어는 값이 객체가 될 수 있는), 캡슐화를 이루며 이것의 높은 추상화 방안(모나드에 의해 가능한)덕분에 Functor는 자신이 받을 값을 모르고도 체이닝 패턴을 약속할 수 있는 것이다.



또한 Functor(함수자)의 가장 중요한 요소 중 하나로, fmap이 있다.

fmap은 컨테이너를 열고 그 안에 보관된 값에 주어진 함수를 적용한 다음, 그 결과를 동일한 형식의 새 컨테이너를 만들어 담고(승급) 닫아 반환한다.

이 개념은 더 자세히는 applicative functor라고 부른다고 하며, 그 모습은 간단하게 아래의 그림과 같다.
image



위의 내 예시코드 블록에서 봤다시피, fmap은 그저 컨테이너 문을 여닫고 인자로 받은 함수를 값에 매핑하는 통로정도의 역할만 수행하며, 이것이 functor의 핵심이다.

그리고 Functor 판별 제 1,2 법칙은 아래와 같다고 한다.

  1. 부수효과가 없어야 한다: 컨텍스트에 R.identity 함수를 매핑하면 동일한 값을 얻어야 한다. 이는 함수자가 부수효과 없이 감싼 값의 자료구조를 그대로 유지한다는 결정적 증거가 된다.
wrap('Get Functional').fmap(R.identity); //-> Wrapper('Get Functional')
  1. 합성이 가능해야 한다: 이는, 1번을 충족할 경우 자연히 만족된다는 정리가 증명되었다고 제이슨이 조사한 자료를 통해 알 수 있었다.




2. 모나드 : 제어 흐름에서 데이터 흐름으로

특정한 케이스를 특정한 로직에 위임하여 처리할 수 있다는 점이 모나드의 가장 중요한 특성 중 하나라고 설명한다.

함수자(Functor)처럼 모나드도 자신의 상대가 어떤 값인지는 전혀 모른 채, 일련의 단계로 계산 과정을 서술하는 디자인패턴입니다. 컨테이너 안으로 값을 승급하고, 어떤 규칙을 정해 통제한다는 생각으로 자료형을 생성하는 것이 바로 모나드입니다.

함수자로 값을 보호하되, 합성을 할 경우 데이터를 안전하고 부수효과 없이 흘리려면 모나드가 필요하다.
예를 들어 아래와 같이 잘못된 입력이 넘어와도 컨테이너가 알아서 함수를 매핑할 수 있다.

half를 짝수에만 적용하고 싶은 케이스 코드이다.

class Empty {
 map(f) {
  return this;
}

fmap (_) {
 return new Empty();

toString() {
 return 'Empty ()';
 }

cosnt isEven = (n) => Nubmer.isFinite(n) && (n % 2 == 0);
const half = (val) => isEven(val) > wrap(val / 2) : empty();

half(4); //-> wrapper(2)
half(3); //-> Empty

앞 예제에서 홀수가 넘어오면 null 대신 Empty 컨테이너를 반환했다. 이렇게 하면 에러 염려 없이 원하는 연산을 수행할 수 있다.

half(4).fmap(plust3); //-> Wrapper(5)
half(3).fmap(plus3); //-> Empty      : 잘못된 입력이 넘어와도 컨테이너가 알아서 함수를 매핑



이 빡에도 모나드로 Functor만의 한계점을 극복하는 다양한 문제를 해결할 수 있다고 한다.
제이슨에 따르면 Functor는 수학상에서 존재하는 개념이지만, 모나드는 이를 실제 프로그래밍 세계에 적용하며 프로그래밍 언어 와 체계 상에서 존재하는 다양한 한계를 극복하기 위해 이것더하고 저것더하고(?)하며 만들어진 개념이라고..

그래서 실제로 모나드란 단 하나 정해진 것이 아닌, 인터페이스와 같은 것이며 이를 기반으로 필요에 따라 수많은 모나드형으로 실제 모나드 인터페이스를 구현한 형식을 만들어 사용하는 것이라고 한다.


하지만 무릇 모나드형이 반드시 지켜야할 인터페이스는 존재하는데 그는 아래와 같다.

  1. 형식 생성자 : 모나드형을 생성한다.
  2. 단위 함수: 어떤 형식의 값을 모나드에 삽입한다. 모나드에서는 이를 of라는 함수로 명명한다고.
  3. 바인드 함수: 연산을 서로 체이닝 한다. 함수자의 fmap에 해당하는 것
  4. 조인 연산: 모나드의 자료구조의 계층을 flatten하는 것. 모나드 반환 함수를 다중합성 할 때 중요하다 -> 이는 functor의 한계점 중 하나를 극복하는 것.



모나드는 특정한 목적에 맞게 활용하고자 하는 많은 연산을 보유하는게 보통이라것 이 책에서 제시한 인터페이스는 전체 API의 극히 일부분에 불과한 최소규격입니다.

그럼에도 모나드 자체는 추상적이고 실질적 의미는 없고, 실제 형식으로 구현되어야 비로소 빛을 발한다고 한다.

FP에서 많이 쓰는 모나드형 몇가지만 있으면 엄청난 중복코드를 제거 및 무수히 많은 일을 해낼 수 있다고 한다.
(EX. Maybe, Either, IO 등)




본격적으로 FP의 정수라고 불리는 모나드를 학습해보았고, 지금껏 개발하며 마주했던 많은 문제들에 대한 다양한 상상력을 자극하는 내용들로 가득했다. 다음 챕터가 매우 기다려진다.

profile
가용한 시간은 한정적이고, 배울건 넘쳐난다.

1개의 댓글

comment-user-thumbnail
2023년 8월 14일

유익한 자료 감사합니다.

답글 달기