Partial Application을 통한 가변 인자 튜플 타입 구현

DatQueue·2022년 9월 6일
0
post-thumbnail

지난 포스팅에서 우린 "가변 인자 튜플 타입(Variadic Tuple Types)"에 관해 알아보았다. 이번 포스팅에선 해당 개념에 "Partial Application"이란 구조를 결합시켜 실용적인 쓰임에 대해 알아보고자 한다.

포스팅에 들어가기에 앞서 "Partial Application"에 관해 낯설다면 해당 포스팅을 먼저 보고오는 것을 권한다. ⬇ ⬇
https://velog.io/@from_numpy/Curry-Partial-Application

실용적 예제 -- Partial Application

그럼 “Partial Application”을 구성하는 partialCall이라는 함수를 통해 조금 더 실용적인 튜플 예제를 알아보자.

코드 베이스 -- partialCall


우리가 이전 포스팅 마지막에서 언급하였던 “함수의 매개변수를 부분적으로 적용하여 새로운 함수를 반환하는 함수” 를 조금 풀어서 설명하자면 partialCall는 매개변수로 들어온 함수(f)의 매개변수를 일부 적용한 다른 함수를 리턴하는 함수이다.

아마 위의 설명을 읽고 한번만에 이해하기란 쉽지 않을 것이다. 본인도 사실 써놓고 저걸 누군가에게 이해해보라는 것은 농담이지 않을까 싶다.


다음의 코드를 한번 보면 조금 더 이해가 편하지 않을까.

function partialCall(f , ...headArgs) {   // 매개변수로 들어온 함수 f
  // 그 함수 f의 매개변수를 일부 적용한 다른 함수 리턴
	return (...tailArgs) => f(...headArgs , ...tailArgs);  
}

자, 그럼 해당 코드 틀을 참조하여 본격적으로 다음 코드를 알아보자. 이번 포스팅에서 가장 핵심이면서 "주"가 되는 코드라 생각한다.

type Arr = readonly unknown[]; // any 대신 unknown을 취해서 조금 더 타입을 엄격히 한다.

function partialCall<T extends Arr, U extends Arr, R>(
  f : (...args : [...T , ...U]) => R ,
  ...headArgs : T
) {
  return (...tailArgs : U) => f(...headArgs , ...tailArgs);
}

위 코드를 보는 순간 굉장히 난해해 보일 것이다. 하지만 해당 코드를 자바스크립트로 컴파일 하는 순간 그 위의 가장 먼저 작성한 코드가 되는 것을 눈치챘을 것이다.


해당 코드를 파헤쳐보기 전, 먼저 생각해 볼 것이 있다.

(1) 앞서 우린 tail, concat함수를 통해 ( 이전 포스팅 참고 ) TypeScript 4.0부터 스프레드 연산자(나머지 매개변수)에서 제네릭 타입을 사용할 수 있다는 사실을 얻을 수 있었고,

(2) “Partial Application” 기반의 partialCall함수는 “함수의 매개변수를 부분적으로 적용하여 새로운 함수를 반환하는 함수” 라는 것 또한 알게 되었다.

해당 두 키워드를 바탕으로 위의 코드를 해석할 수 있어야 한다. 아무 생각없이 그냥 “뭐… 이런 코드구나…” 하는 순간 우리의 현 주제를 망각할 수 있다.


(1)의 핵심은 무엇이었을까?

스프레드 연산자에서 제네릭 타입을 도입함으로써 별도의 오버로딩 없이 타입 추론을 통해 다양한 타입을 런타임에서 결정할 수 있었다.

partialCall함수 또한 이에 해당한다. partialCall의 매개변수로 받은 함수 f의 매개변수로 ...args를 받음과 동시에 [...T , ...U]를 타입으로 두어 매개변수의 타입을 추론할 수 있게 되는 것이다.

(2)의 핵심은 무엇이었을까?

새로운 함수를 리턴하는데 있어, 함수의 매개변수를 부분적으로 적용한다는 것이다. 이것은 “Partial Application”의 원칙이고 이를 통해 우린 “지연 평가”를 할 수 있었다.


위 두 가지 키워드를 바탕으로 다시 위의 코드를 분석해보자.

type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
  f : (...args : [...T , ...U]) => R ,
  ...headArgs : T
) {
  return (...tailArgs : U) => f(...headArgs , ...tailArgs);
}

가장 바깥을 둘러싸는 함수 partialCall의 매개변수 f...headArgs, 그리고 해당 partialCall이 리턴하는 함수 f의 매개변수 ...tailArgs. 이렇게 3개의 매개변수가 있다.

우리는 이 3개의 매개변수를 사용하는데 있어 “부분적으로” 적용한 것을 볼 수 있다.

( 다시 한번 언급하지만 위 밑줄의 내용에 관해 깊게 일일이 설명하진 않겠다. 가장 위에 표시해 둔 "Partial Application"의 기본 개념내용을 꼭 읽고 오길 바란다. )


Partial Application의 필요성


만약 코드를 다음과 같이 작성한다면 어떨까?

type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
  f : (...args : [...T , ...U]) => R ,
  ...headArgs : T,   // Error!
  ...tailArgs : U // ...tailArgs를 밖으로 뺌
) {
  return () => f(...headArgs , ...tailArgs);
}

우리가 코드에서 사용하고자 하는 3개의 매개변수를 굳이 나누지 않고 한번에 partialCall의 매개변수로써 두는 것이다. 그리고 사실은 저렇게 먼저 매개변수를 가장 상위의 함수에 선언하는 것이 더 익숙할 것이다.

하지만, 효율성을 따지기도 전에 위의 코드는 예상했듯이 에러를 띄울것이다.

...headArgs가 자연스럽게 매개변수의 마지막 자리가 아닌 위치로 오게 되며 자바스크립트로 컴파일될 시 에러를 발생시킨다. 나머지 매개변수는 항상 마지막에 있어야 하기 때문이다.


우린 이러한 문제점을 바로 "Partial Application"을 통한 로직으로 해결할 수 있는 것이다. 아래와 같이 말이다.

type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
  f : (...args : [...T , ...U]) => R ,
  ...headArgs : T
) {
  return (...tailArgs : U) => f(...headArgs , ...tailArgs);
}

어떻게 이 말을 받아들일지는 모르겠지만 조금 더 와닿게 말하자면 "Partial Application"은 함수의 인자의 일부를 미리 전달한다고 보면 된다.
그리고 매개변수로 둔 함수를 여러 번 (혹은 원하는 만큼의) 리턴하는 과정을 통해 더 적은 수의 인자로 함수를 실행시킬 수 있다.

이것은 굉장한 강점이자 효율적인 코드 작성이라 할 수 있다.


위의 내용은 모든 언어에 해당하는 "프로그래밍적 특성"이라면 타입스크립트의 "타입적 측면"에서도 나름의 강점을 보인다.

partialCall함수가 받는 인자가 어떤 타입이어야 하는지, 리턴하는 타입이 어떤 타입이어야 하는지 더 편하게 파악할 수 있다.


아래 코드를 통해 위의 두 가지 내용을 모두 확인해보자.
type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
 f : (...args : [...T , ...U]) => R ,
 ...headArgs : T
) {
 return (...tailArgs : U) => f(...headArgs , ...tailArgs);
}

// 해당 'foo'가 partialCall 함수안에서 'f'로 쓰이게 될 것이다.
const foo = (x:string , y:number , z:boolean) => {};

//not working !! -- Error

//Argument of type 'number' is not assignable to parameter of type 'string'.
const f1 = partialCall(foo , 100);
//Expected 4 arguments , but got 5.
const f2 = partialCall(foo , "hello" , 100 , true , "oops");

//working !!
const f3 = partialCall(foo , "hello" );

f3(123,true); // assignable (o)

//Expected 2 , but got 0. Arguments of the rest parameter 'tailArgs' were not provided.
f3();
//Argument of type 'string' is not assignable to parameter of type 'boolean'.
f3(123 , "hello");

복잡하게 생각할 것 없다. 우리는 먼저 매개변수의 타입이 정해져있는 foo함수를 만든다.

const foo = (x:string , y:number , z:boolean) => {};

해당 foo함수는 partialCall함수를 호출하는 과정에 있어 partialCall의 첫 번째 매개변수로 들어가게 된다.

//Error
//Argument of type 'number' is not assignable to parameter of type 'string'.
const f1 = partialCall(foo , 100);
//Expected 4 arguments , but got 5.
const f2 = partialCall(foo , "hello" , 100 , true , "oops");

//Assignable
const f3 = partialCall(foo , "hello" );

직접 본인의 에디터에 입력하면 알 수 있겠지만 f1f2은 위의 주석에서 작성한 문구의 이유로 에러를 띄운다. 어떻게 위와 같은 에러를 발생시킨 것일까?

여기서 잠깐 vsCode 에디터의 도움을 받아보자. 호출 부의 partialCall에 마우스를 갖다대어 해당 함수의 정의에 대해 알아보자. 즉, 매개변수를 서로 다르게 기입한 각각의 partialCall에 대해 알아볼 필요가 있다.


먼저, 첫 번째 partialCall(foo , 100)부터 알아보자.

(물론 두 번째 매개변수로 100이 온 것 부터가 에러긴 하다.)

제네릭 매개변수인 T 즉, ...headArgsx:string이 되고, y:numberz:boolean...tailArgs로써 제네릭 매개변수 U에 할당된다.

“Partial Application” 구조를 통해 배웠듯이, partialCall(foo , 100)를 받은 변수 f1또한 함수로써 표현할 수 있다. 그리고 함수 f1은 위에서도 볼 수 있겠지만

const f1: (y:number , z:boolean) => void

다음과 같이 나타낼 수 있다. 그리고 해당 f1함수의 매개변수인 y:number , z:boolean은 우리가 선언하였던 ...tailArgs : U에 할당된다.


그럼, 두 번째 partialCall(foo , "hello" , 100 , true , "oops")를 알아보자.

첫 번째 매개변수로 foo가 들어간 것은 동일하지만 처음 호출 구문과는 다르게 ...headArgsfoo의 모든 매개변수 (string , number , boolean)을 전부 넣어주었다. 물론 매개변수로 “oop”를 넣어주어 할당될 매개변수를 초과시켜 에러를 발생시키긴 했지만 말이다.

아무튼 ...headArgs 즉, 제네릭 타입 T에 모든 값들이 들어가게 되면서 ...tailArgs 즉 , U는 텅텅비게 되었다. 마찬가지로 위에서 볼 수 있듯이 f2는 아래와 같다.

const f2 : () => void

더이상 f2 부분에서 평가할 수 있는 값은 없는 것이다.

f3은 볼 필요도 없다. f1은 사실, 즉 첫 번재 호출부문은 에러이고, 이것을 제대로 호출한 것이 f3이다. 두 번째 매개변수로 string이 와야하기 때문이다.


마지막으로 따로 만들어본 호출구문을 알아보자. 해당 호출구문은 아래와 같다.

const f4 = partialCall(foo , "hello" , 100);

이 경우는 어떨까?

“hello”와 100이 headArgs로 할당되고, 하나 남은 매개변수 타입 boolean...tailArgs로 들어가게 됨과 동시에 또 다른 리턴 함수 f4의 매개변수 타입으로써 쓰인다. 그리고 f4는 아래와 같다.

const f4 : (z: boolean) => void

이렇게 우린 함수 호출 부분에서 “매개변수”를 어떻게 처리하느냐에 따라 “Partial Application”에서 함수 매개변수의 타입과 리턴되는 부분의 타입을 다룰 수 있다.

위의 코드를 통해 앞서 프로그래밍적 특성으로 언급하였던 "매개변수로 전달하는 함수를 통해 더 적은 인자로 함수를 실행 " 한다는 말이 어떠한 뜻인지 이해가 갈 것이라 생각한다.


생각정리

이전 포스팅에선 "가변 인자 튜플"이란 무엇인가에 대해 알아보았고 이번 포스팅에선 "가변 인자 튜플"과 "Partial Application"이란 두 개념을 결합해 조금 더 실용적인 "타입스크립트에서의 튜플"의 쓰임에 관해 알아보았다.

가변 인자 튜플에 관해 이해하는 것도 쉽지 않았지만 "Partial Application"이란 생소한 함수형 프로그래밍 개념까지 결합해 이해하려니 더욱 난해하고 어려웠다. 그에 따라 이번 블로그 포스팅에서 어떻게 해야 누군가에게 더 쉽고 유익하게 이 내용을 전달할 수 있을지도 많은 고민이 따랐다.

이번 포스팅은 먼저 학습한 시간보다 이렇게 포스팅 작성을 하면서 누군가에게 더 쉽게 전달해주고자 머리를 쓴 것이 오히려 더 '나'의 지식을 채워준 것 같다. 뜬금없지만, 다시 한번 블로그 작성에 적극 찬성(?)하게 되는 계기였다.

타입스크립트의 "가변 인자 튜플"에 초점을 맞추고 싶었지만 "Partial Application"과 함께 꼭 다루고 싶어서 누군가는 이전 포스팅들에 대비해 내용이 산으로 갔다고 생각할지도 모른다. 그렇지만 이렇게라도 새로운 키워드들을 접해보는 경험이 중요하고 소중하다 생각한다. 단지 글을 잘 못쓰는(?) 본인의 잘못이 있을 뿐이다...

이 포스팅을 읽게 되고, 혹시 의문이나 문제점 또는 틀린 내용이 있다면 아낌없는 가르침 바랍니다.(!)

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

0개의 댓글