함수형 프로그래밍 #03 | Array: 여러 개일 수 있는 값

📌 비결정적인 함수

배열이나 리스트는 0개 이상의 데이터를 가지기 때문에 이를 return할 때 0개 이상의 값이 반환이 되는데 이 개수가 결정되지 않기 때문에 비결정적이라고도 부른다. 의미상 값이 여러개가 되기도 하지만 배열, 리스트에 의해 여러 값은 하나의 값으로 취급과 같기 때문에 순수함수가 아니라고 볼 수는 없다.


📌 고차함수

위의 판에박힌 코드들을 해결하기 위해 함수형 프로그래밍에서는 고차함수를 사용한다. 고차함수하나 이상의 함수를 인수로 취하는 함수 || 함수를 결과로 반환하는 함수를 말한다.

대표적인 고차함수인 map을 제네릭으로 구현해보자.

export const map = <A, B>(arr: Array<A>, f: (a: A) => B): Array<B> => {
  const result: Array<B> = [];
  for (const value of arr) {
    result.push(f(value));
  }
  return result;
}

map함수 내에서 for루프를 사용하기는 했지만 사용하는 입장에서는 map함수가 어떻게 구현되어 있는지 신경쓸 필요가 없고, 주어진 배열의 모든 원소에 주어진 함수를 사용해서 새로운 배열을 얻을 수 있다는 것만 알고있으면 된다. 즉, 기존에 직접 다루던 부수효과를 분리해서 격리한 후에 별도의 효과로 취급할 수 있도록 추상화 한 것이다.


📌 장바구니 프로그램 (명령형)

//cart.ts

export interface Item {
  code: string;
  outOfStock: boolean;
  name: string;
  price: number;
  quantity: number;
}
import {cart} from "./cart.js";
const list = () => {
  let html = "<ul>";
  let totalCount = 0;
  let totalPrice = 0;
  
  for (let i = 0; i < cart.length; i++) {
    html += "<li>";
    html += `<h2>${cart[i].name}</h2>`;
    html += `<div>가격: ${cart[i].price}</div>`;
    html += `<div>수량: ${cart[i].quantity}</div>`;
    html += "</li>";
    
    totalCount += cart[i].quantity;
    totalPrice += cart[i].price * cart[i].quantity;
  }
  html += "</ul>";
  
  html += `<h2>전체 수량: ${totalCount}`;
  html += `<h2>전체 가격: ${totalPrice}`;
  return html;
}

const app = document.getElementById("app");
if (app !== null) {
  app.innerHTML = `
  	<h1>장바구니</h1>
  	${list()}
  `;
}

위에서 전체수량, 전체가격 계산하는 코드를 분리하기 위해 for문을 하나 더 작성해볼 수도 있다. 만약 cart[i].outOfStock이 true일 때 취소선을 긋는 기능이 추가된다고 했을 때 for문 안에 if else문을 추가해서 코드를 작성할 것이다. 즉, 명령형으로 작성한 이 코드는 요구사항이 늘어날 수록 대응해야하는 코드가 점점 많아지는 것이다. 게다가 기존 요구사항이 변경되면 그것까지 반영을 해야하고, 이렇게 되면 변경사항이 늘어나면 실수하기가 쉬워진다.


📌 장바구니 프로그램 (순수함수)

장바구니를 그린다.

-> 장바구니를 순회하면서

-> 화면에 상품 이름, 가격, 수량을 표시한다.

전체 가격과 전체 수량도 화면에 그린다.

-> 2번의 동작을 수행할 때 totalPrice, totalCount에 값을 누적한다.

재고 없는 상품의 처리

-> 2번과 6번의 동작을 수행할 때 재고 여부에 따라 다르게 동작시킨다.

아이템 목록 표시

  • 재고가 있는 아이템
  • 재고가 없는 아이템

전체 수량 표시

전체 가격 표시

const stockItem = (item: Item): string => `
	<li>
  	<h2>${item.name}</h2>
    <div>가격: ${item.price}</div>
    <div>수량: ${item.quantity}</div>
	</li>
`;

const outOfStockItem = (item: Item): string => `
	<li class="gray">
  	<h2>${item.name} (품절)</h2>
    <div class="strike">가격: ${item.price}</div>
    <div class="strike">수량: ${item.quantity}</div>
	</li>
`;

const item = (item: Item): string => {
  if (item.outOfStock) {
    return outOfStockItem(item);
  } else {
    return stockItem(item);
  }
}

const totalCalculator = (list: Array<Item>, getValue: (item: Item) => number) => {
  let total = 0;
  for (let i = 0; i < list.length; i++) {
    if (list[i].outOfStock === false) {
      total += getValue(list[i]);
    }
  }
  return total;
}

const totalCount = (list: Array<Item>): string => {
  const totalCount = totalCalculator(list, (item) => item.quantity);
  return `<h2>전체 수량: ${totalCount}상자</h2>`;
}

const totalPrice = (list: Array<Item>): string => {
  const totalPrice = totalCalculator(list, (item) => item.price * item.quantity);
  return `<h2>전체 가격: ${totalPrice}원</h2>`;
}

const list = (list: Array<Item>) => {
  let html = "<ul>";
  
  for (let i = 0; i < list.length; i++) {
    html += item(list[i]);
  }
  html += "</ul>";
  return `${html}`;
}

const app = document.getElementById("app");
if (app !== null) {
  app.innerHTML = `
  	<h1>장바구니</h1>
  	${list(cart)}
  	${totalCount(cart)}
  	${totalPrice(cart)}
  `;
}

📌 장바구니 프로그램 (순수함수 + 메소드 체이닝)

totalCalculator를 메소드 체이닝으로 해보자.

전체 목록중 재고가 있는 상품만 getValue를 실행하고 그 값을 모두 더한다.

  1. 재고가 있는 상품만 분류하기
  2. 분류된 상품들에 대해서 getValue 실행하기
  3. getValue가 실행된 값 모두 더하기
const totalCalculator = (list: Array<Item>, getValue: (item: Item) => number) => {
  return list
  	.filter(item => item.outOfStock === false)
  	.map(getValue)
  	.reduce((total, value) => total + value, 0);
}

list를 메소드 체이닝으로 해보자.

  1. 목록에 있는 아이템을 태그로 변경
  2. 태그의 목록을 모두 하나의 문자열로 연결
const list = (list: Array<Item>) => {
  return `
  	<ul>
  		${list
				.map(item)
				.reduce((tags, tag) => tags + tag, "")}
  	</ul>
  `;
}