3장. 액션과 계산, 데이터의 차이를 알기

액션과 계산, 데이터

  • 액션

    • 실행 시점과 횟수에 의존한다.
    • 부수 효과, 부수 효과가 있는 함수, 순수하지 않은 함수 라고도 불림
  • 계산

    • 입력으로 출력을 계산한다.
    • 순수 함수, 수학 함수 라고도 불림
  • 데이터

    • 이벤트에 대한 사실
    • 일어난 일의 결과를 기록한 것
  • 장보기 과정으로 액션, 계산, 데이터 분리해보기 Untitled

    1. 액션과 계산, 데이터는 어디에나 적용할 수 있다.
    2. 액션 안에는 계산과 데이터, 또 다른 액션이 숨어 있을지도 모른다.
    3. 계산은 더 작은 계산과 데이터로 나누고 연결할 수 있다.
    4. 데이터는 데이터만 조합할 수 있다.
    5. 계산은 때로 ‘우리 머릿속에서’ 일어난다.

데이터

  • 함수형 프로그래밍의 데이터 불변성
    1. 카피-온-라이트 : 변경할 때 복사본을 만든다.
    2. 방어적 복사 : 보관하려고 하는 데이터의 복사본을 만든다.
  • 데이터의 장점
    1. 직렬화 : 직렬화된 데이터는 전송하거나 디스크에 저장했다가 읽기 쉬움
    2. 동일성 비교 : 데이터 간 비교가 쉬움
    3. 자유로운 해석 : 여러 가지 방법으로 해석 가능
  • 단점
    1. 해석이 반드시 필요

쿠폰 보내기 서비스에 함수형 사고 적용하기

  • 친구 10명을 추천하면 더 좋은 쿠폰을 보내주려고 한다.
  • 이메일 DB에는 이메일별로 각 사용자가 추천한 친구 수를 기록한다.
  • 쿠폰DB는 각 쿠폰에 ‘bad’, ‘good’, ‘best’과 같은 등급 정보가 있다.

1. 서비스를 구현하는데 필요한 것들을 액션과 계산, 데이터로 분류하기

2. 타임라인 다이어그램으로 그리기

3. 다이어그램을 코드로 구현하기

var subscriber = {
  email: "sam@pmail.com",
  rec_count: 16,
};
 
var rank1 = "best";
var rank2 = "good";
 
function subCouponRank(subscriber) {
  if (subscriber.rec_count >= 10) return "best";
  else return "good";
}
 
var coupon = {
  code: "10PERCENT",
  rank: "bad",
};
 
function selectCouponsByRank(coupons, rank) {
  var ret = [];
  for (var c = 0; c < coupons.length; c++) {
    var coupon = coupons[c];
    if (coupon.rank === rank) ret.push(coupon.code);
  }
  return ret;
}
 
var message = {
  from: "newsletter@coupondog.co",
  to: "sam@pmail.com",
  subject: "Your weekly coupons inside",
  body: "Here are your coupons ...",
};
 
function emailForSubscriber(subscriber, goods, bests) {
  var rank = subCouponRank(subscriber);
  if (rank === "best")
    return {
      from: "newsletter@coupondog.co",
      to: subscriber.email,
      subject: "Your best weekly coupons inside",
      body: "Here are the best coupons: " + bests.join(", "),
    };
  // rank === "good"
  else
    return {
      from: "newsletter@coupondog.co",
      to: subscriber.email,
      subject: "Your good weekly coupons inside",
      body: "Here are the good coupons: " + goods.join(", "),
    };
}
 
function emailsForSubscribers(subscribers, goods, bests) {
  var emails = [];
  for (var s = 0; s < subscribers.length; s++) {
    var subscriber = subscribers[s];
    var email = emailForSubscriber(subscriber, goods, bests);
    emails.push(email);
  }
  return emails;
}
 
function sendIssue() {
  var coupons = fetchCouponsFromDB();
  var goodCoupons = selectCouponsByRank(coupons, "good");
  var bestCoupons = selectCouponsByRank(coupons, "best");
  var subscribers = fetchSubscribersFromDB();
  var emails = emailsForSubscribers(subscribers, goodCoupons, bestCoupons);
  for (var e = 0; e < emails.length; e++) {
    var email = emails[e];
    emailSystem.send(email);
  }
}

데이터는 사용하는 데 제약이 많고 액션은 가장 제약이 없다.

데이터→계산→액션 순으로 구현하는 것이 함수형 프로그래밍의 일반적인 구현 순서이다.

이미 있는 코드에 함수형 사고 적용하기

function figurePayout(affiliate) {
  var owed = affiliate.sales * affiliate.commission;
  if (owed > 100)
    // don’t send payouts less than $100
    sendPayout(affiliate.bank_code, owed);
}
 
function affiliatePayout(affiliates) {
  for (var a = 0; a < affiliates.length; a++) figurePayout(affiliates[a]);
}
 
function main(affiliates) {
  affiliatePayout(affiliates);
}

→ 해당 코드는 함수형 코드라고 하기 어려움.

← 강조 표시한 액션부터 ~~~ 코드 전체로 액션이 퍼져나감

keyPoint

  • 액션은 실행 시점이나 횟수에 의존한다. 일반적으로 액션은 외부 세계에 영향을 주거나 받는다.
  • 계산은 입력값으로 출력값을 만드는 것이다. 외부 세계에 영향을 주거나 받지 않고 실행 시점이나 횟수에 의존하지 않는다.
  • 데이터는 이벤트에 대한 사실이다. 사실은 변하지 않기 때문에 영구적으로 기록할 수 있다.
  • 함수형 프로그래머는 액션보다 계산을 좋아하고 계산보다 데이터를 좋아한다.
  • 계산은 같은 입력값을 주면 항상 같은 출력값이 나오기 때문에 액션보다 테스트하기 쉽다.

[도전] 알고리즘 문제에 함수형 사고 적용해보기

  • 단어들의 길이가 다르면 오름차순으로 정렬
  • 단어들의 길이가 같으면 사전순으로 정렬
  • 사전의 앞에서 K번째 위치한 단어 출력
// Run by Node.js
const readline = require("readline");
 
(async () => {
  let rl = readline.createInterface({ input: process.stdin });
  let input = [];
 
  for await (const line of rl) {
    input.push(line);
  }
  rl.close();
 
  const result = findWord(input);
  console.log(result);
 
  process.exit();
})();
 
// 입력값 분류하기
function classifyInput(input) {
  const [N, K] = input[0].split(" ").map(Number);
  const words = input.slice(1);
  return [N, K, words];
}
 
// 단어 정렬하기
function sortWords(words) {
  words.sort((a, b) =>
    a.length == b.length ? a.localeCompare(b) : a.length - b.length
  );
  return words;
}
 
// 특정 위치의 단어 출력하기
function outputWord(words, K) {
  return words[K - 1];
}
 
// 단어 찾기
function findWord(input) {
  const [N, K, words] = classifyInput(input);
  const sortedWords = sortWords(words);
  const printedWord = outputWord(sortedWords, K);
  return printedWord;
}