본문 바로가기

FrontEnd

소프트웨어 합성 : 트랜스듀서(Transducers)

반응형

transducer를 이용한 효율적인 데이터 프로세싱을 배워봅시다.

다음 글의 번역입니다. https://medium.com/javascript-scene/transducers-efficient-data-processing-pipelines-in-javascript-7985330fe73d

 

Transducers: Efficient Data Processing Pipelines in JavaScript

Note: This is part of the “Composing Software” series on learning functional programming and compositional software techniques in…

medium.com

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

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"]​
이와 같은 작은 리스트에는 문제가 없지만 몇 가지 잠재적인 문제가 있습니다.
  1. 배열에서만 동작합니다.
    • 네트워크 구독이나 친구의 친구와의 소셜 그래프에서 들어오는 잠재적으로 무한한 데이터 스트림은 어떻습니까?
  2. 배열에서 점 연결 구문을 사용할 때마다 JavaScript는 체인의 다음 작업으로 이동하기 전에 완전히 새로운 중간 배열을 만듭니다. 2,000,000명의 "친구" 목록을 살펴봐야 하는 경우 1~2배 정도 속도가 느려질 수 있습니다.
    • 트랜스듀서를 사용하면 중간 컬렉션을 구축하지 않고도 전체 파이프라인을 통해 각 친구를 스트리밍할 수 있으므로 많은 시간과 메모리 변동을 절약할 수 있습니다.
  3. 점 연결 구문을 사용하면 .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);
트랜스듀서는 로직을 시작하라고 명령하며, 처리할 데이터를 제공하기 전엔 아무 일도 하지 않습니다..
이것이 우리가 toArray()가 필요한 이유입니다.
변환 가능한 프로세스를 제공하고 변환기에 결과를 새 어레이로 변환하도록 지시합니다.
toArray()를 호출하는 대신 스트림, 옵저버블 또는 원하는 것으로 변환하도록 지시할 수 있습니다.
트랜스듀서는
숫자를 문자열에 매핑하거나
객체를 배열에 매핑하거나
배열을 더 작은 배열에 매핑하거나
{ x, y, z } -> { x, y, z }를 매핑하여 아무 것도 변경하지 않을 수 있습니다.
 
트랜스듀서는 또한 스트림 { x, y, z } -> { x, y }에서 신호의 일부를 필터링하거나
출력 스트림에 삽입할 새 값을 생성할 수도 있습니다. ex : { x, y, z } -> { xxx, y, yy, z, ,zz }
 
이 섹션에서는 "신호"와 "스트림"이라는 단어를 서로 바꿔서 사용할 것입니다.
내가 "스트림"이라고 말할 때 특정 데이터 타입을 말하는 것이 아닙니다.
단순히 0개 이상의 값 시퀀스 또는 시간에 따라 표현된 값 목록입니다.

배경 및 어원 (Background and Etymology)

별로 중요하지 않은것 같은데 길게 설명해서 내용을 대부분 쳐냄
앞으로 해당 글에서 말하는 트랜스듀서는 고차 리듀서를 의미합니다.
하드웨어 신호 처리 시스템에서 트랜스듀서는 마이크 트랜스듀서와 같이 한 형태의 에너지를 다른 형태의 에너지로 변환하는 장치입니다(예: 오디오 파동을 전기로 변환). 즉, 한 종류의 신호를 다른 종류의 신호로 변환합니다.
마찬가지로 코드 트랜스듀서는 한 시그널에서 다른 시그널로 변환합니다.
"트랜스듀서"라는 단어의 사용과 소프트웨어의 합성 가능한 데이터 변환 파이프라인의 일반적인 개념은
적어도 1960년대로 거슬러 올라갑니다.
컴퓨터 과학 초기의 많은 소프트웨어 엔지니어는 전기 엔지니어였습니다.
당시 컴퓨터 과학의 일반 연구는 종종 하드웨어와 소프트웨어 설계를 모두 다루었습니다.
따라서 계산 프로세스를 "트랜스듀서"로 생각하는 것은 특별히 새로운 것은 아닙니다.
프로그래밍적인 트랜스듀서 개념을 유명하게 한 것은 SICP라는 책입니다.

데이터 흐름 프로그래밍 :

소프트웨어 "절차"를 다른 노드의 입력에 연결된 출력이 있는 워커 노드의 방향 그래프로 설명할 수 있습니다.
절차를 배열과 배열 처리 연산 대신 모든 것을 연속적으로 실행되는 대화형 프로그램 무한 루프 안의 값의 스트림으로 표현할 수 있습니다.
각 값은 매개변수 입력에 도달할 때 각 노드에서 처리됩니다.
 
 
앞으로 해당 글에서 말하는 트랜스듀서는 고차 리듀서를 의미합니다.
 

트랜스듀서에 대한 음악적 비유

악기를 사용하여 노래를 녹음하려면 공기 중의 음파를 전선의 전기로 변환하는 일종의 물리적 변환기(예: 마이크)가 필요합니다.
그런 다음 전선을 사용하려는 신호 처리 장치로 라우팅합니다.
예를 들어, 일렉트릭 기타에 디스토션을 추가하거나 음성 트랙에 리버브를 추가합니다.
결국 이 다양한 사운드 모음은 최종 녹음을 나타내는 단일 신호(또는 채널 모음)를 형성하기 위해 함께 집계되고 혼합되어야 합니다.

즉, 신호 흐름은 다음과 같을 수 있습니다. 화살표가 변환기 사이의 전선이라고 상상해보십시오.
[ Source ] -> [ Mic ] -> [ Filter ] -> [ Mixer ] -> [ Recording ]​

보다 일반적인 용어로 다음과 같이 표현할 수 있습니다.
[ Enumerator ]->[ Transducer ]->[ Transducer ]->[ Accumulator ]​
음악 제작 소프트웨어를 사용한 적이 있다면 일련의 오디오 효과를 떠올리게 될 것입니다.
이것은 트랜스듀서에 대한 좋은 직관이지만
숫자, 개체, 애니메이션 프레임, 3D 모델 또는 소프트웨어로 표현할 수 있는 모든 것에 훨씬 더 일반적으로 적용될 수 있습니다.

배열에서 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]
그러나 드론의 원격 측정 데이터와 같이 잠재적으로 무한한 숫자 스트림을 필터링하고 두 배로 늘리고 싶다면 어떻게 해야 할까요?
배열은 무한할 수 없으며 배열 처리의 각 단계에서는 단일 값이 파이프라인의 다음 단계를 통과하기 전에 전체 배열을 처리해야 합니다.
동일한 제한은 새 어레이를 생성해야 하고 컴포지션의 각 단계에 대해 새 컬렉션을 반복해야 하기 때문에
배열 메서드를 사용하는 컴포지션의 성능이 저하된다는 것을 의미합니다.
각각 데이터 스트림에 적용할 변환을 나타내는 두 개의 튜브 섹션과 스트림을 나타내는 문자열이 있다고 상상해 보십시오.
첫 번째 변환은 isEven 필터를 나타내고 다음 변환은 double map을 나타냅니다.
배열에서 완전히 변환된 단일 값을 생성하려면 먼저 첫 번째 튜브를 통해 전체 배열을 반복해야 하므로
두번째 튜브를 통해 하나의 아이템을 처리하기 전에 완전히 새로운 필터링된 배열이 생성됩니다.
첫 번째 값을 두 배로 늘리면 하나의 아이템의 결과를 읽기 전에 전체 배열의 아이템이 두 배가 될 때까지 기다려야 합니다.
따라서 위의 코드는 다음과 같습니다.
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는 소비자가 다음 값을 요청할 때까지 기다립니다.

JavaScript의 좋은 예는 제너레이터 함수에 의해 생성된 객체와 같은 Iterable입니다.
반환하는 반복자 객체에서 .next()를 호출하여 다음 값을 요청할 때까지 제너레이터 함수에서는 아무 일도 일어나지 않습니다.

push API는 소스 값을 열거하고 가능한 한 빨리 튜브를 통해 푸시합니다.

array.reduce() 호출은 푸시 API의 좋은 예입니다.
array.reduce()는 배열에서 한 번에 하나의 값을 가져와 감속기를 통해 푸시하여 다른 쪽 끝에 새 값을 생성합니다.
배열 축소와 같은 즉시 실행 프로세스의 경우
전체 배열이 처리될 때까지 배열의 각 요소에 대해 프로세스가 즉시 반복되어 그 동안 추가 프로그램 실행이 차단됩니다.

트랜스듀서는 푸시거나 풀이거나 상관하지 않습니다.
트랜스듀서는 동작 중인 데이터 구조를 인식하지 못합니다.

그들은 단순히 새로운 값을 축적하기 위해 전달한 리듀서를 호출합니다.

트랜스듀서는 고차 리듀서입니다. 리듀서를 가져와서 새 리듀서를 반환하는 리듀서 함수입니다.
Rich Hickey는 트랜스듀서를 프로세스 변환으로 설명합니다.
즉, 변환기를 통해 흐르는 값을 단순히 변경하는 것과는 대조적으로 트랜스듀서는 해당 값에 작용하는 프로세스를 변경합니다.
 
서명은 다음과 같습니다.
reducer = (accumulator, current) => accumulator
transducer = reducer => reducer

 

혹은 다음과 같이 작성할 수 있습니다.

transducer = ((accumulator, current) => accumulator) => ((accumulator, current) => accumulator)

일반적으로 대부분의 트랜스듀서는 부분 적용을 통해 특성화해야 합니다.

예를 들어 map 트랜스듀서는 다음과 같습니다.

map = transform => reducer => reducer

혹은 좀 더 구체화하면 다음과 같습니다.

map = (a => b) => step => reducer
즉, 맵 트랜스듀서는 매핑 함수(변환;transform이라고 함)와 리듀서(단계;step 함수라고 함)를 사용하고 새 리듀서를 반환합니다.
단계(step) 함수는 다음 단계에서 리듀서에 추가할 새 값을 생성할 때 호출하는 리듀서입니다.
 
몇 가지 나이브한 예를 살펴보겠습니다.
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);

 

많은 것을 이해해야 합니다.
분해해 보겠습니다. map은 일부 컨텍스트 내의 값에 함수를 적용합니다.
이 경우 컨텍스트는 트랜스듀서 파이프라인입니다.
대략 이렇습니다.
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

 

끝에 있는 함수 호출의 0은 리듀서의 초기 값을 나타냅니다.
step 함수는 리듀서로 간주되지만 데모 목적으로 하이재킹하고 콘솔에 로깅할 수 있습니다.
단계 함수가 사용되는 방식에 대해 단언해야 하는 경우 단위 테스트에서 동일한 트릭을 사용할 수 있습니다.
변환기는 함께 합성할 때 흥미로워집니다. 단순화된 필터 트랜스듀서를 구현해 보겠습니다.
const filter = predicate => step =>
  (a, c) => predicate(c) ? step(a, c) : a;
Filter는 predicate 함수를 취하고 predicate와 일치하는 값만 통과합니다.
그렇지 않으면 반환된 리듀서는는 변경되지 않은 accumulator를 반환합니다.
이 두 함수 모두 리듀서를 취하고 리듀서를 반환하기 때문에 간단한 함수 합성으로 이들을 합성할 수 있습니다.
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]
트랜스듀서를 사용한 프로그래밍은 매우 직관적입니다.
트랜스듀서를 지원하는 인기 있는 라이브러리에는 Ramda, RxJS 및 Mori가 있습니다.

트랜스듀서는 위에서 아래로 합성됩니다.

 

표준 함수 합성(f(g(x)))을 통해 트랜스듀서는 아래에서 위로/오른쪽에서 왼쪽이 아니라
위에서 아래로/왼쪽에서 오른쪽으로 적용합니다.
즉, 일반 함수 합성을 사용하면 compose(f, g)는 "g 다음에 f를 합성"을 의미합니다.
 
트랜스듀서는 합성 아래에 있는 다른 변환기를 감쌉니다.
즉, 변환기는 "나는 내 일을 하고 파이프라인에서 다음 트랜스듀서를 호출할 것입니다"라고 말합니다.
이는 실행 스택을 뒤집는 효과가 있습니다.
 
맨 위에는 f, 다음은 g, 다음은 h라고 표시된 종이 더미가 있다고 상상해 보세요.
각 시트에 대해 스택 상단에서 시트를 제거하고 새로운 인접 스택 상단에 놓습니다.
완료되면 시트에 h, g, f로 레이블이 지정된 스택이 생깁니다.
 
 

트랜스듀서 규칙

트랜스듀서의 상호운용을 위한 규칙이 있습니다.

  • 초기화:
    • 초기 누적 값이 주어지지 않으면 트랜스듀서는 step 함수를 호출하여 작동할 유효한 초기 값을 생성해야 합니다.
    • 값은 empty 상태를 나타내야 합니다.
    • 예를 들어 배열을 누적하는 accumulator는 인수 없이 단계 함수가 호출될 때 빈 배열을 제공해야 합니다.
  • 조기 종료 :
    • 트랜스듀서를 사용하는 프로세스는 reduce된 누적 값을 받으면 확인하고 중지해야 합니다.
    • 또한 중첩된 redyce를 사용하는 트랜스듀서 스텝 함수는 reduced된 값이 발생했을 때 이를 확인하고 전달해야 합니다.
  •  완료(선택 사항):
    • 일부 변환 프로세스는 완료되지 않지만
    • 완료 가능한 경우 완료 함수를 호출하여 최종 값 및/또는 플러시 상태를 생성하고
    • stateful한 트랜스듀서는 누적된 리소스를 정리하고 잠재적으로 하나의 최종 값을 생성하는 완료 연산을 제공해야 합니다.

초기화

맵 작업으로 돌아가 초기화(empty) 법칙을 따르는지 확인합니다.
물론 특별한 작업을 수행할 필요는 없습니다.
기본값을 생성하기 위해 step 함수를 사용하여 파이프라인으로 요청을 전달하기만 하면 됩니다.
const map = f => step => (a = step(), c) => (
  step(a, f(c))
);​

 

우리가 주목할 부분은 함수 서명의 = step()입니다.
(누적기)에 대한 값이 없으면 체인의 다음 감속기에 값을 생성하도록 요청하여 값을 생성합니다.
결국 파이프라인의 끝에 도달하고 (잘하면) 유효한 초기 값을 생성합니다.

이 규칙을 기억하십시오: 인수 없이 호출될 때 리듀서는 항상 reduce에 대한 유효한 초기(empty) 값을 반환해야 합니다.
일반적으로 React 또는 Redux용 감속기를 포함하여 모든 리듀서 함수에 대해 이 규칙을 따르는 것이 좋습니다.

조기 종료

reduce를 완료한 파이프라인의 다른 트랜스듀서에 신호를 보내는 것이 가능하며
이럴 경우 더 이상 값을 처리할 것으로 기대해서는 안됩니다.
reduced 값을 볼 때 다른 트랜스듀서는 컬렉션에 추가를 중지하기로 결정할 수 있으며
변환 프로세스(최종 step() 함수에 의해 제어됨)는 값에 대한 반복을 중지하기로 결정할 수 있습니다.
변환 프로세스는 reduced된 값을 수신한 결과로 한 번 더 호출할 수 있습니다.
위에서 언급한 완료 호출 입니다.
특별한 reduced된 누적 값으로 그 의도를 알릴 수 있습니다.
 
reduced된 값은 무엇입니까? reduced라는 특별 타입으로 누적 값을 래핑하는 것처럼 간단할 수 있습니다.
패키지를 상자에 포장하고 "Express" 또는 "Fragile"과 같은 메시지로 상자에 레이블을 지정하는 것과 같다고 생각하십시오.
이와 같은 메타데이터 래퍼는 컴퓨팅에서 일반적입니다.
예: http 메시지는 "요청" 또는 "응답"이라는 컨테이너로 래핑되며 이
러한 컨테이너 유형에는 상태 코드, 예상 메시지 길이, 권한 부여 매개변수 등과 같은 정보를 제공하는 헤더가 있습니다.
기본적으로 단일 값만 예상되는 여러 메시지를 보내는 방법입니다.
Reduced() 타입의(비표준) 예는 다음과 같습니다.
const reduced = v => ({
  get isReduced () {
    return true;
  },
  valueOf: () => v,
  toString: () => `Reduced(${ JSON.stringify(v) })`
});​
엄격하게 요구되는 부분들은 다음과 같습니다.
  • 타입 리프트: 타입 내부의 값을 가져오는 방법(예: 이 경우 reduced된 함수)
  • 타입 식별: 값이 감소된 값인지 테스트하는 방법(예: isReduced getter)
  • 값 추출: 타입형에서 값을 다시 가져오는 방법(예: valueOf())
toString()은 디버깅 편의를 위해 엄격하게 여기에 포함됩니다.
콘솔에서 타입과 값을 동시에 검사할 수 있습니다.

완료

"완료 단계에서 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)
);
transduce() 함수는 단계 함수(트랜스듀서 파이프라인의 마지막 단계), 누적 초기값, 트랜스듀서, foldable 초기 값을 취합니다.
foldable은 .reduce() 메서드를 제공하는 모든 객체입니다.
transduce()를 정의하면 배열로 변환하는 함수를 쉽게 만들 수 있습니다.
먼저 배열로 줄이는 리듀서가 필요합니다.
const concatArray = (a, c) => a.concat([c]);
이제 curried transduce()를 사용하여 배열로 변환하는 부분 적용 프로그램을 만들 수 있습니다.
const toArray = transduce(concatArray, []);
toArray()를 사용하면 두 줄의 코드를 하나로 바꿀 수 있으며 다음 외에도 많은 다른 상황에서 재사용할 수 있습니다.
// 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]

트랜스듀서 프로토콜

지금까지 커튼 뒤에 숨은 디테일이 있었지만 이제는 살펴봐야 할 때입니다.
트랜스듀서는 실제로 단일 함수가 아닙니다. 3가지 다른 기능으로 만들어졌습니다.
Clojure는 함수의 arity에 대한 패턴 일치를 사용하여 둘 사이를 전환합니다.
컴퓨터 과학에서 함수의 arity는 함수가 취하는 인수의 수입니다.
트랜스듀서의 경우 리듀서 힘수에 대한 두 가지 인수인 누산기 및 현재 값이 있습니다.
Clojure에서 이 모두는 선택 사항이며 인수가 전달되는지 여부에 따라 동작이 변경됩니다.
매개변수가 전달되지 않으면 함수 내부의 해당 매개변수 타입이 정의되지 않습니다.

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 개체는 다음 속성을 사용합니다(트랜스듀서 프로토콜에서 @@transducer/<name> 네임스페이스도 지정됨:
  • reduced : 접은 값에 대해 항상 true인 부울 값입니다.
  • value : reduced된 값

결론

트랜스듀서는 기본 데이터 타입을 redyce할 수 있는 합성 가능한 고차 리듀서 입니다.
배열을 사용한 메서드 체인보다 훨씬 더 효율적인 코드를 생성하고
중간 집계를 생성하지 않고 잠재적으로 무한한 데이터 세트를 처리합니다.

 

실무에서는 RxJS혹은 Ramda를 사용합니다.

일반적으로 데이터 스트림을 구독, 처리하고 싶을 때 트랜스듀서를 찾습니다.

이러한 경우 RxJS에서 파이프 가능한 연산자를 찾습니다.

RxJS 파이퍼블 연산자는 일반 변환기처럼 작동하지만 리듀서에서 리듀서로 매핑하는 대신 옵저버블에서 옵저버블로 매핑합니다.

 

맵, 필터, 청크, 테이크 등과 같은 여러 연산을 결합해야 할 때마다
트랜스듀서를 사용하여 프로세스를 최적화하고 코드를 읽기 쉽고 깨끗하게 유지합니다.
한 번 사용해 보세요

 

반응형