함수형 패러다임에 대해 3개월 전 알게 되어 천천히 알아가던중 쓰면 쓸 수록 너!무!나! 취향에 맞아 열심히 쓰고 있다.
다른 사람들과의 협업을 위해 개인적으로 매니악한 수준이라 생각되는 모나드까지는 쓰지 않으려 했으나 사용하다보니 필수적이라 생각들어 결국 모나드까지 쓰게 되었다.
이번 게시글에선 함수형 패러다임에 대한 깊은 설명보다 개념과 사용 예시들을 훑어보고자 한다.
깊은 설명을 하기엔 내 지식이 얕기도 하고 이 게시글을 적는 이유 자체가 참여중인 스터디에서 모나드에 대해 익숙치 않은 사람들에게 모나드를 소개하기 위한 게시글이기 때문이다.
우리는 이미 모나드를 쓰고 있다.
모나드가 무엇일지 알기 위해선 펑터, 어플리케이티브 펑터, 함수 합성 어쩌구 저쩌구... 이것저것 사전지식이 필요한데 이런 사전 지식 없이도 자바스크립트를 쓰며 우린 이미 모나드를 쓰고 있다.
모나드를 쉽게 설명하면 어떤 값을 담고 있는 박스 자체이다. 즉 어떤 타입의 값 T
를 담고 있는 Monad<T>
로 표현 가능하다.
이후 이 박스의 값을 변경하는 어떤 함수 (arg : T)=> S
를 적용하면 Monad<T> -> Monad<S>
로 변경된다.
위 설명을 보면 눈치를 챘을 수 있지만 자바스크립트의 Array
자체가 이미 모나드이다.
Array<T>
형태의 배열에서 (arg : T)=> S
함수를 map
메소드의 인수로 제공하면 배열은 Array<S>
형태로 변경된다.
["banana", "apple", "orange"] // Array<string>
.map((str) => str.length) // Array<number>
.map((length) => length % 2); // Array<boolean>
이번엔 인수로 주어진 함수가 S
타입을 반환하는게 아니라 Array<S>
, 즉 모나드 자체를 반환한다고 가정해보자
이 경우 체이닝 단계가 진행 될 수록 결과값들은 Array<Array<....>>
형태인 중첩된 박스 자체의 모습이 될 것이다.
이 때 함수가 반환하는 모나드 자체를 모나드에 담지 않고 값만 담는 메소드가 존재한다. flatMap
이 그렇다.
function stringToChars(string: text): Array<string>; // 배열 모나드를 반환
const chars: Array<string> = ["banana", "apple"].flatMap(stringToChars);
모나드를 쓰면 무엇이 좋을까?
배열의 프로토타입 메소드를 쓰면서 느꼈던 대부분의 장점들이 모나드를 쓰면 얻을 장점의 일부에 해당한다.
filter,flatMap,map
처럼Array<T>
타입의 값들을 반환하는 메소드를 이용한 체이닝 을 통한 간결한 코드- 유명 함수를 인수로 건내줌으로서 가독성 높은 선언적 코드
function getLengthOfString(string: string): number;
function isOdd(n: number): boolean;
function log(text: any): void;
["banana", "apple", "orange"].map(getLengthOfString).filter(isOdd).forEach(log);
사실 이것보다 더 훌륭한 장점들은 다른 모나드들을 살펴보면 알 수 있다.
Maybe Monad
모나드의 기본적인 성질은 여러개의 가능한 결과를 담고 있는 비결정성을 가진 자료구조 라는 것이다.
배열의 경우에도 [1,2,3]
이란 배열은 내부 원소를 하나 꺼냈을 때 그 값이 1,2,3
중 하나라는 비결정적인 값을 가지고 있다.
실제 세계에서의 데이터는 모두 값이 존재하는 결정적인 형태의 값이 아닐 수 있다. 값이 존재하거나 존재하지 않는 비결정적인 데이터를 담기에 Maybe
모나드가 매우 효과적이다.
Maybe 모나드 구현
이것저것 메소드들을 넣을 수 있지만 Maybe
모나드를 소개하기 위해 가장 필수적인 메소드들만 넣은 코드를 넣는다.
export abstract class Maybe<A> {
abstract isJust(): this is Just<A>;
abstract isNothing(): this is Nothing;
abstract map<B>(f: (a: A) => B): Maybe<B>;
abstract flatMap<B>(f: (a: A) => Maybe<B>): Maybe<B>;
// 부수효과 (로깅 등) 후 값을 그대로 유지
tap(effect: (a: A) => void): Maybe<A> {
if (this.isJust()) effect(this.value);
return this;
}
protected constructor(protected readonly value: A) {}
// 정적 생성자들
static of<A>(a: A): Maybe<A> {
return new Just(a);
}
static just<A>(a: A): Maybe<A> {
return new Just(a);
}
static nothing<A = never>(): Maybe<A> {
return new Nothing();
}
}
class Just<A> extends Maybe<A> {
isJust(): this is Just<A> {
return true;
}
isNothing(): this is Nothing {
return false;
}
map<B>(f: (a: A) => B): Maybe<B> {
return new Just(f(this.value));
}
flatMap<B>(f: (a: A) => Maybe<B>): Maybe<B> {
return f(this.value);
}
}
class Nothing extends Maybe<any> {
constructor() {
super(undefined);
}
isJust(): this is Just<never> {
return false;
}
isNothing(): this is Nothing {
return true;
}
map<B>(_f: (a: never) => B): Maybe<B> {
return this;
}
flatMap<B>(_f: (a: never) => Maybe<B>): Maybe<B> {
return this;
}
}
정말 간단하다.
Maybe<S>
모나드는 값이 존재하는 경우엔 Just<S>
를 존재하지 않는 경우엔 Nothing
모나드를 반환한다.
Just<S>
모나드는 map,flatMap
을 통해 내부 값을 변경 해나가지만 Nothing
의 경우엔 자기 자신만을 반환한다.
시나리오를 통한 Maybe 모나드 사용 예시
이런 시나리오를 생각해보자
UserId
를 통해 사용자를 유저 데이터를 조회한다. 이 때User
값이 있을 수도,null
일 수도 있다.- 값이
User
라면 해당 값이 회원 가입시 입력한 생년월일 정보를 추출한다. 입력한 생년월일 정보가 있다면Date
없다면 이 값은null
이다. - 입력한 생년 월일 정보가 유효한지 확인한다. 유효하다면 검증된 값인
Date
를 반환하고 유효하지 않다면null
을 반환한다. Date
값을 문자열로 포맷한다.- 포맷한 문자열을 한국말로 변경한다.
- 한국말을 로깅한다.
1,2,3 단계의 일들은 모두 비결정적이며 이전 단계의 결과값에 따라 호출 유무가 결정된다.
4번 단계부턴 모든 이전 단계들이 호출 된 경우에만 호출되어야 한다.
type UserId = string;
interface User {
id: UserId;
name: string;
birthday?: Date | null;
}
// 1~6 단계 함수들: 내부 구현 없이 선언만 (타입 예시용)
function findUser(id: UserId): Maybe<User>; // 1. 사용자 조회
function extractBirthday(user: User): Maybe<Date>; // 2. 생일 추출
function validatePast(date: Date): Maybe<Date>; // 3. 과거 검증
function formatDate(date: Date): string; // 4. 문자열 포맷
function toKoreanLabel(dateStr: string): string; // 5. 라벨 변환
function logResult(text: string): void; // 6. 로깅 부수효과
function getBirthdayLabel(id: UserId): Maybe<string> {
return findUser(id) // 1. 유저 조회
.flatMap(extractBirthday) // 2. 생일 추출
.flatMap(validatePast) // 3. 과거 검증
.map(formatDate) // 4. 문자열 포맷
.map(toKoreanLabel) // 5. 라벨 변환
.tap(logResult); // 6. 로깅 부수효과
}
findUser
로 부터 시작한 Maybe<User>
는 각 단계에서 null/undefined
와 같이 값이 존재하지 않는 가능성을 안전하게 감싼 채 Maybe
모나드로 변환되며 원하는 시나리오에 맞춰 순차적으로 실행된다.
각 과정에서 한 번이라도 값이 존재하지 않는 Nothing
모나드가 된다면 이후 단계들은 실행되지 않고 최종적으로 Nothing
값이 반환된다.
이 과정을 통해 중간마다 조건문을 통한 체크를 반복할 필요 없이 map,flatMap
을 통한 변환 선언 순서 만으로 데이터의 흐름을 예측 할 수 있다.
자동으로 추론되는 데이터의 타입들