패캠의 "TypeScript를 활용한 함수형 프로그래밍 온보딩"의 함수형 프로그래밍 4,5,6강을 듣고 정리해봤다.
함수형 프로그래밍에서는 부수효과를 찾아내고, 분리해서 공통적인 방법으로 추상화한다.
1) map
을 활용해서 공통적인 부수효과를 격리해서 다룬다.
map(f): Array<A> => Array<B>
효과와 계산을 분리
// 명령형 프로그래밍
function list() {
let html = '<ul>'; // 값 변경하는 부수효과
for(let i = 0; i <= cart.length; i++) {
html += item(cart[i])
}
html += '</ul>'
}
// 함수형 프로그래밍
function list() {
return `<ul>
${cart.map(c => item(c))}
</ul>`
}
map은 제네릭으로 구현해서 다양한 functor에 적용할 수 있다. (ex: Array, Option, Try 등)
type map<A,B> = (functor<A>, A=>B) => functor<B>
2) Option
을 활용해 예외값(undefined
, null
) 처리하기
명령형 프로그래밍에서 예외 값 처리는 다음과 같이 if
문을 활용하여 처리한다.
// item을 받아서 화면에 표시한다.
const CartItem = (item: Item):string => {
let discountPrice = 0
let saleText = ""
if(item.discountPrice !== undefined) {
discountPrice = item.discountPrice
saleText = `(${item.discountPrice}원 할인)`
}
return `<li>
<h2>${item.name}</h2>
<p>가격: ${item.price - discountPrice} ${saleText}</p>
<p>수량: ${item.quantity}상자</p>
</li>`
}
이렇게 하면 CartItem
컴포넌트는 변수를 수정하는 부수효과가 내부에 존재한다.
컴포넌트가 많아지면 이런 부수효과들이 많아지고 관리하기 어려워진다.
이런 부수효과들을 공통적 방법으로 추상화하는 방법으로 Option
이 있다.
Option
Option
은 값이 있을 수도 없을 수도 있는 값들을 나타내는 타입이다. 범용적으로 사용하기 위해 generic
을 사용한다.
export type Some<A> = {
readonly _tag: "Some",
readonly value: A,
}
export type None = {
readonly _tag: "None",
}
export type Option<A> = Some<A> | None;
이런 식으로 Some
, None
타입을 선언하다. 프로퍼티들을 전부 readonly
로 줘서 수정이 불가능하게 한다. _tag
프로퍼티를 활용해서 식별이 용이하게 한다.
Some
은 값이 존재할 때 None
은 값이 없을때 쓴다. 두 타입을 union
으로 묶어서 Option
타입을 선언한다.
Option
타입을 사용하기 편하게 여러 유틸 함수들을 만들어준다.
export const some = <A>(value: A): Option<A> => ({_tag: "Some", value})
export const none = (): Option<never> => ({_tag: "None"})
export const isSome = <A>(oa: Option<A>): oa is Some<A> => oa._tag === "Some"
export const isNone = <A>(oa: Option<A>): oa is None => oa._tag === "None"
export const fromUndefined = <A>(a: A | undefined): Option<A> => {
if(a === undefined) return none()
return some(a)
}
export const getOrElse = <A>(oa: Option<A>, defaultValue: A): A => {
if(isNone(oa)) return defaultValue
return oa.value
}
export const map = <A,B>(oa: Option<A>, f: (a: A) => B): Option<B> => {
// 값이 없으면 값이 없는 상태를 유지
if(isNone(oa)) return oa
// 값이 있으면 값을 함수에 적용한다
return some(f(oa.value))
}
export const mapOrElse = <A,B>(oa: Option<A>, f: (a: A) => B, defaultValue: B) : B => {
return getOrElse(map(oa,f), defaultValue)
}
some
,none
타입인 옵션 생성, 판별 함수js
,ts
에서는 값이 없거나 속성이 없을때undefined
가 들어가 있는 경우가 정말 많기 때문에 해당 값을Option
으로 변환하는fromUndefined
도 만든다.getOrElse
함수로none
타입일 때 기본값을 설정해서 사용할 수 있다.map
:Option<A>
,f:(a: A) =>B
를 인자로 받아 옵션의A
에 함수f
를적용해서B
로 변환하고 걔가 담긴Option<B>
를 리턴한다.mapOrElse
:getOrElse
와map
을 합성한 함수다.map
으로 변환한 애의 타입이None
이면defaultValue
를 주고 아니면, 변환된Option
을 준다.
위의 함수를 이용해서 부수효과를 공통적 방법으로 추상화하고 함수형으로 코드를 리팩토링할 수 있다.
초반부 예시 코드를 리팩토링하면 다음과 같다.
const CartItem = (item: Item):string => {
const optionDiscountPrice = O.fromUndefined(item.discountPrice)
const discountPrice = O.getOrElse(optionDiscountPrice, 0)
const saleText = O.mapOrElse(optionDiscountPrice,(discountPrice) => `(${discountPrice}원 할인)`, '')
return `<li>
<h2>${item.name}</h2>
<p>가격: ${item.price - discountPrice} ${saleText}</p>
<p>수량: ${item.quantity}상자</p>
</li>`
}
discountPrice
가 undefined
일 때 처리를 getOrElse
와 mapOrElse
를 활용해서 처리했다. 컴포넌트 내부에 부수효과가 없어졌다.
그리고 부수효과를 Option
을 활용해서 공통적인 방식으로 처리할 수 있다.
보기에도 좀더 깔끔하다
3) Try로 Error 처리하기
명령형 프로그래밍에서 Error 처리는 if
문을 활용하거나 try catch
문을 활용해서 다룬다.
const ex1 = () => {
try {
return getSome()
} catch(e) {
return 1
}
}
const ex2 = () => {
if(n === 0) return 1
return getSome()
}
const getSome = () => {
if(n === 0) throw Error()
}
함수형 프로그래밍에서 이렇게 처리하면, 함수들의 합성이 어려워진다.
try catch
의 경우 에러 발생시 함수를 중단하고 제일 가까운 catch
절로 이동한다. => 매번 동일한 값에 동일한 결과를 보장하지 않는다
if
문은 평가식이 순수하지 않을 경우 해당 문이 있는 함수는 순수함수가 되지 못한다. => 매번 동일한 값에 동일한 결과를 보장하지 않는다.
이러한 에러들을 따로 추출해서 공통적으로 다루기 위해서 Try
를 활용한다.
Try
Try
는 전체적으로 Option
과 비슷하지만 다음의 큰 차이점이 있다.
Option<A>
: 에러가 발생햇다는 사실만 중요Try<E,R>
: 어떤 에러가 발생했는지 그 내용도 중요
이외에는 Option과 거의 동일하게 구현한다.
type Success<R> = {
readonly _tag: "success";
readonly result: R;
};
type Failed<E> = {
readonly _tag: "failed";
readonly error: E;
};
export type Try<E, R> = Failed<E> | Success<R>;
// 이하 Option과 동일한 방식
success()
failed()
isSuccess()
isFailed()
getOrElse(ta: Try<E,R>, defaultValue: (e: E) => R) : R
map(ta: Try<E, A>, f: (a: A) => B): Try<E, B>
//
장바구니 페이지를 구현할 때,
장바구니에 담긴 항목들을 Try
를 활용해서 깔끔하게 예외 처리를 할 수 있다.
type ParsedItem = { _tag: "parsedItem" } & Item;
type ParseError = {
name: string;
message: string;
};
// 중간중간 생략
const parseItem = (item: Item): T.Try<ParseError, ParsedItem> => {
if (item.quantity < 1) {
return T.failed({
name: item.name,
message: "상품은 반드시 한 개 이상 담아야 합니다."
});
} else if (item.quantity > 10) {
return T.failed({
name: item.name,
message: "한 번에 10개를 초과하여 구매할 수 없습니다."
});
}
return T.success({
_tag: "parsedItem",
...item
});
};
const stockItem = (item: ParsedItem): string => {
return `
<li>
<h2>${item.name}</h2>
<div>가격: ${item.price}원</div>
<div>수량: ${item.quantity}상자</div>
</li>
`;
};
const errorItem = (e: ParseError): string => `
<li style="color: red">
<h2>${e.name}</h2>
<div>${e.message}</div>
</li>
`;
// 중간중간 생략
const list = (list: ArrayItem) => {
return `
<ul>
${list
// 1. 목록에 있는 모든 아이템을 태그로 변경
.map(item => T.getOrElse(
T.map(item, parsedItem => renderItem(parsedItem)),
errorItem
))
// 2. 태그의 목록을 모두 하나의 문자열로 연결
.reduce((tags, tag) => tags + tag, "")}
</ul>
`;
};
처리 흐름은 다음과 같다.
- 아이템 파싱, 에러 발생시 failed에 에러명, 에러 메시지를 담아 놓는다. 성공시에는 아이템을 담는다.
getOrElse
와map
을 활용해서 파싱이 성공시에는 정상적으로stockItem
을 활용해서 렌더링하고 실패시에는errorItem
을 활용해서 렌더링해준다.
이렇게 함으로써 에러를 격리하고 Try
를 활용해서 공통적으로 다룬다.
깔끔하고, 부수효과도 없어지고 에러 처리 로직이 공통적 방법으로 다뤄지기 때문에 좋다.
'프론트엔드 > 함수형 프로그래밍' 카테고리의 다른 글
함수형 프로그래밍과 부수효과 (2) | 2023.01.03 |
---|