티스토리 뷰
프로그래밍에서는 본질적인 데이터의 복잡성 때문에 논리적 정확성이 불가능할 때가 있습니다. 데이터를 추상화 시키는 것은 데이터의 단순한 표현을 만드는 데 도움이 되는 매우 유용한 도구입니다.
이를 위해 'Container' 를 만드는 방법이 있습니다. 'Container'는 오직 우리의 데이터만 가지고 있으며, 그 외에 다른 책임을 가지지 않습니다. 즉, OOP 처럼 'Container' 에 프로퍼티나 메소드를 제공하지 않습니다.
변수를 가져오고 Container 안으로 넣습니다. 그리고 Container 는 함수형 로직을 통과시키는 동안 변수를 안전하게 지키며 필요할 때 즉시적으로 변수를 가져올 수 있습니다. 따라서 Container 는 아래의 두 가지 책임이 있다는 것을 알 수 있습니다.
- 스스로 내부에 변수를 저장하고 있다.
- 오직 우리가 필요로 할 때 변수를 되돌려 준다
또한 절대 컨테이너 내부의 값은 변경되지 않습니다.
함수형 프로그래밍을 할 때 컨테이너는 함수형 구조의 기초에 기여하고 순수 함수형 에러 핸들링과 비동기 액션등 일반적인 기술들을 지원하므로 매우 강력합니다. 하지만 이 컨테이너라는 개념은 전혀 새로운 개념이 아닙니다 이미 JavaScript 를 사용하면서 많이 접해 봤을 개념입니다.
컨테이너에 대해 자세하게 살펴보기 전에, Functor - 펑터라 불리는 특별한 타입의 컨테이너에 대해 간단히 설명하자면 'map' 함수와 함께 사용되는 컨테이너 입니다. 물론 이 말고도 다른 특징이 있지만 아래에서 자세하게 다뤄보도록 하겠습니다.
Arrays
배열은 프로그래밍에서 항상 사용될 정도로 가장 일반적인 컨테이너입니다. 배열은 모든 데이터 추상화 한것 중 가장 단순하지만 강력합니다.
const arr = [ 8, 10, 23, 35, 54 ];
const b = a[1]; // 변수를 이렇게 꺼내옵니다.
arr.push(45) ❌
arr[1] = 45 ❌
// 원본 배열을 수정하는 것 대신 아래와 같이 새로운 배열을 만들어 사용하세요.
const arr2 = [ ...arr, 38, 52 ]
const even = filter(x => x%2 === 0, arr)
함수형 프로그래밍에서 배열의 값을 변경할 수 있는 메소드를 사용해서는 안됩니다. 값을 가져오기만 하거나 배열의 수정이 필요하다면 새로운 배열로 만들어야 합니다.
위에서 펑터는 'map' 함수를 사용할 수 있는 컨테이너라고 이야기 했습니다. 하지만 더 자세하게 이야기 하자면 펑터는 단항 함수로 '매핑'할 수 있는 컨테이너입니다.
여기서 '매핑'이란 컨테이너가 담고 있는 모든 데이터에 단항 함수를 적용하고 그 결과 값으로 다른 컨테이너를 반환하는 함수 (ex: fmap, map)로 처리할 수 있음을 의미합니다.
배열의 경우 이 특별한 함수를 'map' 함수라고 부릅니다.
The Map Function
배열의 Map 함수는 배열을 가져오고 특정한 함수를 모든 요소 하나하나에 적용하고 그 결과값으로 새로운 배열을 반환합니다.
[1,2,3,4].map(multiplyBy2)
//=> [2,4,6,8] 또는 map(multiplyBy2, [1,2,3,4])
//=> [2,4,6,8] where multiplyBy2 = x => x * 2 and map = (fn, arr) => arr.map(fn)
항상 map 으로부터 또 다른 배열을 가져오기 때문에 map 을 다시 연결 하여 여러 단항 함수를 매핑하는 과정을 수행하는 체인을 만들 수 있습니다.
[1,2,3].map(x => x * 3).map(x => x * 2).map(x => x / 6)
Map 함수는 이터레이터 함수 이상의 기능을 제공합니다. 값이 컨테이너 안에 있을 때 함수를 직접 적용 할 수 없으며 값이 변경 될 것으로 예상 할 수 있습니다. 예를 들어
const a = [1, 2, 3]
String(a) = ‘[1 ,2, 3]’
String(a) != [‘1’, ‘2’, ‘3’]
map 함수는 함수가 컨테이너 안의 값들에 접근할 수 있도록 해줍니다.
map(String, [1, 2, 3]) = [‘1’, ‘2’, ‘3’]
또한, map 함수는 절대 기존 컨테이너를 변경하지 않고 내부의 값에 따라 동작합니다.
map 은 컨테이너의 타입을 변경시키지 않지만 컨테이너가 담고 있는 내용의 타입은 변경할 수 있습니다.
컨테이너가 담고 있는 데이터의 타입은 바뀔 수 있습니다. 그리고 그 타입 변경을 map 함수의 정의를 통해 알 수 있습니다.
map :: (a -> b) -> [a] -> [b] or fmap :: (a -> b) -> F a -> F b
여기서 a, b 는 같은 타입일 수도 있고 다른 타입일 수도 있습니다.
이제 간단하게 살펴보면 map 함수가 a -> b 에서 함수를 가져오고 Fa -> Fb 에서 함수를 반환 하는 것을 볼 수 있습니다.
여기서 a -> b 는 a를 가져와 b를 반환하는 단항 함수를 나타냅니다.
multiplyBy2(3) = 6 // is a -> b as 3 -> 6
그리고 Fa -> Fb 는 a 가 들어있는 컨테이너를 가져오고 b 가 들어있는 컨테이너를 반환합니다.
multiplyArrBy2([1]) = [2] // is Fa -> Fb as [1] -> [2], F is []
이에 대한 자세한 내용은 아래 글을 참조해 주세요
Functions
모든 함수는 Functor 이기 때문에 컨테이너 이기도 합니다.
컨테이너는 데이터를 가지고 있지만 함수들은 순수한 로직만 포함하고 있기 때문에 어떻게 함수가 컨테이너가 될 수 있는지 의문이 들 수 있습니다.
잘 생각해보면 함수는 호출되었을 때 변수를 반환합니다. 그래서 어느 방법으로든 변수를 가지고 있어야 합니다. 오직 다른 컨테이너와 다른 차이점은 그 변수들이 동적으로 계산된다는 것입니다.
aFunction(45) // => 90 So aFunction gives the value 90, when it is passed 45
함수를 무한한 숫자의 배열처럼 생각해봅시다. 그리고 이 무한한 숫자 중 특정한 변수를 원한다면, 특정한 인자와 함께 함수를 호출해야 합니다.
배열이 인덱스가 전달되면 변수를 주는 것처럼 함수도 인자가 전달되었을 때 결과를 줍니다.
const a = [ 8, 10, 23, 35, 54 ]
const f = z => z * 2 a[1] = 10
f(2) = 4
배열은 그저 정해진 정수형 인덱스에 대한 결과값만 주지만, 함수는 어떠한 타입의 인자든 제한 없이 받을 수 있습니다. 또한 다른 함수를 인자를 받을 수도 있습니다.
함수는 무한한 값을 가진 컨테이너 입니다.
그럼 함수가 Functor 라면 map 함수도 가지고 있나요?
맞습니다. 함수에 대한 map 을 다음과 같이 정의할 수 있습니다.
const fnMap = (f, mappingFn) => (x => f(mappingFn(x)))
map 함수가 배열을 가져와 새로운 배열을 만드는 것과 마찬가지로 fnMap은 함수를 가져와서 그 함수의 결과에 다른 함수를 적용하고 두 함수를 결합하는 방식으로 새로운 함수를 반환합니다. 따라서 첫 번째 함수의 결과는 두 번째 함수의 인자가 됩니다.
이를 사용해보면 아래와 같습니다.
const multiplyBy6 = fnMap(multiplyBy2, multiplyBy3)
따라서 함수에도 map 함수가 있으며 한 함수를 다른 함수에 매핑하는 경우 두 함수를 융합하는 것입니다. 이를 Compose Function 이라고 부릅니다.
tl;dr
- multiplyBy2 함수는 인자로 주어진 값에 2를 곱하여 반환합니다.
- multiplyBy2 로부터 값을 꺼내거나 호출하기 전에 multiplyBy3 으로 매핑했습니다.
- 따라서 multiplyBy2 의 결과 값들은 3이 곱해집니다.
- 이제 multiplyBy2 를 호출 할 때마다. x 가 원본 값이면 x * 3 * 2 를 얻게 됩니다.
const fnMap = (f, mappingFn) => (x => f(mappingFn(x)))
함수는 데이터 추상화 타입인 배열과 같습니다. 그리고 함수는 데이터를 요구사항에 따라 연산하기만 합니다.
반대로 배열은 '[]' 를 사용하면 바로 결과를 알려주는 함수와 같다고 할 수 있습니다.
이 글을 통해 기억해야 할 중요한 점은 함수형 프로그래밍에서 절대로 데이터를 그대로 사용하지 말고 항상 컨테이너로 감싸서 사용해야 한다는 것 입니다.
마치며
함수형 프로그래밍의 Functor 는 컨테이너의 값을 매핑하여 또 다른 컨테이너를 만들어 낼 수 있는 특별한 컨테이너라는 사실을 알게 되었습니다. 사실 기존까지 매우 어렵거나 복잡한 개념이라고 막연한 두려움을 가지고 있었지만, 알고보니 이미 많이 사용되고 있던 개념이여서 싱겁게 느껴지기 까지 하는 것 같습니다.
요즘 영어로 된 문서를 번역하는 재미에 빠져 있어서 자꾸 번역 포스트만 작성하게 되는 것 같습니다. (영여공부도 하고 프로그래밍 공부도 하고 1석 2조..) 조만간 개인적인 이야기도 들고 오겠습니다.