[FP] 튜플을 통한 함수 단순화 그리고 커링

yongkini ·2024년 5월 7일
0

Functional Programming

목록 보기
4/10
post-thumbnail

파이프라이닝을 할 때, 즉, 함수 합성을 할 때 중요한 조건 중에(혹은 함수간의 호환 척도 중에 하나인) 하나인 '항수', 그리고, 인수가 1개인 순수함수는 한가지 영도, 즉, 단일 책임을 담당하므로 가장 단순한 함수라고 할 수 있다.

: 위의 명제(?)는 아니지만, 문장을 위해서 튜플 이라는 자료 구조를 사용하고자 한다. 튜플은 형식이 다른 원소를 한 데 묶어서 다른 함수에 건네주는 일이 가능한 불변적인 자료구조이다. 물론, 배열이나 객체를 이용할 수 있지만, 튜플을 사용하면 좀 더 쉽게 '불변성'을 유지할 수 있고, 이형배열(본래 배열이란 길이가 정해져있고, 자료 형식도 하나로 정해져있는 자료구조다)을 만들지 않아도 되며, 하나의 데이터로 묶기 위해 임의의 형식을 갖는 객체처럼 새로운 형식을 정의할 필요도 없다는 장점이 있다. 그래서 결론적으로 튜플로 함수의 항수를 줄일 수 있다.

JS에서 튜플 만들고 예시에 적용해보기.

const checkType = (type) => {
    return (val) => {
        if(typeof val !== type) {
            throw new TypeError(`형식 불일치: ${type}이어야 하는데, ${typeof val} 입니다.`);
        } else {
            return val;
        }
    }
}
const Tuple = function() {
    const typeInfo = Array.prototype.slice.call(arguments);
    const _T = function () {
        const values = Array.prototype.slice.call(arguments);
        if(values.some(
            val => val === null || val === undefined)) {
            throw new ReferenceError("튜플은 null 값을 가질 수 없습니다!.");
        }
        if(values.length !== typeInfo.length) {
            throw new TypeError("튜플 항수가 프로토타입과 맞지 않습니다!");
        }
        values.forEach((val, index) => {
            this['_' + (index + 1)] = checkType(typeInfo[index])(val);
        }, this);

        Object.freeze(this);
    }

    _T.prototype.values = function () {
        return Object.keys(this).map(k => this[k]);
    }

    return _T;
}
const normalize = str => str.replace(/\-/g, '');
const trim = str => str.replace(/^\s*|\s*$/g, '');
const Status = Tuple('boolean', 'string');
const isValid = function (str) {
    if(str.length === 0) {
        return new Status(false, '잘못된 입력입니다. 빈 값일 리 없지요!');
    } else {
        return new Status(true, '성공!');
    }
}
isValid(normalize(trim('444-44-4444'))); // _T {_1: true, _2: '성공!'}

구조 분해 할당을 이용한 최적화(?)

const StringPair = Tuple('string', 'string');
const name = new StringPair('Barkley', 'Rosser');

[first, last] = name.values();
first; // 'Barkley'
last; // 'Rosser'

그러나, 더 나은 대체품?! = 함수 커링

: 위에서 만든 checkType 함수는 커링을 이용해서 만들었다. 하지만, RamdaJS를 써서 좀 더 편하게 만들어보자


const checkType = R.curry((type, val) => {
  if (typeof val !== type) {
    throw new TypeError(
      `형식 불일치: ${type}이어야 하는데, ${typeof val} 입니다.`
    );
  } else {
    return val;
  }
});

const result = checkType("boolean")("str"); // TypeError

이런식으로 커링은 JS 특성상(not TS) 여러개의 인수를 받아야하는 함수에서 한개의 인수만을 입력해도 나머지 인수가 undefined로 되는 것을 방지하며, 동시에 위에서 언급한 간단한 함수의 조건인 단항 함수 요건도 충족해준다.

위에서 말한 튜플도 단항함수를 만들기 위한 좋은 방법이지만, 커링을 이용하면 위와 같이 여러개의 인수를 무조건 받도록 강제하는 동시에 단항 함수로서 함수를 쓸 수 있다.

커링의 쓰임새

함수 팩토리를 모방한다.

: 객체 지향에서 인터페이스는 특정 클래스를 상속 받는 클래스에서 반드시 구현해야할 규약을 정해놓은 추상적 형식이다. 어떤 인터페이스에 findStudent라는 함수가 있으면 이 인터페이스를 구현한 코드는 반드시 이 함수를 구현해야 한다.

** 함수 팩토리 = 객체를 생성해서 반환하는 함수

이 때, 포인트는 호출자 관점에서 메서드를 호출한다는 사실이 중요하지 객체의 출처는 관심이 없다는 것이다. 즉, 특정 타입을 넣어서 특정 결과값만(참조 투명성 및 순수함수) 제대로 나오면 상관이 없다는 것.

예를 들어서, 어떤 학생 객체를 찾아서 리턴하는 함수인 findStudent라는 함수가 있다고 해보자. 이 때, 학생 객체는 DB에서 찾을수도 있고, caching 된 메모리(배열이라고 하자)에서 찾을 수 있다고 해보자. 이 때, 호출자 관점에선 앞서 말한 것처럼 결과값으로 학생 객체가 정상적으로 나오면 그만이다. 이 때, 커링을 이용해서 이를 구현해보면(객체 지향에서 인터페이스가 아닌),

결론적으로

(function () {
  localStorage.setItem("test", JSON.stringify(["yongki", "jihoon", "chaeun"]));
})();

const array = JSON.parse(localStorage.getItem("test"));

console.log(array);
const findStudentFromCache = R.curry((arr, ssn) => {
  return Array.isArray(array) ? arr[ssn] : null;
});

const findStudentFromDB = R.curry((db, ssn) => {
  return db[ssn];
});

const dummyDB = {
  0: "yongki",
  1: "jihoon",
  2: "chaeun",
};
const findStudent = (type) =>
  type === "db" ? findStudentFromDB(dummyDB) : findStudentFromCache(array);

const studentName = findStudent("cache")("2");
console.log(studentName); // chaeun
const studentName = findStudent("cache")("2");
console.log(studentName); // chaeun

이렇게 호출자 시점에서는 findStudent 를 호출할뿐 그 안에서 findStudentFromDB를 쓰는지 findStudentFromCache를 쓰는지 어떤 방식으로 실행되는지 알 필요가 없이 Student 값을 결과값으로 받을 수 있다.

재사용 가능한 함수 템플릿을 구현한다.

: 사실 이렇게 재사용 가능한 함수 템플릿을 구현해 쓸지는 의문이지만, 무슨 말인지 이해하는 용도로 예시 코드를 짜봤다.

아래 코드는 이런 코드다.

  • 대학교에 팔아먹을? 코드로 특정 학교의 전공들, 전공별 과목들을 정해주고, 특정 학생의 전공과 분과를 선택해주면 랜덤으로 특정 학기의 추천 과목을 리턴해준다.
  • 이 때, A 학교에도 이 프로그램을 팔아야하고, B학교에도 팔아야하므로 명령형 프로그래밍처럼 randomSubjectNameForAUniv 이렇게 각각 함수를 만드는게 아니라 모듈성을 갖게 만들어서 재사용해서 쓰고 싶다고 생각해보자.
  • 그리고 앞서 배운 커링을 써보자(R.curry). 커링을 써서 특정 학교에서 열리는 전공들을 입력해놓고(전공은 잘 안바뀌니까), 그 다음으로 그 전공의 분과를 입력하도록 한다(이것도 잘 안바뀌지만 바뀔 수 있다). 그 다음부터는 전공 코드와 해당 기능을 쓰는 학생의 선호 분과를 선택하도록 한다. 앞서 말한 4가지(전공, 전공의 분과, 전공 코드, 학생의 선호 분과)를 각각 단항함수처럼 입력하도록 하고, 해당 기능들을 전부 분리해서 재사용할 수 있도록한다.

그 결과는 이러한 코드가 된다.

const getRandomSubjectName = R.curry(
  (majors, majorCode, subjects, subMajorName) => {
    const randomIndex = Math.floor(Math.random() * 3);
    const subjectName = subjects[majors[majorCode]][subMajorName];
    return subjectName ? subjectName[randomIndex] : "수강 신청 실패";
  }
);

const XUnivMajors = [
  "철학",
  "심리학",
  "사회학",
  "컴퓨터 공학",
  "경제학",
  "경영학",
];

const XUnivSubjects = {
  철학: {
    미학: ["미학 이론", "서양 미학사", "마그리트 미학"],
    논리학: ["논리학 개괄", "비트겐슈타인 철학"],
    인식론: ["인지과학과 인식론", "칸트 인식론"],
  },
  "컴퓨터 공학": {
    자료구조: ["자료구조 개론"],
    네트워크: ["OSI 7계층", "네트워크 아키텍처 설계"],
    프로그래밍: ["파이썬으로 배우는 크롤링", "자바 입문", "C언어"],
  },
};

const XUnivSubjectsVersionForSecondSemester = {
  철학: {
    미학: ["메를로퐁티의 눈", "서양 미학사2", "헤겔 미학"],
    논리학: ["비트겐슈타인 철학2", "수리 논리학"],
    인식론: ["매트릭스와 인식론", "오컴의 면도날"],
  },
  "컴퓨터 공학": {
    자료구조: ["자료구조 심화"],
    네트워크: ["HTTP 이론", "네트워크 아키텍처 설계2"],
    프로그래밍: [
      "JS로 배우는 FE 프로그래밍",
      "자바 입문2",
      "C언어2",
      "JAVA로 배우는 백엔드 프로그래밍",
    ],
  },
};

const XUnivSystemGetRandomSubject = getRandomSubjectName(XUnivMajors);

const MAJOR_CODE = 0;
const MAJOR_CODE_2 = 3;

const XUnivRandomSubjectSystemForPhilosophyMajor =
  XUnivSystemGetRandomSubject(MAJOR_CODE);

const XUnivRandomSubjectSystemForCSMajor =
  XUnivSystemGetRandomSubject(MAJOR_CODE_2);

const studentYongKi = {
  preference: "미학",
  major: "philosophy",
};

const studentJihoon = {
  preference: "프로그래밍",
  major: "컴퓨터 공학",
};

const XUnivRandomSubjectSystemForPhilosophySubMajorsForFirstSemester =
  XUnivRandomSubjectSystemForPhilosophyMajor(XUnivSubjects);

const XUnivRandomSubjectSystemForCSSubMajorsForSecondSemester =
  XUnivRandomSubjectSystemForCSMajor(XUnivSubjectsVersionForSecondSemester);

const randomSubjectNameForThisSemester =
  XUnivRandomSubjectSystemForPhilosophySubMajorsForFirstSemester(
    studentYongKi.preference
  );

const randomSubjectNameForNextSemester =
  XUnivRandomSubjectSystemForCSSubMajorsForSecondSemester(
    studentJihoon.preference
  );

console.log(randomSubjectNameForThisSemester); // 서양 미학사
console.log("-----------------------------------");
console.log(randomSubjectNameForNextSemester); // 마그리트 미학

재사용을 억지로 한 느낌이 조금은 있지만, XUnivSystemGetRandomSubject 이걸 사용해서 두개의 전공에 맞는 XUnivRandomSubjectSystem 을 만들어냈다. 이 system을 가지고 first, second semester에 맞는 함수도 만들어냈다. 물론 전공을 똑같이 했으면 first, second semester 별로 나뉜 XUnivRandomSubjectSystemForSubMajors의 재사용이 더 두드러졌겠지만, 일단 패스한다.

위와 같이 커링을 쓰면 재사용성이 향상되며, 앞서 말한 중요한 요소 중에 하나인(커링의 장점 중) 다인수 함수를 단항 함수로 바꿔준다.

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

0개의 댓글