[FP] 부분 적용 vs 커링

yongkini ·2024년 10월 17일
0

Functional Programming

목록 보기
11/21
post-thumbnail

부분 적용과 커링을 비교해보자

What's the difference between them ?

커링

: 일단 Currying or curry는 내가 평소에 자주 쓰는 스킬이기도 하다. 하지만, 이번에 부분 적용에 대해 제대로 이해하면서 내가 결국 부분 적용을 쓰고 싶었던건데 커리를 쓰고 있었던거구나 싶기도 했다. 하지만, 각각의 장단점이 있기 때문에 그리고 서로 유사한 측면이 있기 때문에 커리를 쓰는게 나쁘진 않았다. 오히려 좀 더 함수나 로직을 세분화할 수 있다는 점은 장점인 것 같다.

그래서 커링이 뭔가하면,
const generateValidateFn = curry((validateFn: ValidateFn, message: string) => ({
  validateFn,
  message,
}));

위 코드는 내가 이번에 Input 태그에서 유효성 검사를 다룰 때 썼던 커리 함수이다. 이 함수의 목적은 일단 유효성 검사를 하는 함수를 먼저 넣어주고, 그 다음에 그 함수의 결과에 따라 보여줄 에러 메시지를 두번째 매개변수로 넣어준다. 그러면, input value가 바뀔 때마다 해당 함수를 실행해서 메시지를 보여줄지 안보여줄지를 리턴하게 된다.

그러면 저렇게 커리를 쓰는 장점이 뭘까. 일단 내가 저 함수를 가져다 쓴 예시를 통해 살펴보면,

export const validateItsEmptyFn = generateValidateFn(isNotEmptyString);

이런식으로 가져다 썼는데, 여기서 isNotEmptyString

// 공백을 모두 제거
export const removeBlank = (value: string) => {
  return value.replace(/ /g, '');
};


export const isNotEmptyString = (value: string | number) => removeBlank(String(value)) !== '';

위와 같은 함수이다. 간단하게 문자열에서 공백을 제거하고 유효한 컨텐츠가 하나도 없을 때를 체킹하기 위한 함수이다. 결론적으로, 먼저 isNotEmptyString 이 함수를 파라미터로 넣어주고 validateItsEmptyFn 이 함수, 즉, 아직 메시지를 받지 못한 이 함수를 export 해서 유연하게 쓸 수 있는거다. 어떻게 유연하게 쓰는가 하면,

<ImageInput
   :rules="[validateItsEmptyFn('이미지를 업로드해주세요.')]"             
/>
<TextInput
   :rules="[validateItsEmptyFn('텍스트를 입력해주세요.')]"             
/>
<NumberInput
   :rules="[validateItsEmptyFn('숫자를 입력해주세요.')]"             
/>

이런식으로 하나의 함수에 메시지만 변경해서 재사용할 수 있다. 물론 이런 함수 재사용은 꼭 커링을 통해서만 할 수 있는건 아니다. 하지만, 다른 함수에서도 각각의 세분화된 역할을 가진 함수들을 가져다 쓰려면 커링이 꽤나 유용하다.

const generateValidateFn = curry((validateFn: ValidateFn, message: string) => ({
  validateFn,
  message,
}));

const checkRangeOfWeight = (value: number) => value >= 0 && value <= 1;

const validateStrength = testRegex(/^[0-9]+(\.[0-9]+)?$/);
const validateKeywordName = testRegex(/^[a-zA-Z0-9()[\].,\s\\]+$/);

export const checkFileType = (file: File) => checkIsValidFileType(ALLOWED_FILE_TYPES, file);

export const validateItsEmptyFn = generateValidateFn(isNotEmptyString);
export const validateWeightRange = generateValidateFn(checkRangeOfWeight);
export const validateItsNumber = generateValidateFn(validateStrength);
export const validateKeywordNameFn = generateValidateFn(validateKeywordName);

물론 이건 내가 적용한 예시고, 완벽하게 효율적이다?하는 코드는 세상에 없기에 내 프로젝트 내에서 그리고 내가 설계한 내용 상으로는 저렇게 쓰는게 재사용성 측면에서 좋았기 때문에 예시로 가져와봤다. currying은 내가 느낀 바로는 partial 보다 재사용성이 '강제적으로' 좀 더 좋을 수 밖에 없다?라는 특징을 갖는다. 아래는 Claude를 통해 질문한 둘의 차이점인데, 실제로 그런 내용이 있었다.

Key differences:

Argument handling:

Partial application: Can pre-fill any number of arguments in any position.
Currying: Always handles one argument at a time, in order.


Resulting function:

Partial application: Results in a function that takes all remaining arguments at once.
Currying: Results in a chain of single-argument functions.


Flexibility:

Partial application: More flexible in which arguments you can pre-fill.
Currying: More rigid, but can lead to more reusable and composable functions. // 이부분이 내가 느낀바와 유사한 것 같다.

Partial(or 부분 적용)

: Partial은 책에서 보면 뭔가 복잡해 보이는데 커링보다 심플한 개념이고, 더 flexible 한 기능이다. Currying을 쓰면서 계속 한개의 argument만 넣어야 된다는게 살짝 아쉽긴 했지만, 앞서 말했듯이 그게 재사용성을 강제로 높여준다는 측면에서 장점이기도 했어서 그냥 쓰곤 했는데, Partial은 나름의 재사용성을 가져가면서도(이 부분에서는 그렇게 강제성을 띄지 않기 때문에) 유동적으로 argument 개수 혹은 넣어줘야하는 argument의 위치 등을 결정할 수 있다. 따라서, currying과 partial의 가장 큰 차이점은(눈에 띄는 혹은 알아채기 쉬운) currying은 순차적으로 argument를 하나하나씩 넣어서 최종적으로 모든 arguments가 채워졌을 때 함수가 activated 된다는 것이고, Partial은 순차적이지도 않고, 하나하나씩 넣을 필요도 없이 개발자가 원하는 스타일대로 argument에 구멍(?)을 뚫어놓고 필요할 때마다 뚫린 구멍에 argument를 채워넣어서 activate 하면 된다는 차이점이 있다.

그럼 이제 Partial의 예시를 살펴보자.
const generateValidateFn = _.partial(
  (validateFn: ValidateFn, message: string) => ({
    validateFn,
    message,
  }),
  _.partial.placeholder,
  '유효성 검사 결과가 올바르지 않습니다.',
);

아까는 커링으로 만들었던 이 함수를 이번엔 partial로 만들어봤다(이번엔 lodash-es의 메서드 중에 하나은 partial를 사용했다). currying의 경우

export const validateItsEmptyFn = generateValidateFn(isNotEmptyString);

이런식으로 일단 validateFn에 해당 하는 파라미터를 하나 넣어주고(반드시 순차적으로 하나씩 넣어야된다), 그 다음에

<TextInput
   :rules="[validateItsEmptyFn('텍스트를 입력해주세요.')]"             
/>

이런식으로 마지막 message 파라미터를 넣어주면 그제서야 activate 돼서 결과값을 리턴한다. 하지만, partial의 경우 curry처럼 순차적으로 그리고 하나씩 argument를 넣어줄 필요가 없다.

const generateValidateFn = _.partial(
  (validateFn: ValidateFn, message: string) => ({
    validateFn,
    message,
  }),
  _.partial.placeholder,
  '유효성 검사 결과가 올바르지 않습니다.',
);

이걸 다시보면, 일단 순차적으로 할필요가 없다는 측면에서 먼저 유효성 검사 메시지를 확정시켜줬다. 보면 가운데 validateFn 파라미터 쪽에는 _.partial.placeholder를 넣어줬는데, lodash-es에서 제공하는 placeholder로 첫번째 파라미터를 일단 패스하고 두번째 파라미터를 일단 먼저 채우기 위해 사용한다. 그래서 generateValidateFn 현재 이 상태이다.

const generateValidateFn = curry((validateFn: ValidateFn, message: string) => ({
  validateFn,
  message: '유효성 검사 결과가 올바르지 않습니다.',
}));

그럼 여기서 실제로 함수를 activate하고 값을 리턴받으려면 나머지 하나 남은 파라미터를 넣어주면 된다.

export const validateItsEmptyFn = generateValidateFn(isNotEmptyString);

그러면 activate되고 결과값이 나온다.

여기서는 argument를 하나하나 비순차적으로 넣어주는 예시를 보여줬는데, 이렇게 개수 상관없이 원하는 argument를 pre-filled 해놓고 쓸 수 있다.

const generateValidateFnForImage = _.partial(
  (type:string, validateFn: ValidateFn, message: string) => ({
    validateFn,
    message,
  }),
  'image',
  _.partial.placeholder,
  '유효성 검사 결과가 올바르지 않습니다.',
  
);

이미지 태그를 위한 것

const generateValidateFnForImage = _.partial(
  (type:string, validateFn: ValidateFn, message: string) => ({
    validateFn,
    message,
  }),
  'text',
  _.partial.placeholder,
  '유효성 검사 결과가 올바르지 않습니다.',
  
);

text 태그를 위한 것

const generateValidateFnForImage = _.partial(
  (type:string, validateFn: ValidateFn, message: string) => ({
    validateFn,
    message,
  }),
  'number',
  _.partial.placeholder,
  '유효성 검사 결과가 올바르지 않습니다.',
  
);

number 태그를 위한 것.

이렇게 2개의 arguments를 pre-filled 해놓고 마지막에 그냥 가운데 validateFn만 채워주면 된다. 예시를 위해 가져와본거라 굳이 이렇게 쓸일(로직상)이 있을진 모르겠다.

이렇게도 활용할 수 있다 Partial 편

: Partial은 특정 함수를 유연하게 커스텀할 수 있다. A, B, C라는 매개변수를 넣어줘야하는 함수 D가 있을 때, 이 함수를 내 입맛에 맞게 혹은 조금 더 편하게 혹은 기능을 좀 더 한정해서 쓰고 싶을 때 Partial을 이용하면 좋다. 예를 들어서,
Array.prototype.slice 를 사용할 때 매개변수를 하나만 넣어서 배열의 앞에서 부터 매개변수 개수 만큼의 값을 slice하도록 쓰고 싶다고 해보자. 이런식으로 쓰는거다

const array = [1,2,3];
console.log(array.slice(2)); // [1,2]

원래의 slice 로직대로면 저렇게 값을 넣어주면 [3] 이 리턴된다. 이유는 하나만 넣게 되면 원래 slice의 파라미터는 from, to 순서로 받게돼 있어서 from에 2가 들어가서 index 2부터 끝까지를 슬라이싱해서 [3]이 리턴되는거다.

const array = [1,2,3];
console.log(array.slice(2)) // [3]

그러면 Partial을 이용해서 커스텀 메서드를 만들어보자. 물론 저 Array.prototype.slice를 굳이 가져다 쓸 필욘 없지만(새로 만들면 되지만) 심플하게 로직을 빌려서 커스텀 해보자(그게 Partial을 쓰는 이유이기도 하고)

  Array.prototype.customSlice = _.partial(Array.prototype.slice, 0);
  
  const array = [1, 2, 3];
  console.log(array.customSlice(2)); // [1,2]

하지만 이렇게 쓰면 만약 버전 업데이트에 의해서 실제로(그럴일은 없겠지만) customSlice가 만들어진다면 문제가 생길 수 있고(에러 처리를 하면 되긴 하겠지만), 이렇게 전역 객체에 들어있는 핵심 자료형을 확장하는거에 대한 리스크를 감수해야 한다는 이슈가 있다. 어쨌든, 이런식으로 언어의 핵심을 확장하는 기능도 혹은 용도로 쓰인다는 점.

Result

: 결론적으로 나는 커링을 먼저 쓰기 시작했고, Partial은 커링이랑 비슷한걸로만 알고 굳이 쓰진 않았다(프로젝트 내에서). 근데, 쓰면서 커링의 단점?이자 장점이 약간 불편하다는 생각이 들었는데, 마침 Partial이 그 부분을 완벽하게 해결해준다는 점에서 커링과 부분적용을 적당히 하이브리드로 써주면 좋을 것 같다는 생각이 들었다.

: 추가로, 커링, 부분 적용 둘다 함수형 프로그래밍을 하는 데에 있어서 '함수의 합성을 단순화' 해준다는 측면에서 이점이 크다. 애초에 이걸 알아보기 시작한 계기 혹은 이 부분을 공부하기 전에 어떤 인트로? 같은 부분에서 나온 내용이 이거였다. 내 이전 포스팅 중에 튜플 관련 포스팅이 있었을텐데, 그부분과 연결시켜 생각하면 된다. 매개 변수의 개수가 많아질수록 복잡성이 늘어나는데, 이러한 복잡성을 낮추기 위한 방법으로 튜플 등의 자료 구조를 쓰는 방법이 있고, 커링, 부분 적용처럼 아예 받는 매개변수 자체를 컨트롤 하는 방법도 있다. 인수가 적은 함수가 인수가 많은 함수보다 다루기 쉽다 라는 간단한 원리에 입각한 접근이었고, 이론적인 측면에서는 이정도 선에서 이해하고, 실전에서 쓰면서 장점을 느끼는게 제일 좋은 것 같다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글