관리 메뉴

틈틈히 메모중

[#5] 복잡성을 줄이는 디자인 패턴 본문

함수형 프로그래밍/함수형 자바스크립트

[#5] 복잡성을 줄이는 디자인 패턴

개피곤씨 2023. 9. 6. 23:38

 🧡 명령형 에러 처리 체계의 문제점   

try-catch문으로 예외처리를 하는 것은 
  1. 함수 합성과 체이닝을 할수 없게 하고
  2. 동일 input일때 동일한 output을 낸다는 참조 투명성원리에 위배되며,
  3. 부수효과발생가능성
  4. 에러발생시 호출지점을 벗어나 비지역성 non-locality원리에 위배
  5. catch문안에서 특정 예외를 처리하는데에도 에너지를 써야 하기 때문에 호출자 부담 증가 
  6. 에러 조건 처리 블록들이 중첩되어 사용하기 어려움 

예외처리는 적재적소에 쓰면 가장 좋다. 

 

 🧡 컨테이너로 잘못된 데이터 접근을 차단    

값을 컨테이너화 해서 값을 안전하게 다루고, 프로그램 불변성이 지켜지도록  직접적인 접근 차단하기 

//값을 함수형 자료형으로 감싸기 
class Wrapper {
	constructor(value){
    	this._value = value;
        }
    //map :: (a->b) -> a -> b
    map(f){
    	return f(this._value);
        }
    toString(){
    	return `Wrapper ( ${this._value} )`;
        }
}

//wrap :: a -> Wrapper(a);
const wrap = value => new Wrapper(value);

const 실험1 = wrap('함수형가즈아');
실험1.map(R.indentity); // '함수형가즈아 

const 실험2 = wrap(null);
실험2.map(doWork); // doWork가 null체크 해야 null값 거를수있음

 위 함수에서 map을 변형하여, 함수 호출전에 null이나 빈문자열 등 체크해보기 

//Wrapper클래스 안에 추가하기 
//fmap :: (a -> b) -> Wrapper[a] -> Wrapper[b]
fmap(f){
	return new Wrapper(f(this.value));
    }

fmap 은 "함수자"이다.
fmap은 함수와 (a->b),  함수자(감싼 컨텍스트 Wrapper(a)를 받아서 새로운 함수자 Wrapper(b)를 반환함

 🧡 함수자 를    자료 변환 도구로 활용  

const add = R.curry((a,b) =>  a + b);
const add3 = add(3);

const two = wrap(2);

const five = two.fmap(add3); //Wrapper(5)
five.map(R.identity) // 5

//fmap은 같은 형식을 반환함 = 체이닝할수있는 조건 만족

 

map, filter도 결국 함수자이다. 

함수자가 되려면 
1. 부수효과가 없어야 함
2. 합성이 가능해야함 
3. mapping기능을 제공해야함 (함수자 안에 있는 값을 함수로 변환할수있어야 함)

 🧡모나드는 합성을 촉진하는 자료형   

모나드 ? 순수함수에서 side effect를 다루고 값의 흐름을 추상화 하기 위한 디자인 패턴, 모델
  - 값을 감싸고 있어 컨테이너처럼 동작함 
  - 값에 대한 연산을 순차적으로 캡슐화 하고, 각 연산의 결과를 다음연산에 전달하는 방식으로 흐름을 추상화함 
  - 결과를 다시 모나드로 반환함 
  - 일정 법칙을 따름 
=> Maybe모나드, Either모나드, Promise모나드, State모나드...

 

 

 

 🧡 에러 처리 전략을 모나드 형에 통합   

Maybe 모나드 ? 값이 존재할수도 있고, 존재 하지 않을수도 있는 상황을 다룸 (널 포인터 예외를 방지하기)

// Maybe 모나드 구현
const Maybe = (value) => ({
  map: (fn) => (value ? Maybe(fn(value)) : Maybe(null)),
  value: () => value,
});

// 사용 예시
const user = { name: 'Alice', age: 30 };
const getName = (user) => user.name;

const maybeUser = Maybe(user);
const maybeName = maybeUser.map(getName);

console.log(maybeName.value()); // 'Alice'
//Maybe모나드와 그 하위형 Just, Nothing

class Maybe{
 static just(a){//주어진 값을 포함하는 Just모나드 생성
 	return new Just(a);
    }
    
 static nothing(){ //아무것도 없는 Nothing모나드 생성
 	return new Nothing();
    }
 
 static fromNullable(a){ //값이 존재하면 Just 모나드를, 그렇지 않으면 Nothing 모나드를 생성
 	return a != null ? Maybe.just(a) : Maybe.nothing();
    }
 
 static of(a){ //just(a)와 동일한 역할
 	return just(a);
    }
 
 get isNothing() {//현재 모나드가 Nothing인지 여부를 나타내는 속성
 	return false;
    }
 
 get isJust(){ //현재 모나드가 Just인지 여부를 나타내는 속성
 	return false;
    }
}

class Just extends Maybe{
	constructor(value){//Just 모나드를 생성할 때 사용하는 생성자입니다. 값을 포함
    	super();
        this._value = value;
        }
    
    get value(){ //현재 Just 모나드에 포함된 값을 반환
    	return this._value;
        }
    
    map(f){ //현재 Just 모나드에 함수를 적용하고, 그 결과를 새로운 Just 모나드로 감싸서 반환
    	return Maybe.fromNullable(f(this._value));
        }
    
    getOrElse(){//현재 Just 모나드에 포함된 값을 반환
    	return this._value;
        }
        
    filter(f){//현재 Just 모나드에 필터 함수를 적용하고, 조건을 만족하지 않으면 Nothing 모나드를 반환
    	Maybe.fromNullabe(f(this._value) ? this._value : null);
        }
    
    chain(f){//현재 Just 모나드에 함수를 적용하고, 그 결과를 반환
    	return f(this._value);
        }
        
    toString(){
    	return `Maybe.Just(${this._value})`;
        }
 }
        
class Nothing extends Maybe{
	map(f){ //현재 Nothing 모나드를 그대로 반환
    	return this;
        }
    
    get value(){ //Nothing 모나드는 값을 포함하지 않으므로 값을 가져올 수 없다고 표시해줌
    	throw new TypeError('Nothing값을 가져올수 없습니다.');
        }
     
    getOrElse(other){  //대체값(other)을 반환
    	return other;
        }
        
    filter(f){ //값이 존재하고, 주어진 함수를 만족하면 해당 값이 담긴 Just반환, 그외에는 Nothing반환
    	return this._value;
        }
        
    chain(f){ //현재 Nothing 모나드를 그대로 반환
    	return this;
        }
     
     toString(){
     	return `Maybe.Nothing`;
        }
 }

 


Either 모나드 ? 두가지 중 하나의 값을 나타냄 ( 예외처리와 오류관리에 유용 )

// Either 모나드 구현
const Left = (value) => ({
  map: (fn) => Left(value),
  value: () => value,
});

const Right = (value) => ({
  map: (fn) => Right(fn(value)),
  value: () => value,
});

// 사용 예시
const divide = (a, b) => (b === 0 ? Left('Division by zero') : Right(a / b));

const result1 = divide(10, 2); // Right(5)
const result2 = divide(10, 0); // Left('Division by zero')

console.log(result1.value()); // 5
console.log(result2.value()); // 'Division by zero'
class Either{
	constructor(value){
    	this._value = value;
        }
    
    get value)(){
    	return this._value;
        }
        
    static left(a){
    	return new Left(a);
        }
    
    static right(a){
    	return new Right(A);
        }
        
    static fromNullable(val){
    	return val !== null && val !== undefined ? Either.right(val) : Either.left(val);
        }
        
    static of(a){
    	return Either.right(a);
        }
}

class Left extends Either {
	map(_){
    	return this; //쓰지 않음
        }
    
    get value(){
    	throw new TypeError("Left(a) 값을 가져올 수 없습니다.");
        }
      
    getOrElse(other){
    	return other;
        }
    
    orElse(f){
    	return f(this._value);
        }
        
    chain(f){
    	return this;
        }
        
     getOrElseThrow(a){
     	throw new Error(a);
        }
        
     filter(f){
     	return this;
        }
     
     toString(){
     	return `Either.Left(${this._value})`;
        }
}


class Right extends Either {
	map(_){
    	return Either.of(f(this._value));
        }

    getOrElse(other){
    	return this._value; 
        }
    
    orElse(f){
    	return this; // 쓰지 않음 
        }
        
    chain(f){
    	return f(this._value);
        }
        
     getOrElseThrow(_){
     	return this._value;
        }
        
     filter(f){
     	return Either.fromNullable(f(this._value) ? this._value : null);
        }
     
     toString(){
     	return `Either.Right(${this._value})`;
        }
}

 

 

Promise 모나드 ? 비동기 작업을 추상화함 (js의 내장 Promise객체가 Promise모나드의 예시 )

// Promise 모나드 사용
const fetchData = () => new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Data fetched successfully');
  }, 1000);
});

fetchData()
  .then((data) => {
    console.log(data); // 'Data fetched successfully'
  })
  .catch((error) => {
    console.error(error);
  });

 

 

State 모나드 ? 상태(state)를 가지고 있고, 그 상태를 변환하는데 사용(불변성 유지에 유용)

// State 모나드 구현
const State = (updateFn) => ({
  map: (fn) => State((state) => {
    const [newValue, newState] = updateFn(state);
    return [fn(newValue), newState];
  }),
});

// 사용 예시
const increment = State((state) => [state + 1, state]);

const [result, newState] = increment
  .map((value) => value * 2)
  .map((value) => value + 10)
  .map((value) => value - 5)
  .map((value) => value ** 2)
  .map((value) => value.toString());

console.log(result); // '36'

 

 

 

 🧡 모나드형의 교차 배치 및 합성   

const map = R.curry((f, container) => container.map(f);

const chain = R.curry((f, container) => container.chain(f);