함수형 프로그래밍에서 부수효과(side effect)를 다루는 방법 1

Raymond Yoo·2023년 12월 11일
0
post-thumbnail

구체적인 예시를 통해서 함수형 프로그래밍에서
부수효과(side effect) 다루는 방법을 확인해보자.

split, parse, divide 내부구현

요구조건을 간단하게 설명하면 이렇다.
처음에 입력값으로 문자열을 입력받는다.
숫자 두 개를 고르고 가운데 쉼표(,)를 넣은 뒤에
하나의 문자열로 합치면 올바른 입력값이 된다.
이렇게 입력받은 문자열을 쉼표를 기준으로
두 개의 문자열로 나눈 뒤에
각각의 문자열을 숫자로 변환한 후에
이렇게 얻은 두 개의 숫자를 나눈 결과값을 반환한다.
최종적으로 출력타입은 실수 타입이 된다.

위의 요구조건을 만족하기 위해서는
세 단게의 태스크를 거쳐야 하므로
자연스럽게 세 개의 함수로 구현하게 된다.
split 함수에서 쉼표를 기준으로 문자열을 분리하고
parse 함수에서 두 개의 문자열을 숫자로 변환하고
divide 함수로 나누기 결과값을 얻어낸다.

split, parse, divide 함수 합성

앞에 세 개의 함수 split, parse, divide 는 서로 서로
입력타입과 출력타입이 서로 맞아떨어지기 때문에
split -> parse -> divide 형태로
세 개의 함수를 한 줄로 합성하는게 가능해진다.

앞쪽에 세 개의 함수
split, parse, divide 의 내부구현을 다시 보자.
이 세 개의 함수는 겉보기는 순수 함수 같지만
자세히 들여다보면 split, parse, divide 모두
부분 함수(partial function)이다.
split 함수의 입력 파라미터가 null 이라면
String.split() 메서드를 호출하는 부분에서 예외가 발생한다.
parse 함수의 입력 파라미터 문자열 쌍에서
둘 중에 하나라도 숫자가 아닌 문자가 섞여 있다면 예외가 발생한다.
divide 함수의 입력 파라미터 실수 쌍에서
분모가 되는 두번째 실수값이 0 이라면 예외가 발생한다.

예외처리 방법은 여러가지가 있지만
여기서는 Optional<T> 타입을 사용하기로 하자.
Optional<T> 는 T 타입의 값이 있거나 empty 상태이거나
둘 중의 하나의 값을 가지는 객체이다.
이 Optional 을 사용해서 위의 세 가지 함수의 타입을
에외처리를 완료한 상태로 변경해보자.

split, parse, divide 세 개의 함수에 예외처리 로직 추가

이제 위 세 개의 함수는 이런식으로 바뀐다.
split 함수는 입력타입이 String 이고
출력타입은 String 쌍의 Optional 이다.
parse 함수는 입력타입이 String 쌍의 Optional 이고
출력타입은 Double 쌍의 Optional 이다.
divide 함수는 입력타입이 Double 쌍의 Optional 이고
출력타입은 Double 의 Optional 이다.

split, parse, divide 세 개의 함수 호출 후 ifPresent() 확인 처리

Optional 을 사용하면 구현결과는 이런식으로 된다.
split, parse, divide 라는 세 개의 태스크를 실행하되
앞선 태스크가 성공했을때만 다음 태스크를 실행한다.
split 실행이 성공하면,
즉 if (split.isPresent() == true) 이면
다음 함수인 parse 를 실행한다.
parse 실행이 성공하면,
즉 if (parse.isPresent() == true) 이면
다음 함수인 divide 를 실행한다.
divide 실행이 성공하면,
즉 if (divide.isPresent() == true) 이면
최종 결과값을 반환한다.
이런식으로 구현하면 예외처리를 빈틈없이 완벽하게 구현한 것이다.
하지만 이것은 프로그래머들이 콜백헬(callback hell)이라고
부르는 것과 비슷한 결과를 불러왔다.
지금은 함수호출이 세 개 밖에 없으니 인덴트가 그다지 깊지 않지만
하나의 요구조건을 만족하기 위해서 필요한
중간 단계의 태스크가 많아질수록
이 죽음의 트라이앵글도 점점 커질 것이다.

더 이상 split, parse, divide 함수 합성은 불가능

이 그림을 보면 무엇이 달라졌는지 한 눈에 잘 보인다.
앞에서 split, parse, divide 함수의 타입이 딱딱 맞아떨어질 때는
세 개의 함수를 합성해서 하나의 함수인 것처럼
깔끔하게 한 줄로 만들 수 있었다.
그러나 지금은 그런 깔끔한 처리가 불가능하다.
왜냐하면 split, parse, divide 의 입력타입과 출력타입이
서로 일치하지 않기 때문이다.

앞의 코드에서 본 깊이 들여쓰기 되는 삼각형을 없애고
타입이 바뀐 split, parse, divide
세 가지 합수의 합성을 통해서
마치 하나의 함수를 호출한 것처럼 만드는 것이
함수형 프로그래밍이 추구하는 방향성이다.

kleisli arrow, fish operator

앞에서 등장한 함수들을 추상화시켜서 분석해보자.
함수 f 는 A -> M[B] 타입이고
함수 g 는 B -> M[C] 타입이다.
함수형 프로그래밍에서는
출력값이 M[B] 이고 입력값이 B 인
두 개의 함수 f, g 를 합성하는 함수를
kleisli arrow 라고 하고 >=> 와 같이 표시한다.
>=> 를 보면 물고기처럼 생겼기 때문에
fish oprator 라고도 부른다.

오른쪽에 타입명세를 살펴보자. 함수 f 와 g 를 합성하려면
M[B] 를 B 로 만들어줄 무언가를 찾아야 하는 것처럼 보인다.

Functor 다시 살펴보기

아니면 아예 다른 방향에서 접근해서
합성해서 얻어낸 함수 g o f 자체의 타입에만 집중해보자.
함수 g o f 의 타입은 A -> M[C] 이다.
그러면 중간단계는 어떻든지 상관없이
입력타입이 A 이고 마지막 출력타입이 M[C] 가 되게끔
하면 되지 않을까?

Functor 사용해서 flatten-map(g)-f 형태로 함수 합성

박스 안에 어떤 타입이 담겨있다고 할때
박스에서 안쪽에 저장된 내용물을
꺼내는 것을 flatten 이라고 해보자.
이 flatten 을 이용해서 위의 그림과 같이
f - map(g) - flatten 순서로 합성하고 나면
최종함수 (flatten o map(g) o f) 는 A -> M[C] 타입을 갖는다.
이런식으로 하면
앞에 있는 함수의 출력값이 박스 안에 있는 X 타입이고
뒤에 있는 함수의 입력값이 그냥 X 타입이어도
다시 한 줄로 깔끔하게 함수 합성을 할 수 있게 되었다.

Monad - f, g, map(g) - flatten, flatMap

프로그래밍에서는 map(g) 과 flatten 을
합성하는 과정이 자주 등장하는데
흔히 flatMap 이라고 줄여서 부른다.

오른쪽에 각 함수의 타입명세를 살펴보면
A 는 normal 타입이고 M 은 wrapper 타입이다.
M[A] 는 박스 안에 담겨있는 색깔있는 원이라고 생각하면 된다.
함수 f 는 A -> M[B] 타입이고
함수 g 는 B -> M[C] 타입이다.
map 함수는 (A -> B) -> (M[A] -> M[B]) 타입이다.
flatMap 함수는 (A -> M[B]) -> (M[A] -> M[B]) 타입이다.
함수 g >=> f 는 내부적으로
x -> flatMap(g)(f(x)) 를 호출하는 것과 같고
결과적으로는 처음에 살펴본 것처럼 A -> M[C] 타입이 된다.

flatMap 사용해서 개선한 split, parse, divide 함수 합성

지금까지 알아본 flatMap 을 적용해서
세 개의 함수 split, parse, divide 를 다시 합성해보면
위의 그림과 같은 형태가 된다.

이제 우리는 함수형 프로그래밍의 독특한 무기 두 가지
map 함수와 flatMap 함수 사용하는 법을 알게 되었다.
함수 이름이나 타입 명세는 구현하는 언어마다
맥락마다 프레임워크마다 다를 수 있지만
기본적인 원리는 동일할 것이다.
이 두 가지를 기억하면서 필요한 때마다 적절하게 선택해서 사용하면
자연스럽게 함수형 프로그래밍 원리를 적용해서
모든 부수효과를 더 잘 다룰 수 있게 된다.

방금 설명한 내용이
간단하게 말하면 Monad 를 코드에 적용한 결과이다.
물론 Monad 개념은 지금 이상의 미묘한 내용들 포함하지만
함수형 프로그래밍 관점에서 Monad 를
코드레벨에 적용한다면 이런식으로 된다.
Monad 는 flatMap 메서드를 가진 Functor 라고 말할 수 있곘다.

<참조>
유튜브 영상, No Nonsense Monad & Functor - The foundation of Functional Programming by César Tron-Lozai

profile
세상에 도움이 되고, 동료에게 도움이 되고, 나에게 도움이 되는 코드를 만들고 싶습니다.

0개의 댓글