Skip to content

Chapter 08: 컨테이너

강력한 컨테이너

http://blog.dwinegar.com/2011/06/another-jar.html

데이터를 순수 함수 파이프라인으로 처리하는 방법을 살펴봤습니다. 이는 선언적인 동작 명세입니다. 하지만 제어 흐름, 에러 처리, 비동기 액션, 상태 관리, 그리고 부수 효과는 어떻게 다룰까요? 이번 장에서는 이러한 추상화의 기본 토대를 탐구합니다.

먼저 모든 타입의 값을 담을 수 있는 컨테이너를 생성합니다. 타피오카 푸딩만 담는 지퍼락은 거의 쓸모가 없습니다. 객체로 만들겠지만 OOP 개념의 프로퍼티와 메서드를 부여하지 않을 것입니다. 이는 우리의 소중한 데이터를 품는 특수한 상자로 취급할 것입니다.

js
class Container {
  constructor(x) {
    this.$value = x;
  }
  
  static of(x) {
    return new Container(x);
  }
}

첫 번째 컨테이너는 Container로 명명합니다. new 키워드 사용을 피하기 위해 Container.of 생성자를 사용합니다. of 함수는 단순히 값을 컨테이너에 담는 역할을 합니다.

새로 만든 컨테이너를 살펴봅시다...

js
Container.of(3);
// Container(3)

Container.of('hotdogs');
// Container("hotdogs")

Container.of(Container.of({ name: 'yoda' }));
// Container(Container({ name: 'yoda' }))

Node에서는 Container(x){$value: x}로 표시되지만 실제 구조를 이해하는 것이 중요합니다. 개념 설명을 위해 inspect 메서드 오버라이드가 있다고 가정합니다.

진행 전 명확히 할 사항:

  • Container는 하나의 프로퍼티($value)를 가진 객체입니다. 대부분 단일 값 보관을 하지만 이에 국한되지 않습니다.

  • $value는 특정 타입에 구애받지 않습니다.

  • 값이 컨테이너에 삽입되면 그 상태를 유지합니다. .value 접근은 컨테이너 목적을 훼손합니다.

이렇게 하는 이유는 후반부에서 명확해질 것입니다.

나의 첫 번째 펑터

컨테이너 내부 값에 함수 적용을 위해 map 메서드를 도입합니다.

js
// (a -> b) -> Container a -> Container b
Container.prototype.map = function (f) {
  return Container.of(f(this.$value));
};

배열의 map 메서드와 유사하지만 Container a 타입에서 동작합니다:

js
Container.of(2).map(two => two + 2); 
// Container(4)

Container.of('flamethrowers').map(s => s.toUpperCase()); 
// Container('FLAMETHROWERS')

Container.of('bombs').map(append(' away')).map(prop('length')); 
// Container(10)

값을 컨테이너에 보관한 채로 연속적 함수 적용이 가능합니다. 타입 변환도 동시에 가능합니다.

map을 계속 호출하면 함수 합성과 같은 효과 발생. 이를 가능케 하는 수학적 원리가 펑터입니다.

펑터는 map을 구현하고 특정 법칙을 준수하는 타입입니다.

펑터는 카테고리 이론에서 유래한 인터페이스 계약입니다. 실용적 관점에서 직관과 활용법을 살펴봅니다.

map을 통한 함수 적용 추상화가 제공하는 강력한 이점을 이해합니다.

슈뢰딩거의 Maybe

cool cat, need reference

기본 컨테이너를 확장한 Maybe를 도입합니다. 부록 B 참조

전체 구현은 부록 B 참조

js
class Maybe {
  static of(x) {
    return new Maybe(x);
  }

  get isNothing() {
    return this.$value === null || this.$value === undefined;
  }

  constructor(x) {
    this.$value = x;
  }

  map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this.$value));
  }

  inspect() {
    return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
  }
}

Maybe는 값 존재 여부를 체크하여 널 안전성을 제공합니다(교육용 단순화 구현 참고).

js
Maybe.of('Malkovich Malkovich').map(match(/a/ig));
// Just(True)

Maybe.of(null).map(match(/a/ig));
// Nothing

Maybe.of({ name: 'Boris' }).map(prop('age')).map(add(10));
// Nothing

Maybe.of({ name: 'Dinah', age: 14 }).map(prop('age')).map(add(10));
// Just(24)

널 값 위에서도 안전하게 함수 적용이 가능합니다.

포인트프리 스타일 적용을 위해 맵 체이닝 사용:

js
// map :: Functor f => (a -> b) -> f a -> f b
const map = curry((f, anyFunctor) => anyFunctor.map(f));

타입 서명 Functor f =>은 펑터 제약을 표현합니다.

사용 사례

결과 반환 실패 가능 함수에 Maybe 적용 예시:

js
// safeHead :: [a] -> Maybe(a)
const safeHead = xs => Maybe.of(xs[0]);

// streetName :: Object -> Maybe String
const streetName = compose(map(prop('street')), safeHead, prop('addresses'));

streetName({ addresses: [] });
// Nothing

streetName({ addresses: [{ street: 'Shady Ln.', number: 4201 }] });
// Just('Shady Ln.')

safeHead는 널 안전성을 제공하며 체인 구조에서 명시적 에러 처리를 강제합니다.

명시적 실패 신호를 위한 Nothing 반환 예시:

js
// withdraw :: Number -> Account -> Maybe(Account)
const withdraw = curry((amount, { balance }) =>
  Maybe.of(balance >= amount ? { balance: balance - amount } : null));

// This function is hypothetical, not implemented here... nor anywhere else.
// updateLedger :: Account -> Account 
const updateLedger = account => account;

// remainingBalance :: Account -> String
const remainingBalance = ({ balance }) => `Your balance is $${balance}`;

// finishTransaction :: Account -> String
const finishTransaction = compose(remainingBalance, updateLedger);


// getTwenty :: Account -> Maybe(String)
const getTwenty = compose(map(finishTransaction), withdraw(20));

getTwenty({ balance: 200.00 }); 
// Just('Your balance is $180')

getTwenty({ balance: 10.00 });
// Nothing

에러 발생 시 후속 연산 중단으로 트랜잭션 무결성 보장

값 추출

프로그램 실행 종착점에서 효과 발생 필요성 설명

컨테이너 내 값 유지를 통한 안전한 처리 철학 강조

maybe 헬퍼 함수를 통한 값 추출 방법

js
// maybe :: b -> (a -> b) -> Maybe a -> b
const maybe = curry((v, f, m) => {
  if (m.isNothing) {
    return v;
  }

  return f(m.$value);
});

// getTwenty :: Account -> String
const getTwenty = compose(maybe('You\'re broke!', finishTransaction), withdraw(20));

getTwenty({ balance: 200.00 }); 
// 'Your balance is $180.00'

getTwenty({ balance: 10.00 }); 
// 'You\'re broke!'

maybe를 이용한 조건 분기 처리 패턴

Maybe 도입 초기 적응 과정과 장기적 이점 설명

안전성 확보에 대한 비유적 표현

실제 구현에서는 Some/None 또는 Just/Nothing 타입 분리 설명

순수 에러 처리

pick a hand... need a reference

Either 타입을 통한 예우 있는 에러 처리 메커니즘 도입

전체 구현은 부록 B 참조

js
class Either {
  static of(x) {
    return new Right(x);
  }

  constructor(x) {
    this.$value = x;
  }
}

class Left extends Either {
  map(f) {
    return this;
  }

  inspect() {
    return `Left(${inspect(this.$value)})`;
  }
}

class Right extends Either {
  map(f) {
    return Either.of(f(this.$value));
  }

  inspect() {
    return `Right(${inspect(this.$value)})`;
  }
}

const left = x => new Left(x);

Left/Right 서브타입 구조 설명

js
Either.of('rain').map(str => `b${str}`); 
// Right('brain')

left('rain').map(str => `It's gonna ${str}, better bring your umbrella!`); 
// Left('rain')

Either.of({ host: 'localhost', port: 80 }).map(prop('host'));
// Right('localhost')

left('rolls eyes...').map(prop('host'));
// Left('rolls eyes...')

Left의 에러 캡슐화 동작 방식 설명

생일 유효성 검사 사례 연구

js
const moment = require('moment');

// getAge :: Date -> User -> Either(String, Number)
const getAge = curry((now, user) => {
  const birthDate = moment(user.birthDate, 'YYYY-MM-DD');

  return birthDate.isValid()
    ? Either.of(now.diff(birthDate, 'years'))
    : left('Birth date could not be parsed');
});

getAge(moment(), { birthDate: '2005-12-12' });
// Right(9)

getAge(moment(), { birthDate: 'July 4, 2001' });
// Left('Birth date could not be parsed')

Either 타입을 통한 에러 메시지 전파 구조

js
// fortune :: Number -> String
const fortune = compose(concat('If you survive, you will be '), toString, add(1));

// zoltar :: User -> Either(String, _)
const zoltar = compose(map(console.log), map(fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// Right(undefined)

zoltar({ birthDate: 'balloons!' });
// Left('Birth date could not be parsed')

throw 대비 Either의 차이점 설명

점 표기법 vs 포인트프리 스타일 비교

리프팅(lifting) 개념을 통한 함수 변환 설명

Either의 다용도 활용 제안

Either의 이론적 배경(합 타입) 간략 소개

either 헬퍼 함수 사용 예시

js
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = curry((f, g, e) => {
  let result;

  switch (e.constructor) {
    case Left:
      result = f(e.$value);
      break;

    case Right:
      result = g(e.$value);
      break;

    // No Default
  }

  return result;
});

// zoltar :: User -> _
const zoltar = compose(console.log, either(id, fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// undefined

zoltar({ birthDate: 'balloons!' });
// 'Birth date could not be parsed'
// undefined

에러 처리 파이프라인 완성 예시

이펙트 처리

dominoes.. need a reference

IO 타입을 통한 부수 효과 지연 처리 예시

js
// getFromStorage :: String -> (_ -> String)
const getFromStorage = key => () => localStorage[key];

함수 래핑을 통한 순수성 유지 기법

IO 타입 필요성 제기

js
class IO {
  static of(x) {
    return new IO(() => x);
  }

  constructor(fn) {
    this.$value = fn;
  }

  map(fn) {
    return new IO(compose(fn, this.$value));
  }

  inspect() {
    return `IO(${inspect(this.$value)})`;
  }
}

IO의 핵심 구조 설명(함수 래핑 메커니즘)

IO 사용 예시:

javascript
// 예시 코드
const ioWindow = IO(() => window);
js
// ioWindow :: IO Window
const ioWindow = new IO(() => window);

ioWindow.map(win => win.innerWidth);
// IO(1430)

ioWindow
  .map(prop('location'))
  .map(prop('href'))
  .map(split('/'));
// IO(['http:', '', 'localhost:8000', 'blog', 'posts'])


// $ :: String -> IO [DOM]
const $ = selector => new IO(() => document.querySelectorAll(selector));

$('#myDiv').map(head).map(div => div.innerHTML);
// IO('I am some inner html')

도미노 체인 비유를 통한 지연 실행 설명

펑터 법칙에 근거한 직관적 사용 강조

실행 책임의 소비자 귀책 원칙

js
// url :: IO String
const url = new IO(() => window.location.href);

// toPairs :: String -> [[String]]
const toPairs = compose(map(split('=')), split('&'));

// params :: String -> [[String]]
const params = compose(toPairs, last, split('?'));

// findParam :: String -> IO Maybe [String]
const findParam = key => map(compose(Maybe.of, find(compose(eq(key), head)), params), url);

// -- Impure calling code ----------------------------------------------

// run it by calling $value()!
findParam('searchTerm').$value();
// Just(['searchTerm', 'wafflehouse'])

컨테이너 중첩 사용 사례(IO(Maybe([x])))

unsafePerformIO 명명으로 위험성 강조

js
class IO {
  constructor(io) {
    this.unsafePerformIO = io;
  }

  map(fn) {
    return new IO(compose(fn, this.unsafePerformIO));
  }
}

명시적 실행 메서드 호출 예시

Task 타입 소개 예고

비동기 태스크

콜백 지옥 해결책으로서의 Task 도입

Folktale 라이브러리 예시 코드

js
// -- Node readFile example ------------------------------------------

const fs = require('fs');

// readFile :: String -> Task Error String
const readFile = filename => new Task((reject, result) => {
  fs.readFile(filename, (err, data) => (err ? reject(err) : result(data)));
});

readFile('metamorphosis').map(split('\n')).map(head);
// Task('One morning, as Gregor Samsa was waking up from anxious dreams, he discovered that
// in bed he had been changed into a monstrous verminous bug.')


// -- jQuery getJSON example -----------------------------------------

// getJSON :: String -> {} -> Task Error JSON
const getJSON = curry((url, params) => new Task((reject, result) => {
  $.getJSON(url, params, result).fail(reject);
}));

getJSON('/video', { id: 10 }).map(prop('title'));
// Task('Family Matters ep 15')


// -- Default Minimal Context ----------------------------------------

// We can put normal, non futuristic values inside as well
Task.of(3).map(three => three + 1);
// Task(4)

map을 통한 비동기 값 처리 패턴

프로미스와의 유사점/차이점 설명

IO와 Task의 관계 설명

fork 메서드를 통한 비동기 실행 메커니즘

js
// -- Pure application -------------------------------------------------
// blogPage :: Posts -> HTML
const blogPage = Handlebars.compile(blogTemplate);

// renderPage :: Posts -> HTML
const renderPage = compose(blogPage, sortBy(prop('date')));

// blog :: Params -> Task Error HTML
const blog = compose(map(renderPage), getJSON('/posts'));


// -- Impure calling code ----------------------------------------------
blog({}).fork(
  error => $('#error').html(error.message),
  page => $('#main').html(page),
);

$('#spinner').show();

비동기 작업 흐름의 선형성 강조

가독성 개선 효과 설명

Task의 에러 처리 통합 기능 설명

동기/비동기 혼용 시나리오 예시

js
// Postgres.connect :: Url -> IO DbConnection
// runQuery :: DbConnection -> ResultSet
// readFile :: String -> Task Error String

// -- Pure application -------------------------------------------------

// dbUrl :: Config -> Either Error Url
const dbUrl = ({ uname, pass, host, db }) => {
  if (uname && pass && host && db) {
    return Either.of(`db:pg://${uname}:${pass}@${host}5432/${db}`);
  }

  return left(Error('Invalid config!'));
};

// connectDb :: Config -> Either Error (IO DbConnection)
const connectDb = compose(map(Postgres.connect), dbUrl);

// getConfig :: Filename -> Task Error (Either Error (IO DbConnection))
const getConfig = compose(map(compose(connectDb, JSON.parse)), readFile);


// -- Impure calling code ----------------------------------------------

getConfig('db.json').fork(
  logErr('couldn\'t read file'),
  either(console.log, map(runQuery)),
);

Task 체인 내에서 Either/IO 활용 사례

간결성 선언

모나드 학습 예고

이론적 기반

펑터 법칙 소개

js
// identity
map(id) === id;

// composition
compose(map(f), map(g)) === map(compose(f, g));

항등 법칙 확인 방법

js
const idLaw1 = map(id);
const idLaw2 = id;

idLaw1(Container.of(2)); // Container(2)
idLaw2(Container.of(2)); // Container(2)

합성 법칙 설명

js
const compLaw1 = compose(map(append(' world')), map(append(' cruel')));
const compLaw2 = map(compose(append(' world'), append(' cruel')));

compLaw1(Container.of('Goodbye')); // Container('Goodbye cruel world')
compLaw2(Container.of('Goodbye')); // Container('Goodbye cruel world')

카테고리 이론 연결 고리

범주 매핑 도식화 설명

Categories mapped

Maybe의 카테고리 매핑 사례

펑터 도식의 교환 법칙 설명

functor diagram

구체적 수학적 적용 예시

js
// topRoute :: String -> Maybe String
const topRoute = compose(Maybe.of, reverse);

// bottomRoute :: String -> Maybe String
const bottomRoute = compose(map(reverse), Maybe.of);

topRoute('hi'); // Just('ih')
bottomRoute('hi'); // Just('ih')

시각적 표현 강조

functor diagram 2

리팩토링에의 활용 가능성

펑터 중첩 사례:

js
const nested = Task.of([Either.of('pillows'), left('no sleep for you')]);

map(map(map(toUpperCase)), nested);
// Task([Right('PILLOWS'), Left('no sleep for you')])

펑터 합성의 필요성 제기

js
class Compose {
  constructor(fgx) {
    this.getCompose = fgx;
  }

  static of(fgx) {
    return new Compose(fgx);
  }

  map(fn) {
    return new Compose(map(map(fn), this.getCompose));
  }
}

const tmd = Task.of(Maybe.of('Rock over London'));

const ctmd = Compose.of(tmd);

const ctmd2 = map(append(', rock on, Chicago'), ctmd);
// Compose(Task(Just('Rock over London, rock on, Chicago')))

ctmd2.getCompose;
// Task(Just('Rock over London, rock on, Chicago'))

합성 펑터 구현 예시

요약

다양한 펑터 활용 사례 열거

다중 펑터 처리의 한계와 모나드 학습 예고

Chapter 09: 모나딕 양파

연습 문제

연습해 보세요!

addmap을 사용해 펑터 내부 값을 증가시키는 함수를 작성하세요. // incrF :: Functor f => f Int -> f Int const incrF = undefined;


다음 User 객체가 주어졌을 때:

js
const user = { id: 2, name: 'Albert', active: true };

연습해 보세요!

safeProphead를 사용해 사용자 이름의 첫 이니셜을 찾으세요. // initial :: User -> Maybe String const initial = undefined;


다음 헬퍼 함수들이 주어졌을 때:

js
// showWelcome :: User -> String
const showWelcome = compose(concat('Welcome '), prop('name'));

// checkActive :: User -> Either String User
const checkActive = function checkActive(user) {
  return user.active
    ? Either.of(user)
    : left('Your account is not active');
};

연습해 보세요!

checkActiveshowWelcome을 사용해 접근 권한 부여/에러 반환 함수를 작성하세요. // eitherWelcome :: User -> Either String String const eitherWelcome = undefined;


다음 함수들을 고려하세요:

js
// validateUser :: (User -> Either String ()) -> User -> Either String User
const validateUser = curry((validate, user) => validate(user).map(_ => user));

// save :: User -> IO User
const save = user => new IO(() => ({ ...user, saved: true }));

연습해 보세요!

사용자 이름 유효성 검사(validateName) 및 회원가입 처리(register) 함수를 작성하세요. either의 두 인자는 동일 타입 반환 필요 // validateName :: User -> Either String () const validateName = undefined; // register :: User -> IO String const register = compose(undefined, validateUser(validateName));