transducer를 이용한 효율적인 데이터 프로세싱을 배워봅시다.
다음 글의 번역입니다. https://medium.com/javascript-scene/transducers-efficient-data-processing-pipelines-in-javascript-7985330fe73d
Transducer는 합성 가능한 고차 리듀서입니다.
리듀서를 입력으로 사용하고 다른 리듀서를 반환합니다.
- 간단한 함수 합성 적용 가능
- 여러 작업이 있는 대규모 컬렉션에 효율적: 파이프라인의 작업 수에 관계없이 컬렉션을 한 번만 반복합니다.
- 모든 열거(enumerable) 가능한 소스(예: 배열, 트리, 스트림, 그래프 등)에 대해 변환 가능
- Transducer 파이프라인을 변경하지 않고 지연 또는 즉시 평가에 모두 사용 가능
리듀서는 여러 입력을 단일 출력으로 접습니다.
"fold"는 다음과 같이 단일 출력을 생성하는 거의 모든 이진 연산으로 대체될 수 있습니다.
// Sums: (1, 2) = 3
const add = (a, c) => a + c;// Products: (2, 4) = 8
const multiply = (a, c) => a * c;// String concatenation: ('abc', '123') = 'abc123'
const concatString = (a, c) => a + c;// Array concatenation: ([1,2], [3,4]) = [1, 2, 3, 4]
const concatArray = (a, c) => [...a, ...c];
왜 리듀서는 합성이 안되나요?
일반적인 리듀서는 두 개의 인수를 예상하고 단일 출력 값만 반환하기 때문에 합성할 수 없으므로
출력을 간단히 다음 리듀서의 입력에 연결할 수 없습니다.
즉, 타입이 정렬되지 않습니다.
f: (a, c) => a
g: (a, c) => a
h: ???
f: reducer => reducer
g: reducer => reducer
h: reducer => reducer
왜 트랜스듀서를 사용하나요?
const friends = [
{ id: 1, name: 'Sting', nearMe: true },
{ id: 2, name: 'Radiohead', nearMe: true },
{ id: 3, name: 'NIN', nearMe: false },
{ id: 4, name: 'Echo', nearMe: true },
{ id: 5, name: 'Zeppelin', nearMe: false }
];
const isNearMe = ({ nearMe }) => nearMe;
const getName = ({ name }) => name;
const results = friends
.filter(isNearMe)
.map(getName);
console.log(results);
// => ["Sting", "Radiohead", "Echo"]
- 배열에서만 동작합니다.
- 네트워크 구독이나 친구의 친구와의 소셜 그래프에서 들어오는 잠재적으로 무한한 데이터 스트림은 어떻습니까?
- 배열에서 점 연결 구문을 사용할 때마다 JavaScript는 체인의 다음 작업으로 이동하기 전에 완전히 새로운 중간 배열을 만듭니다. 2,000,000명의 "친구" 목록을 살펴봐야 하는 경우 1~2배 정도 속도가 느려질 수 있습니다.
- 트랜스듀서를 사용하면 중간 컬렉션을 구축하지 않고도 전체 파이프라인을 통해 각 친구를 스트리밍할 수 있으므로 많은 시간과 메모리 변동을 절약할 수 있습니다.
- 점 연결 구문을 사용하면 .filter(), .map(), .reduce(), .concat() 등과 같은 표준 작업의 다양한 구현을 빌드해야 합니다.
- 배열 메서드는 JavaScript에 내장되어 있습니다만,
- 사용자 지정 데이터 타입을 만들고 처음부터 모든 메서드를 작성하지 않고 여러 표준 작업을 지원하려면 어떻게 해야 할까요?
- 트랜스듀서는 잠재적으로 모든 데이터 타입과 함께 작동할 수 있습니다.
- 트랜스듀서를 한 번 작성하고 트랜스듀서를 지원하는 모든 곳에서 사용하십시오.
이것이 트랜스듀서로 어떻게 나타나는지 봅시다.
이 코드는 아직 동작하지 않지만 따라하면 이 변환기 파이프라인의 모든 부분을 직접 구축할 수 있습니다.
const friends = [
{ id: 1, name: 'Sting', nearMe: true },
{ id: 2, name: 'Radiohead', nearMe: true },
{ id: 3, name: 'NIN', nearMe: false },
{ id: 4, name: 'Echo', nearMe: true },
{ id: 5, name: 'Zeppelin', nearMe: false }
];
const isNearMe = ({ nearMe }) => nearMe;
const getName = ({ name }) => name;
const getFriendsNearMe = compose(
filter(isNearMe),
map(getName)
);
const results2 = toArray(getFriendsNearMe, friends);
배경 및 어원 (Background and Etymology)
별로 중요하지 않은것 같은데 길게 설명해서 내용을 대부분 쳐냄
앞으로 해당 글에서 말하는 트랜스듀서는 고차 리듀서를 의미합니다.
데이터 흐름 프로그래밍 :
트랜스듀서에 대한 음악적 비유
즉, 신호 흐름은 다음과 같을 수 있습니다. 화살표가 변환기 사이의 전선이라고 상상해보십시오.
[ Source ] -> [ Mic ] -> [ Filter ] -> [ Mixer ] -> [ Recording ]
보다 일반적인 용어로 다음과 같이 표현할 수 있습니다.
[ Enumerator ]->[ Transducer ]->[ Transducer ]->[ Accumulator ]
배열에서 map 메서드를 사용한 적이 있다면 트랜스듀서처럼 작동하는 것을 경험했을 것입니다.
예를 들어 일련의 숫자를 두 배로 늘리려면 다음을 수행합니다.
const double = x => x * 2;
const arr = [1, 2, 3];
const result = arr.map(double);
이 예에서 배열은 반복 가능한 개체입니다.
map 메소드는 원래 배열을 반복하고 각 요소에 2를 곱하는 처리 단계인 double을 통해 해당 요소를 처리한 다음
결과를 새 배열에 누적합니다.
const double = x => x * 2;
const isEven = x => x % 2 === 0;const arr = [1, 2, 3, 4, 5, 6];const result = arr
.filter(isEven)
.map(double)
;console.log(result);
// [4, 8, 12]
const double = x => x * 2;
const isEven = x => x % 2 === 0;
const arr = [1, 2, 3, 4, 5, 6];
const tempResult = arr.filter(isEven);
const result = tempResult.map(double);
console.log(result);
// [4, 8, 12]
- 풀: 느긋한 평가
- 푸시: 즉시 평가
pull API는 소비자가 다음 값을 요청할 때까지 기다립니다.
push API는 소스 값을 열거하고 가능한 한 빨리 튜브를 통해 푸시합니다.
트랜스듀서는 푸시거나 풀이거나 상관하지 않습니다.
트랜스듀서는 동작 중인 데이터 구조를 인식하지 못합니다.
그들은 단순히 새로운 값을 축적하기 위해 전달한 리듀서를 호출합니다.
reducer = (accumulator, current) => accumulator
transducer = reducer => reducer
혹은 다음과 같이 작성할 수 있습니다.
transducer = ((accumulator, current) => accumulator) => ((accumulator, current) => accumulator)
일반적으로 대부분의 트랜스듀서는 부분 적용을 통해 특성화해야 합니다.
예를 들어 map 트랜스듀서는 다음과 같습니다.
map = transform => reducer => reducer
혹은 좀 더 구체화하면 다음과 같습니다.
map = (a => b) => step => reducer
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const map = f => step =>
(a, c) => step(a, f(c));
const filter = predicate => step =>
(a, c) => predicate(c) ? step(a, c) : a;
const isEven = n => n % 2 === 0;
const double = n => n * 2;
const doubleEvens = compose(
filter(isEven),
map(double)
);
const arrayConcat = (a, c) => a.concat([c]);
const xform = doubleEvens(arrayConcat);
const result = [1,2,3,4,5,6].reduce(xform, []); // [4, 8, 12]
console.log(result);
const map = f => step =>
(a, c) => step(a, f(c));
이렇게 사용할 수 있습니다.
const double = x => x * 2;
const doubleMap = map(double);
const step = (a, c) => console.log(c);
doubleMap(step)(0, 4); // 8
doubleMap(step)(0, 21); // 42
const filter = predicate => step =>
(a, c) => predicate(c) ? step(a, c) : a;
const compose = (...fns) => x => fns.reduceRight((y, f) => f(y), x);
const isEven = n => n % 2 === 0;
const double = n => n * 2;
const doubleEvens = compose(
filter(isEven),
map(double)
);
이 함수 또한 트랜스듀서를 반환할 것입니다.
즉, 트랜스듀서에 결과를 누적하는 방법을 알려주기 위해 최종 step 함수를 제공해야 함을 의미합니다.
const arrayConcat = (a, c) => a.concat([c]);
const xform = doubleEvens(arrayConcat);
이 호출의 결과는 호환되는 모든 reduce API에 직접 전달할 수 있는 표준 리듀서입니다.
두 번째 인수는 reduce의 초기 값을 나타냅니다. 이 경우 빈 배열 입니다.
const xform = compose(
map(inc),
filter(isEven)
);
into([], xform, [1, 2, 3, 4]); // [2, 4]
트랜스듀서는 위에서 아래로 합성됩니다.
트랜스듀서 규칙
트랜스듀서의 상호운용을 위한 규칙이 있습니다.
- 초기화:
- 초기 누적 값이 주어지지 않으면 트랜스듀서는 step 함수를 호출하여 작동할 유효한 초기 값을 생성해야 합니다.
- 값은 empty 상태를 나타내야 합니다.
- 예를 들어 배열을 누적하는 accumulator는 인수 없이 단계 함수가 호출될 때 빈 배열을 제공해야 합니다.
- 조기 종료 :
- 트랜스듀서를 사용하는 프로세스는 reduce된 누적 값을 받으면 확인하고 중지해야 합니다.
- 또한 중첩된 redyce를 사용하는 트랜스듀서 스텝 함수는 reduced된 값이 발생했을 때 이를 확인하고 전달해야 합니다.
- 완료(선택 사항):
- 일부 변환 프로세스는 완료되지 않지만
- 완료 가능한 경우 완료 함수를 호출하여 최종 값 및/또는 플러시 상태를 생성하고
- stateful한 트랜스듀서는 누적된 리소스를 정리하고 잠재적으로 하나의 최종 값을 생성하는 완료 연산을 제공해야 합니다.
초기화
맵 작업으로 돌아가 초기화(empty) 법칙을 따르는지 확인합니다.const map = f => step => (a = step(), c) => (
step(a, f(c))
);
이 규칙을 기억하십시오: 인수 없이 호출될 때 리듀서는 항상 reduce에 대한 유효한 초기(empty) 값을 반환해야 합니다.
일반적으로 React 또는 Redux용 감속기를 포함하여 모든 리듀서 함수에 대해 이 규칙을 따르는 것이 좋습니다.
조기 종료
예: http 메시지는 "요청" 또는 "응답"이라는 컨테이너로 래핑되며 이
러한 컨테이너 유형에는 상태 코드, 예상 메시지 길이, 권한 부여 매개변수 등과 같은 정보를 제공하는 헤더가 있습니다.
const reduced = v => ({
get isReduced () {
return true;
},
valueOf: () => v,
toString: () => `Reduced(${ JSON.stringify(v) })`
});
- 타입 리프트: 타입 내부의 값을 가져오는 방법(예: 이 경우 reduced된 함수)
- 타입 식별: 값이 감소된 값인지 테스트하는 방법(예: isReduced getter)
- 값 추출: 타입형에서 값을 다시 가져오는 방법(예: valueOf())
완료
"완료 단계에서 reduction 상태의 트랜스듀서는 중첩된 단계에서 reduced된 값을 본 적이 없는 한 중첩된 트랜스듀서의 완료 함수를 호출하기 전에 상태를 플러시해야 합니다. 이 경우 보류 상태는 폐기되어야 합니다." ~ Clojure 트랜스듀서 문서
다시 말해, 이전 함수가 reduce가 완료되었다는 신호를 보낸 뒤 플러시할 상태가 더 있는 경우 완료 단계는 이를 처리할 시간입니다.
이 단계에서 선택적으로 다음을 수행할 수 있습니다.
- 값을 하나 더 보내기(대기 상태를 플러시).
- 보류 상태 삭제
- 필요한 상태 정리 수행
트랜스듀싱
다양한 타입의 데이터를 변환하는 것이 가능하지만
트랜스듀서는 프로세스를 일반화할 수 있습니다.
// import a standard curry, or use this magic spell:
const curry = (
f, arr = []
) => (...args) => (
a => a.length === f.length ?
f(...a) :
curry(f, a)
)([...arr, ...args]);
const transduce = curry((step, initial, xform, foldable) =>
foldable.reduce(xform(step), initial)
);
const concatArray = (a, c) => a.concat([c]);
const toArray = transduce(concatArray, []);
// Manual transduce:
const xform = doubleEvens(arrayConcat);
const result = [1,2,3,4,5,6].reduce(xform, []);
// => [4, 8, 12]
// Automatic transduce:
const result2 = toArray(doubleEvens, [1,2,3,4,5,6]);
console.log(result2); // [4, 8, 12]
트랜스듀서 프로토콜
JavaScript 트랜스듀서 프로토콜은 상황을 약간 다르게 처리합니다.
함수 arity를 사용하는 대신 JavaScript 트랜스듀서는 트랜스듀서를 가져와 트랜스듀서를 반환하는 함수입니다.
리듀서는 세 가지 메서드가 있는 개체입니다.
- init : 누산기에 대한 유효한 초기 값을 반환합니다(일반적으로 다음 step()을 호출하기만 하면 됨).
- step : 변환을 적용합니다(예: map(f): step(accumulator, f(current))).
- result : 트랜스듀서가 새 값 없이 호출되면 완료 단계를 처리해야 합니다(트랜스듀서가 stateful 하지 않은 경우 step(a)).
참고: JavaScript의 트랜스듀서 프로토콜은 각각 @@transducer/init, @@transducer/step 및 @@transducer/result를 사용합니다.
일부 라이브러리는 트랜스듀서를 자동으로 래핑하는 transducer() 유틸리티를 제공합니다.
const map = f => next => transducer({
init: () => next.init(),
result: a => next.result(a),
step: (a, c) => next.step(a, f(c))
});
기본적으로 대부분의 변환기는 init() 호출을 파이프라인의 다음 변환기로 전달해야 합니다.
전송 데이터 타입을 모르기 때문에 유효한 초기 값을 생성할 수 없기 때문입니다.
- reduced : 접은 값에 대해 항상 true인 부울 값입니다.
- value : reduced된 값
결론
트랜스듀서는 기본 데이터 타입을 redyce할 수 있는 합성 가능한 고차 리듀서 입니다.
배열을 사용한 메서드 체인보다 훨씬 더 효율적인 코드를 생성하고
중간 집계를 생성하지 않고 잠재적으로 무한한 데이터 세트를 처리합니다.
실무에서는 RxJS혹은 Ramda를 사용합니다.
일반적으로 데이터 스트림을 구독, 처리하고 싶을 때 트랜스듀서를 찾습니다.
이러한 경우 RxJS에서 파이프 가능한 연산자를 찾습니다.
RxJS 파이퍼블 연산자는 일반 변환기처럼 작동하지만 리듀서에서 리듀서로 매핑하는 대신 옵저버블에서 옵저버블로 매핑합니다.
맵, 필터, 청크, 테이크 등과 같은 여러 연산을 결합해야 할 때마다
트랜스듀서를 사용하여 프로세스를 최적화하고 코드를 읽기 쉽고 깨끗하게 유지합니다.
한 번 사용해 보세요
'FrontEnd' 카테고리의 다른 글
javascript 프로젝트에 d.ts를 이용하여 타입스크립트 도입하기 (0) | 2022.09.22 |
---|---|
스토리북 개발팀이 알려주는 컨테이너 / 프리젠터 패턴 - Context API를 이용해 의존성 주입하기 (0) | 2022.09.22 |
소프트웨어 합성 : 리듀서(reducer) (0) | 2022.09.20 |
AST 활용 1편 : ESLint console.log 체크 플러그인 만들기 (0) | 2022.09.19 |
[React] React.cloneElement 사용 사례 (0) | 2022.09.15 |