함수형 프로그래밍 #02 | 함수와 타입

📌 전역변수 사용 시 문제점(명령형 코드)

아래의 앱은 hot module replacement(HMR) 라는 module을 통해 변경사항이 발생한 파일에 대해서만 다시 실행이 되도록 한다.

//price.js
export const totalPrice = 0;

function addOrangePrice() {
	for(let i = 0; i < 100; i++) {
	  totalPrice += 200;
  }
}

export function main() {
	addOrangePrice();
}
//index.js
import * as C from "./price.js";

C.main();

const app = document.getElementById("app");
if (app !== null) {
  app.innerHTML = `
  <h1>total price: ${Math.rount(C.totalPrice)}</h1>
  `;
}

처음 앱을 실행했을 때에는 total price: 20000 이 화면에 출력되지만

index.js의 innerHTML 부분을 Total price로 변경해보자. 그럼 price.js 파일이 다시 실행되는게 아닌 main함수만 다시 실행이 되기 때문에 40000이 화면에 출력이 된다.


📌 위의 코드 순수함수로 바꾸기

//price.js
function priceOfOrange() {
	return 200
}

export function getTotalPrice() {
 	return priceOfOrange() * 100;
}
//index.js
import * as C from "./price.js";

const app = document.getElementById("app");
if (app !== null) {
  app.innerHTML = `
  <h1>total price: ${Math.rount(C.getTotalPrice())}</h1>
  `;
}

“과일 가격을 계산을 계산하기 위해 totalPrice에 과일 가격을 더해야 한다.” 라는 HOW의 관점에서 “과일 가격이 얼마이다.”라는 WHAT으로 코드를 작성한다.

📌 레코드 vs 순수함수

레코드와 순수함수는 같은 입력에 대해 같은 출력을 한다는 점에서 비슷해 보인다.

function getPrice(name: string) {
  if (name === "tomato") {
    return 7000;
  } else if (name === "orange") {
    return 15000;
  } else if (name === "apple") {
    return 10000;
  }
}
const priceOfFruit = {
  tomato: 7000,
  orange: 15000,
  apple: 10000
}

위를 보면 같은 역할을 하지만 아래를 보자.

const isEven = {
  tomato: true,
  orange: true,
  apple: false
}
//문자열의 길이가 홀수, 짝수인지 전부 하드코딩해야 하고 실수가 생긴다.
//데이터가 생길 때마다 추가해야한다.

const isEvenFn = (str: string) => str.length % 2 === 0
// 순수함수로 바꾼 결과이다.

📌 수학에서 보는 함수합성

수학에서의 함수는 어떤 집합의 각 원소를 다른 집합의 유일한 원소에 대응시키는 이항 관계다. 함수합성은 한 함수의 공역이, 다른 함수의 정의역과 일치하는 경우 두 함수를 이어 하나의 함수로 만드는 연산이다.

img
프로그래밍과 수학에서의 정의역, 공역, 치역

추가적으로 수학에서의 집합은 특정한 조건에 맞는 별개의 원소들의 모임으로, 즉 수학에서의 타입은 할당할 수 있는 값들의 집합이다.


📌 전(total) 함수와 부분(partial) 함수

전 함수는 정의역의 모든 원소에 대해 반환값이 정의되어야 하는 함수를 말하고,

부분 함수는 가능한 입력 중에서 일부에만 반환값이 정의되어 있는 함수를 말한다.

=> getPrice는 부분 함수이다.

img
total function, partial function

부분 함수는 실제 프로그래밍에서 구현할 수 없다. getPrice의 경우에는 undefined라는 리터럴 값을 추가하여 전함수로 만들어진다.

애초에 정의역에 해당하는 공역이 있는 타입으로 받으면 되지 않나? => 짝수만 허용하는 타입을 만들 수 있겠어?

//06

📌 함수 합성 시 undefined

function getPrice(name: string): number | undefined {
  if (name === "tomato") {
    return 7000;
  } else if (name === "orange") {
    return 15000;
  } else if (name === "apple") {
    return 10000;
  }
}

function isExpensive(price: number) {
  return price > 10000;
}

위의 함수는 isExpensive(getPrice) 이렇게 합성할 수 없다. 그 이유는 getPrice의 return(공역)타입과 isExpensive의 정의역 타입이 일치하지 않기 때문이다.

function isExpensive(price: number | undefined) {
	if (price === undefined) false;
  return price > 10000;
}

가장 간단한 해결방법으로 정의역 타입을 일치시켜 준다는 것이다. 그리고 해당 정의역에 대한 코드도 작성하는 것을 잊지말자.


📌 제네릭을 통한 함수합성

const compose = (isExpensive, getPrice) => (name) => {
  return isExpensive(getPrice(name));
}

//함수형에서는 f,g,h 이렇게 함수를 표현한다.
const compose = (g, f) => (x) => {
  return g(f(x));
}

위의 함수에 타입을 표현해보자.

const compose = <A, B, C>(g: (y: B) => C, f: (x: A) => B) => (X: A) => {
  return g(f(x));
}

위에는 복잡한데 간단하게 매개변수를 지우고 보면 더 보기 편할 것이다.

<A, B, C>((B) => C, (A) => B) => A => C

[B타입을 받아 C타입을 리턴하는 함수, A타입을 받아 B타입을 리턴하는 함수] 둘을 합성해서 A타입을 받아 C타입을 리턴하는 함수로 만들어주는 함수라는 것을 알 수 있다.

이렇게 보면 제네릭을 “타입을 생성하는 함수”로 볼 수 있다.