JS - Currying


수학이나 컴퓨터 과학에서 currying이라는 기법이 있다.
function(a, b)와 같이 여러 개 인수를 갖는 함수를 functionT(a)(b)와 같은 형식으로 변환하는 것을 말한다.

JS

간단히

const sum = function(a, b) {
  return a + b;
}

와 같은 함수를 변형해본다.

우선 currying 과정을 담당하는 함수를 작성한다.

function currying(targetFunction) {
  return function f1(a) { // #1
    return function f2(b) { // #2
      return targetFunction(a, b); // #3
    };
  };
}

currying 함수의 인자로 어떤 대상함수를 넘겨주면,

  1. 임의의 인자 a를 받는 함수 f1을 리턴한다.
  2. f1은 임의의 인자 b를 받는 f2함수를 리턴한다.
  3. f2는 처음 currying 함수가 인자로 받았던 대상함수에 a와 b를 전달한 결괏값을 리턴한다.

함수 선언 과정을 분리하여

function currying(targetFunction) {
  function f1(a) {
    function f2(b) {
      return targetFunction(a, b); // #3
    }
    return f2; // #2
  }
  return f1; // #1
}

이런 식으로 작성하고, 각 반환 결과만 정리하면

  1. currying은 f1을 반환한다.
  2. f1은 f2를 반환한다.
  3. f2는 대상함수의 결괏값을 반환한다.

특이사항으로, currying함수와 f1함수는 함수 자체(f1, f2)를 반환하고, f2 함수는 함수의 결괏값(targetFunction(a, b)을 반환한다.

이제 currying 함수를 이용해 sum을 변환시키면

const curriedSum = currying(sum);
console.log(curriedSum(2)(3)); // 5

이런 식으로 사용할 수 있다.

일반적으로는 함수의 인자 개수와 상관없이 실행되도록 구현하고, 그에 더해 currying 된 호출과 그렇지 않은 호출 둘 다 작동되도록 구현된 라이브러리등을 사용하는 듯 하지만 우선 여기까지만 정리한다.

currying 참고#1 currying 참고#2

오딘프로젝트 - Git을 통한 오픈소스 기여

Accessibiliy 챕터를 공부하던 중 오타라고 판단되는 부분을 두 개 발견하였다.

변경 후보

  1. front end
    ...particularly when you come to front end testing,...
    front end 라는 표현이 front-end 혹은 frontend 여야 하지 않나 싶었다.
  2. pane
    ...starting from the Accessibility pane section...
    pane이란 단어를 몰랐고, 연결된 링크에서도 pane이란 단어를 찾지 못해 panel을 잘못 기입했다고 판단하였다.

작업 절차

  1. 세팅

    설명 비고
    원본 리포지토리를 내 깃헙 계정으로 복사 github웹 fork
    복사된 리포지토리를 로컬로 복사 git clone
    원본 리포지토리를 추가 remote로 등록 git remote add upstream git@github.com:TheOdinProject/curriculum.git
  2. 변경사항 작업

    설명 비고
    작업 브랜치 생성 및 이동 git switch -c fix/a11y-section-typo
    변경사항 적용 및 커밋 git add . && git commit -m "fix: fix typos"
  3. remote 업로드

    설명 비고
    upstream을 최신버전으로 업데이트 git fetch upstream
    upstream을 로컬 메인에 머지 git switch main && git merge upstream/main
    로컬 메인 브랜치를 작업브랜치로 머지 git switch fix/a11y-section-typo && git merge main
    작업브랜치를 내 리포지토리(origin)으로 푸시 git push origin fix/a11y-section-typo
    원본 리포지토리로 pull request 생성 github웹 create pull request

중간 결과

오딘포르젝트 풀리퀘스트 템플릿에 따라 풀리퀘스트 메세지를 작성한 후 요청 작업을 완료 하였다.
프로젝트 관리자중 한 명인 @wise-king-sullyman이 내 PR을 리뷰하였고,

  • 내가 알기로는 프로젝트 전체에서 front end, front-end 둘 중 어떤 단어를 써야하는지 따로 정해진 것은 없다. 혹시 프로젝트 전체적으로 단어를 통일할 필요를 느낀다면 새로운 issue를 만드는 것을 추천한다.
  • pane이란 단어는 원래 연결된 링크 원본인 Google에서 사용하던 단어인데, 현재는 tab으로 변경된 것 같다. 링크 및 단어 수정을 요청한다.

라는 리뷰를 받았다.
추가로 @mao-sz도 front end 단어 사용에 대한 리뷰를 해주었다.

  • 비록 어느 한 단어로 정해진건 아니지만, 맥락상 front end 라는 표현이 틀렸다고 보기는 어렵다.

front end를 frontend 혹은 front-end로 자연스럽게 느끼는건 순전히 주관적인 견해라고 판단하여 수정을 철회하였다.

PR 수정

PR을 수정하려면 내 작업 브랜치에서 변경된 커밋을 만들어 origin에 다시 푸시해주면 된다. 하지만 그동안 upstream main이 업데이트 되었으므로 작업 절차가 약간 변경 되었다.

  1. remote 업로드

    설명 비고
    upstream을 최신버전으로 업데이트 git fetch upstream
    upstream을 로컬 메인에 머지 git switch main && git merge upstream/main
    작업브랜치를 로컬 메인으로 리베이스 git switch fix/a11y-section-typo && git rebase main
    작업브랜치를 내 리포지토리(origin)으로 강제 푸시 git push --force-with-lease origin fix/a11y-section-typo
    원본 리포지토리로 pull request 생성 github웹 create pull request

작업 브랜치에서 merge main을 하면 fast-forward-merge가 되지 않고 새로운 커밋을 만드는 일반적인 merge가 진행 된다. 우선 merge 한 후 squash를 통해 커밋을 관리 할 수도 있을 것 같긴 하지만, 나는 rebase를 통해 커밋을 정리하기로 했다. 이렇게 생성된 커밋은 내용은 같아도 기존 커밋과 다른 해시를 갖고 있기 때문에 git push --force-with-lease 키워드를 통해 origin 브랜치의 기록을 덮어쓴다.

최종 결과

변경된 사항을 적용하여 PR을 수정한 결과 최종 승인을 받아 upstream/main에 내 브랜치가 머지되었다.
PR

사람과 컴퓨터가 돌아가면서 보드 위의 한 칸을 선택하는 기능을 구현한다.

사람이 선택할 때까지 코드가 그 이후로 진행되지 않고 멈춰야 한다.

const getSquareFromListener = () => {
  return new Promise((resolve, reject) => {
    main.addEventListener("click", function attackListener(e) {
      const square = e.target.closest(".square");
      if (!square) {
        reject(new Error());
      }
      resolve(square);
      main.removerEventListener("click", attackListener);
    });
  });
};                       
try {
  const playerSquare = await getSquareFromListener();
} catch (e) {
  console.error(e)
}

얼핏 보면 복잡하지만


i) Promise 객체를 리턴하는 함수를 만든다.
ii) Promise는 attackListener라는 함수를 main의 클릭 리스너로 등록한다.
iii) attackListener는 클릭된 DOM에서 가장 가까운 square를 찾아서 resolve를 통해 반환한다. 없으면 reject를 통해 에러를 반환하여 콘솔창에 출력한다. removeEventListener를 통해 자기 자신: attackListener을 main에 등록된 클릭 리스너에서 삭제한다.
iv) await 키워드를 이용해 함수를 호출한다.


라고 정리해 두면 언젠가 이해할 수 있을지도

특정 시점 이후 서로 다른 두 브랜치를 합치는 방법: merge vs rebase

Merge

0123

  • 주브랜치에서 git merge feat/branch 명령어를 입력한다.
  • Git이 두 브랜치의 공통 조상 커밋(Commit 1)과 각 브랜치의 최신 커밋(Commit 3, Commit 5)이 가리키고 있는 스냅샷을 토대로 새로운 스냅샷을 생성하고, 그 스냅샷을 가리키는 새로운 커밋(Commit 6)을 생성한다.
  • 병합된 브랜치를 삭제한다.

Rebase

012345

  • 작업중인 브랜치에서 주브랜치를 향해 커맨드를 입력한다: git rebase main
  • Git이 두 브랜치의 공통 조상 커밋을 기준으로, 현재 브랜치의 그 이후 커밋(Commit 2, Commit 3)을 임시파일에 저장한다.
  • 현재브랜치를 주브랜치의 최신 커밋으로 리셋한다(HEAD의 포인터를 Commit 5로 옮기고 작업 중인 파일까지 전부 바꾸는 하드 리셋과 유사한 동작).
  • 임시저장했던 커밋을 이어 붙인다(Commit 2', Commit 3').
  • 주브랜치로 체크아웃 한 뒤, 최신 커밋으로 fast-forwad merge 한다.
  • 병합된 브랜치를 삭제한다.

차이점

  • merge는 주브랜치, rebase는 작업브랜치에서 이루어진다.
  • merge는 모든 기록을 보존한 상태에서 새로운 커밋을 만들고, rebase는 작업브랜치의 기록을 떼어내어 주 브랜치에 새로운 커밋의 형태로 붙인다. 이때 떼어낸 커밋과 붙여진 커밋은 내용은 같지만 실제로는 다른 커밋이다.
# Exercise 4 - permutations

Write a function that takes in an empty array or an input array of an consecutive positive integers, starting at 1, and returns an array of all possible permutations of the original array

The integers will not repeat.

```javascript
permutations([1, 2, 3]); // [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
// An empty set has a single permutation, 0! = 1
permutations([]); // [[]]
```

 

문제:

1부터 시작하여 연속된 정수로 이루어져 있거나 빈 배열이 주어질 때, 가능한 모든 순열로 이루어진 배열을 반환하는 함수를 작성하여라.

const permutations = function (input) {
};

 

풀이:

- base case

재귀함수는 우선 베이스케이스를 잘 잡아야 한다.

다시 호출되는 인자도 배열이기 때문에 베이스 케이스도 배열을 반환해야 한다.

 

우선 반환할 빈 배열을 하나 생성한다.

let answer = [];

 

첫 번째 테스트 케이스는 빈배열을 인자로 준다.

test("1 possible permutation for a set containing 0 numbers", () => {
    expect(permutations([])).toEqual([[]]);
  });

 

이경우 가능한 순열은 [] 하나밖에 없으므로 [[]] 을 리턴해야 한다.

if (input.length === 0) {
  answer.push(input);
  return answer;
}

 

두 번째 테스트 케이스의 인자는 값이 하나만 있는 배열이다.

test("1 possible permutation for a set containing 1 number", () => {
    expect(permutations([1])).toEqual([[1]]);
  });

 

마찬가지로, 가능한 순열조합은 하나밖에 없으므로 그대로 배열에 담아 반환한다.

if (input.length === 1) {
  answer.push(input);
  return answer;
}

 

내가 이해하기로 베이스케이스는 재귀가 멈추는 조건이다. 빈 배열이 인자로 주어지는 것은 예외케이스에 가까운 것 같고, 그보다 배열에 요소가 단 하나만 있을 때, 재귀를 멈추고 인자를 그대로 담은 배열을 반환하여 실제로 내용을 채워 간다는 것을 이해하고 나서야 풀이 방향을 알 것 같았다.

 

어쨌든 코드 생긴 건 비슷하니까 두 조건을 합치면

const permutations = function (input) {
  let answer = [];
  
  if (input.length <= 1) {
    answer.push(input);
    return answer;
  }
  
  return answer[];
};

 

베이스케이스는 완성된다.

 

- recursion call

예를 들어 input = [1, 2, 3] 일 때 [[3]] => [[2, 3]] => [[1, 2, 3]] 식으로 버블링 되어야 한다. 반대로 과정을 생각하면 [1, 2, 3]에서는 1이라는 요소를 빼고, [2, 3]에서는 다시 2를 빼고 하는 식으로 input의 각 요소를 순회하며 그 요소를 제외한 새로운 배열을 만들어야 하므로, 우선 해당 코드를 작성한다.

input.forEach((currentItem) => {
  const restItems = input.filter((item) => item !== currentItem);
}

 

이 상태에서 restItems를 인자로 주는 permutations를 다시 호출하면 restItems.length === 1인 경우까지 호출된 후 [[3]]이 제일 먼저 반환될 것이고, 이때 input = [2, 3], currentItems = 2이다. 반환된 배열을 permutation이라고 하자

input.forEach((currentItem) => { // currentItem = 2
  const restItems = input.filter((item) => item !== currentItem); // restItems = [3]
  const permutation = pemutations(restItems); // permutation = [[3]]
}

 

하드코딩으로 일단 결과만 내보면 permutation[0]에 currentItem을 붙여줘야 한다. test코드에서 sort로 후처리가 있기 때문에 push하는게 성능면에서 이득이지만, 우선 알아보기 쉽게 unshift로 진행한다.

input.forEach((currentItem) => { // currentItem = 2
  const restItems = input.filter((item) => item !== currentItem); // restItems = [3]
  const permutation = pemutations(restItems);
  permutation[0].unshift(curretItem); // permutation[0] = [2, 3], permutation = [[2, 3]]
}

 

그리고 permutatoin을 answer 에 담아주는데 둘 다 배열이므로 concat을 사용한다.

input.forEach((currentItem) => { // currentItem = 2
  const restItems = input.filter((item) => item !== currentItem); // restItems = [3]
  const permutation = pemutations(restItems);
  permutation[0].unshift(curretItem); // permutation[0] = [2, 3], permutation = [[2, 3]]
  answer = answer.concat(permutation) // answer = [[2, 3]]
}

 

currentItem이 3로 넘어간 후 같은 과정을 반복하면, input = [2, 3] 일 때 반환되는 answer =  [[2, 3], [3, 2]]가 된다. 그 이전 호출로 돌아가면 currentItem = 1일 때 permutation = [[2, 3], [3, 2]]이다. 그런데 여기서 permutation[0]에만 currentItem을 붙이면 permutation =  [[1, 2, 3], [3, 2]]이 된다. 이를 해결하기 위해 permutation의 각 요소를 순회하며 currentItem을 붙이게 코드를 수정한다.

input.forEach((currentItem) => { // currentItem = 1
  const restItems = input.filter((item) => item !== currentItem); // restItems = [[2, 3]]
  const permutation = pemutations(restItems); // [[2, 3], [3, 2]]
  permutation.forEach((p) => p.unshift(currentItem)); // permutation = [[1, 2, 3], [1, 3, 2]]
  answer = answer.concat(permutation) // answer = [[1, 2, 3], [1, 3, 2]]
}

 

curretItem이 2로 넘어간 후 restItems = [1, 3]으로 바꾸어 다시 같은 과정을 진행하면 permutations(restItems)은  [[1, 3], [3, 1]]을 반환한다. 여기에 curretItem을 붙이고 나면 permutation = [[2, 1, 3], [2, 3, 1]]을, answer에 concat 하면 answer = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1]]이 된다. currentItem이 3으로 넘어가도 같은 방식으로 동작하여 최종적으로

answer = [
    [1, 2, 3], 
    [1, 3, 2],
    [2, 1, 3],
    [2, 3, 1],
    [3, 1, 2],
    [3, 2, 1]
];

 

이 된다.

test("6 possible permutations for a set containing 3 numbers", () => {
    expect(permutations([1, 2, 3]).sort()).toEqual(
      [
        [1, 2, 3],
        [1, 3, 2],
        [2, 1, 3],
        [2, 3, 1],
        [3, 1, 2],
        [3, 2, 1],
      ].sort()
    );
  });

 

input을 [1, 2, 3, 4]로 바꾸어도 기대한대로 작동하였다.

+ Recent posts