본문 바로가기

FrontEnd

소프트웨어 합성 : 리듀서(reducer)

반응형

reducer를 이용한 소프트웨어 합성에 대해 좀 더 알아봅니다.

다음 글의 번역입니다. https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d

 

Reduce (Composing Software)

Note: This is part of the “Composing Software” series (now a book!) on learning functional programming and compositional software…

medium.com

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)

What is Reducer?

함수형 프로그래밍에서 일반적으로 사용되는 Reduce(일명: fold, accumulate) 유틸리티는
반복이 완료되고 누적된 값이 반환될 때까지 리스트를 반복하고 누적 값과 목록의 다음 항목에 함수를 적용할 수 있습니다.
많은 유용한 함수들을 reduce로 구현할 수 있습니다.
아이템 컬렉션에 대해 사소하지 않은 로직을 수행하는 가장 우아한 방법입니다.
 
Reduce는 reducer 함수와 초기값을 취하여 누적된 값을 반환합니다.
Array.prototype.reduce()의 경우 초기값 리스트가 this에 의해 제공되므로 인수 중 하나가 아닙니다.
array.reduce(
  reducer: (accumulator: any, current: any) => any,
  initialValue: any
) => accumulator: any
배열의 원소의 합을 구해봅시다.
[2, 4, 6].reduce((acc, n) => acc + n, 0); // 12
배열의 각 요소에 대해 리듀서가 호출되어 누적값와 현재 값을 전달합니다.
리듀서의 역할은 현재 값을 어떻게든 누적 값으로 "fold"하는 것입니다. (누적값으로 연산을 적용해 접음)
접는 방법을 지정하는 것이 리듀서 함수의 목적입니다.
리듀서는 새 누적 값을 반환하고 reduce()는 배열의 다음 값으로  이동합니다.
리듀서는 초기 값이 필요할 수 있으므로 대부분의 구현은 초기 값을 매개변수로 사용합니다.
이 합산 리듀서의 경우 리듀서가 처음 호출될 때 acc는 0에서 시작합니다(두 번째 매개변수로 .reduce()에 전달한 값).
리듀서는 0 + 2(2는 배열의 첫 번째 요소)인 2를 반환합니다.
다음 호출의 경우 acc = 2, n = 4이고 리듀서는 2 + 4(6)의 결과를 반환합니다.
마지막 반복에서 acc = 6, n = 6이고 감속기는 12를 반환합니다.
반복이 완료되었으므로 .reduce()는 최종 누적 값인 12를 반환합니다.
이 경우 익명의 reduce 함수를 전달했지만 이를 추상화하고 이름을 지정할 수 있습니다.
const summingReducer = (acc, n) => acc + n;
[2, 4, 6].reduce(summingReducer, 0); // 12
일반적으로 reduce()는 왼쪽에서 오른쪽으로 작동합니다.
JavaScript에는 오른쪽에서 왼쪽으로 작동하는 [].reduceRight()도 있습니다.
즉, .reduceRight()를 [2, 4, 6]에 적용하면 첫 번째 반복에서 n의 첫 번째 값으로 6을 사용하고 2로 끝납니다. (<-)

Reducer는 다재다능 합니다.

리듀서는 다재다능합니다. reduce를 사용하여 map(), filter(), forEach() 및 기타 많은 흥미로운 것들을 정의하는 것은 쉽습니다.

 Map :

const map = (fn, arr) => arr.reduce((acc, item, index, arr) => {
  return acc.concat(fn(item, index, arr));
}, []);
map의 경우 누적 값은 원래 배열의 각 값에 대한 새 요소가 있는 새 배열입니다.
새 값은 전달된 매핑 함수(fn)를 arr 인수의 각 요소에 적용하여 생성됩니다.
반복 대상 배열 arr의 항목에 fn을 호출하고
accumulator 배열 acc와 concat하여 새 배열을 리턴합니다. 

Filter :

const filter = (fn, arr) => arr.reduce((newArr, item) => {
  return fn(item) ? newArr.concat([item]) : newArr;
}, []);

Fliter는 조건부 함수를 사용하고
아이템이가 조건부 검사를 통과하면 조건부로 새 배열에 현재 값을 추가한다는 점을 제외하고는
map과 거의 동일한 방식으로 작동합니다.
(fn(item)이 true를 반환함).

 

Filter는 조건부 함수를 사용하고 항목이 조건부 검사를 통과하면 조건부로 새 배열에 현재 값을 추가한다는 점을 제외하고는
map과 거의 동일한 방식으로 작동합니다(fn(item)이 true를 반환함).

 

Map, Filter 예시는 데이터 리스트의 항목에 함수를 적용하고 결과를 누적 값으로 접습니다.
하지만 데이터가 함수 목록이라면 어떻게 될까요?

 

Compose :

Reduce는 함수를 합성하는 편리한 방법이기도 합니다.
함수 합성을 기억하십시오: 함수 f를 x의 결과, 즉 합성 f에 적용하려는 경우 f .  g 를 의미합니다.

f(g(x))

Reduce를 사용하면 해당 프로세스를 추상화하여 여러 함수를 합성할 수 있으므로
다음을 나타내는 함수를 쉽게 정의할 수 있습니다.

f(g(h(x)))

그렇게 하려면 reduce를 반대로 실행해야 합니다.
즉, 왼쪽에서 오른쪽이 아니라 오른쪽에서 왼쪽입니다.
고맙게도 JavaScript는 .reduceRight() 메서드를 제공합니다.

Pipe :

compose()는 내부에서 외부로, 즉 수학 표기법 의미에서 컴포지션을 표현하려는 경우에 좋습니다.
그러나 그것을 일련의 사건의 발생 순서로 생각하고 싶다면 어떻게 해야 할까요?

숫자에 1을 더한 다음 두 배로 늘리고 싶다고 상상해 보십시오. `compose()`를 사용하면 다음과 같습니다.
const add1 = n => n + 1;
const double = n => n * 2;
const add1ThenDouble = compose(
  double,
  add1
);
add1ThenDouble(2); // 6
// ((2 + 1 = 3) * 2 = 6)
문제가 보이시나요?

첫 번째 단계는 마지막에 나열되어 있으므로
함수 실행 순서를 이해하려면 목록의 맨 아래에서 시작하여 맨 위로 뒤로 이동해야 합니다.
오른쪽에서 왼쪽 대신 왼쪽에서 오른쪽으로 접을 수 있습니다.
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
이제 다음과 같이 add1ThenDouble()을 작성할 수 있습니다.
const add1ThenDouble = pipe(
  add1,
  double
);add1ThenDouble(2); // 6
// ((2 + 1 = 3) * 2 = 6)
이것은 때때로 거꾸로 작성하면 다른 결과를 얻기 때문에 중요합니다.
const doubleThenAdd1 = pipe(
  double,
  add1
);doubleThenAdd1(2); // 5
나중에 compose() 및 pipe()에 대한 자세한 내용을 살펴보겠습니다.
지금 이해해야 하는 것은 reduce()가 매우 강력한 도구이며 꼭 배워야 한다는 것입니다.
reduce를 사용하는 것이 매우 까다로워지면 일부 사람들은 따라가기가 어려울 수 있습니다.

리덕스에 관하여

Redux의 중요한 상태 업데이트 로직을 설명하는 데 사용되는 "리듀서"라는 용어를 들어본 적이 있을 것입니다.
이 글을 쓰는 시점에서 Redux는 React와 Angular(후자는 ngrx/store를 통해)를 사용하여 구축된
웹 애플리케이션을 위한 가장 인기 있는 상태 관리 라이브러리/아키텍처입니다.

Redux는 리듀서 함수를 사용하여 애플리케이션 상태를 관리합니다.
Redux 스타일의 리듀서는 현재 상태와 액션 객체를 가져와서 새로운 상태를 반환합니다.

reducer(state: Any, action: { type: String, payload: Any}) => newState: Any
Redux에는 염두에 두어야 할 몇 가지 리듀서 규칙이 있습니다.
  1. 매개변수 없이 호출된 리듀서는 유효한 초기 상태를 반환해야 합니다.
  2. 리듀서가 액션 타입을 처리하지 않을 경우에도 상태를 반환해야 합니다.
  3. Redux 리듀서는 순수 함수여야 합니다.
합계 리듀서를 액션 객체를 접는 Redux 스타일 리듀서로 다시 작성해 보겠습니다.
const ADD_VALUE = 'ADD_VALUE';const summingReducer = (state = 0, action = {}) => {
  const { type, payload } = action;  switch (type) {
    case ADD_VALUE:
      return state + payload.value;
    default: return state;
  }
};​

 

Redux의 멋진 점은 리듀서가 [].reduce()를 포함하여
리듀서 함수 서명을 존중하는 모든 reduce() 구현에 연결할 수 있는 표준 리듀서라는 것입니다.
즉, 액션 객체의 배열을 만들고 접어 동일한 액션이 스토어에 전달된 경우와 동일한 상태를 나타내는 상태의 스냅샷을 얻을 수 있습니다.
const actions = [
  { type: 'ADD_VALUE', payload: { value: 1 } },
  { type: 'ADD_VALUE', payload: { value: 1 } },
  { type: 'ADD_VALUE', payload: { value: 1 } },
];
actions.reduce(summingReducer, 0); // 3
이는 Redux 스타일의 리듀서의 단위 테스트를 매우 간단하게 만듭니다.

결론 :

당신은 reduce가 매우 유용하고 다재다능한 추상화라는 것을 보기 시작해야 합니다.
확실히 맵이나 필터보다 이해하기가 조금 더 까다롭지만
함수형 프로그래밍 유틸리티 벨트의 필수 도구입니다.
다른 많은 훌륭한 도구를 만드는 데 사용할 수 있는 도구입니다.
반응형