Peter Cho

함수형 자바스크립트 | 루이스 아텐시오

2018.09.16 | 14 Minute Read

요약

  • 함수형 프로그래밍은 자료구조를 새로 만들어 어떤 요건을 충족시키는 게 아니라, 배열 등의 흔한 자료구조를 이용해 다수의 굵게 나뉜 고차 연산을 적용합니다.
    • 기능이 기술된 함수를 인수로 받는 다.
    • 수동 루프를 함수로 대체한다.(map, filter 등)

서론

코드가 복잡해지지 않게 하려면 어떻게 해야 할까요? 복잡성을 다스리는 비결은, 코드베이스의 크기가 커질수록 복잡성이 증가하지 않도록 붙잡아 두는 것입니다. 바로 여기서 함수형 프로그래밍이 요긴하게 쓰이지요.

자료구조는 적게, 일은 더 많이

어플리케이션의 제어 흐름

프로그램이 정답에 이르기까지 거치는 경로를 제어흐름이라고 합니다.

명령형 프로그램은 작업 수행에 필요한 전 단계를 노출하여 흐름이나 경로를 아주 자세히 서술합니다. 보통 작업을 수행하는 단계는 루프와 분기문, 구문마다 값이 바뀌는 변수들로 빼곡히 들어찬다.

let loop = optC()
while (loop) {
  const condition = optA()
  if (condition) {
    optB1()
  } else {
    optB2()
  }
  loop = optC()
}
optD()

반면, 선언적 프로그래밍, 특히 함수형 프로그래밍은 독립적인 블랙박스 연산들이 단순하게, 즉 최소한의 제어 구조를 통해 연결되어 추상화 수준이 높습니다. 실제로 함수형 프로그램은 데이터제어 흐름 자체를 고수준 컴포넌트 사이의 단순한 연결로 취급합니다.

덕분에 다음과 같이 코드가 짧아집니다.

optA().optB().optC().optD()

메서드 체이닝

메서드 체이닝은 여러 메서드를 단일 구문으로 호출하는 OOP 패턴입니다. 메서드가 모두 동일한 객체에 속해 있으며 메서드 흘리기라고도 합니다.

// OOP
'Functional Programming'.substring(0, 10).toLowerCase() + ' is fun'

// FP
concat(toLowerCase(substring('Functional Programming', 1, 10))), ' is fun')

이렇게 함수 코드를 안쪽에서 바깥쪽으로 작성하면 메서드 체이닝 방식만큼 매끄럽지 못합니다. 로직을 파악하려면 가장 안쪽에 감싼 함수부터 한 꺼풀씩 벗겨내야 하고 가독성도 현저히 떨어지지요.

함수 체이닝

객체지향 프로그램은 주로 상속을 통해 코드를 재사용합니다. 순수 객체지향 언어에서, 특히 언어 자체의 자료구조를 구현한 코드를 보면 이런 패턴이 자주 눈에 띕니다. 가령 자바에는 List 인터페이스를 용도에 맞게 달리 구현한 ArrayList, LinkedList, DoublyLinkedList, CopyOnWriteArrayList 등이 있습니다. 이들은 모두 한 부도에서 출발하여 나름대로 특수한 기능을 덧붙인 클래스입니다.

함수형 프로그래밍은 접근 방법이 다릅니다. 자료구조를 새로 만들어 어떤 요건을 충족시키는 게 아니라, 배열 등의 흔한 자료구조를 이용해 다수의 굵게 나뉜 고계 연산을 적용합니다. 이러한 고계 연산으로 다음과 같은 일을 합니다.

  • 작업을 수행하기 위해 무슨 일을 해야 하는지 기술된 함수를 인수로 받습니다.
  • 임시 변수의 값을 계속 바꾸면서 부수효과를 일으키는 기존 수동 루프를 대체합니다. 그 결과 관리할 코드가 줄고 에러가 날 만한 코드 역시 줄어듭니다.

복잡성을 줄이는 디자인 패턴

함수형 프로그래밍이 수치에 관한 학술적 문제만을 다루는 패러다임이라서 실세계에서 맞닥뜨리는 실패 가능성에 대해선 거의 관심이 없다고 오해하는 사람들이 있습니다. 하지만 최근 수년 동안, 외려 함수형 프로그래밍이 다른 프로그래밍보다 에러를 더 깔끔하게 잘 처리한다는 사실이 발혀졌습니다.

프로그램 실행 중 예외가 나거나 네트워크 연결이 끊기는 등 예기치 않는 사고로 인해 데이터가 null, undefined로 세팅되면 갖가지 골치 아픈 문제가 생깁니다. 그래서 언제 발생할지 모를 이슈를 대비해 null체크문을 넣는 라 코드는 어쩔 수 없이 점점 복잡해집니다.

그 결과 훨씬 더 복잡하게 꼬인 코드만 양산됩니다. 애플리케이션 덩치가 커지고 복잡도가 커질수록 확장하기도 어렵고 알 수 없는 수수께끼 코드로 가즉해진다.

함수 매핑이 가능한 단순 자료형을 생성하는 함수자(Functor)라는 개념을 소개합니다. 다양한 방식으로 에러를 처리하는 로직이 들어 있는 모나드(Monad)라는 자료형에 함수자를 적용합니다.

모나드는 함수형 프로그래밍에서 가장 난해한 개념 중 하나로, 범주론이라는 수학 분야에서 비롯된 결과물입니다.

try-catch 에러 처리

명령형 자바스크립트 코드에서 예외를 붙잡아 던지는 방법은 단점이 많고 함수형 설계와도 잘맞지 않습니다. 예외를 던지는 함수의 특징은 다음과 같습니다.

  • 다른 함수형 장치처럼 합성이나 체이닝을 할 수 없습니다.
  • 예외를 던지는 행위는 함수 호출에서 빠져나갈 구멍을 찾는 것이므로 단일한, 예측 가능한 값을 지향하는 참조 투명성 원리에 위배됩니다.
  • 예기치 않게 스택이 풀리면 함수 호출 범위를 벗어나 전체 시스템에 영향을 미치는 부수 효과를 일으킵니다.

null 체크라는 고질병

뜻밖의 함수 호출이 실패하는 것보다, 차라리 null을 돌려받으면 적어도 함수를 한군데로 흘러가게 할 수는 있습니다. 하지만 나아질 건 조금도 없습니다. 함수가 null을 반환하면 이 함수를 부른 호출자는 성가신 null 체크를 해야 하는 부담을 떠안습니다.

const getCountry = (student) => {
  const school = student.getSchool()
  if (school !== null) {
    const addr = school.getAddress()
    if (addr !== null) {
      const country = addr.getCountry()
      return country
    }
    return null 
  }
  throw new Error('국가 조회 중 에러 발생!')
}

이 함수는 기껏해야 객체 속성을 얻는 기능이 전부라서 더 간단히 구현해야 합니다. try-catch나 null 체크 코드로 감싸는 건 겁쟁이나 하는 짓입니다. 판박이 코드를 안 쓰고도 에러를 멋지게 처리할 방법은 없을까요?

더 나은 방안: 함수자

함수형 에러 처리는 철저히 다른 방법으로 접근해 소프트웨어 시스템의 난관을 해결합니다. 기본 아이디어는 비슷합니다. 잠재적으로 위험한 코드 주위에 안정망(말하자면 컨테이너)을 설치하는 것입니다.

함수형 프로그램에서는 위함헌 코드를 감싼다는 개념은 그대로 가져가되 try-catch 블록은 제거할 수 있습니다. 이것이 명령형과 가장 큰 차이점입니다. 함수형 자료형을 사용하여 불순함과의 분리를 일급 시민으로 만드는 것이지요.

불안전한 값을 감쌈

값을 컨테이너화하는 행위는 함수형 프로그래밍의 기본 디자인 패턴입니다. 값을 안정적으로 다루고 불변성을 지키기 위해 직접 접근을 차단하는 것입니다. 이렇게 감싼 값에 접근하는 유일한 방법은 연산을 컨테이너에 매핑하는 것입니다.

Wrapper라는 단순 자료형을 만들어 개념을 좀 더 구체적으로 알아봅시다. 형식은 단순하지만 그 바탕에 깔려 있는 원리는 실로 강력하고, 기초가 되는 내용이니 꼭 이해 해야 합니다.

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 = (val) => new Wrapper(val)

요점은 에러가 날지 모를 값을 래퍼 객체로 감싼다는 것입니다. 어떤 값이 컨테이너 속으로 들어가면 절대로 값을 직접 조회/변경할 수 없습니다.

올바른 값을 넣어 확인해봅시다.

const wrappedValue = wrap('Get Functional')
wrappedValue.map(R.identity) // => 'Get Functional'
wrappedValue.map(console.log)
wrappedValue.map(R.toUpper) // => 'GET FUNCTIONAL'

이 단순한 아이디어 덕분에, 컨테이너 안에 넣어 보호된 값을 얻고 싶은 코드는 무조건 Wrapper.map을 통해서만 컨테이너 내부에 손을 뻗칠 수 있는 구조로 만들 수 있습니다.

다음은 map을 변형한 fmap 함수입니다.

// fmap :: (A -> B) -> Wrapper[A] -> Wrapper[B]
// 변환된 값을 호출부에 반환하기 전에 컨테이너로 감쌉니다.
fmap (f) {
  return new Wrapper(f(this._value))
}

fmap은 주어진 함수를 콘텍스트로 감싼 값에 적용하는 방법이 구현된 함수입니다. 먼저 컨테이너를 열고 그 안에 보관된 값에 주어진 함수를 적용한 다음, 그 결과를 동일한 형식의 새 컨테이너에 넣고 닫는 것으로 마무리하지요. 이런 함수를 함수자라고 합니다.

함수자의 세계로

함수자는 값을 래퍼 안으로 승급(lifting)한 다음 수정하고 다시 래퍼에 넣을 목적을 염두에 둔 함수 매핑이 가능한 자료구조입니다. fmap 을 일반적으로 정의하면 다음과 같습니다.

fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)

fmap 함수는 함수(A -> B)와 함수자(감싼 콘텍스트) Wrapper(A)를 받아 새로운 함수자 Wrapper(B)를 반환합니다. 이렇게 반환된 함수자에는 주어진 함수를 값에 적용한 후 다시 래퍼로 감싼 결과가 담겨 있습니다. fmap은 꼭 렌즈랑 비슷해서 호출할 때마다 컨테이너를 새로 복사 후 반환하는 불변 연산을 수행합니다.

const plus = _.curry((a, b) => a + b);
const plus3 = plus(3);
const two = wrap(2);
const five = two.fmap(plus3);
five.map(_.identity);

결국 함수자로는 예외를 던지거나, 원소를 바꾸거나, 함수 로직을 변경하는 일 따위는 할 수 없습니다. 콘텍스트를 생성 또는 추상하여 원본값을 바꾸지 않는 상태로 안전하게 값을 꺼내어 연산을 수행하는 것이 함수자의 존재 이유입니다. 함수자는 null 데이터를 다루는 법을 알지 못하므로 그 자체로는 별로 매력이 없습니다. 함수자는 한 형식의 함수를 다른 형식의 함수로 매핑합니다. 더 구체적인 동작은 모나드라는 함수형 자료형에서 일어납니다. 모나드는 그 무엇보다 능률적으로 코드 에러를 처리해서 물 흐르듯 매끄럽게 함수 합성을 가능케 합니다.

모나드를 응용한 함수형 에러 처리

모나드를 함수형 프로그램에 응용하면 앞서 언급한 전통적인 에러 처리의 문제점을 일거에 해소할 수 있습니다. 함수자를 쓰면 값에 어떤 함수를 불변/안전하게 적용할 수 있지만, 곳곳에서 남용한다면 금세 난처한 상황에 빠질 수 있다고 말했습니다.

const findStudent = curry((db, ssn) => wrap(find(db, ssn)));
const getAddress = student => wrap(student.fmap(prop('address')));
const studentAddress = compose(getAddress, findStudent(DB('student')));
studentAddress('444-44-4444'); // Wrapper(Wrapper(address))

합성 함수가 서너 개로 늘어난다면? 아무래도 다른 방법을 찾아야겠습니다. 그게 바로 모나드죠.

모나드: 제어 흐름에서 데이터 흐름으로

특정한 케이스를 등적한 로직에 위임하여 처리할 수 있다는 점을 제외하면 모나드는 함수자와 비슷합니다.

Wrapper(2).fmap(half); // Wrapper(1)
Wrapper(3).fmap(half); // Wrapper(1.5)

짝수에만 half를 적용하고 싶다고 해보죠. 함수자는 정의상 주어진 함수를 그대로 적용하고 그 결과를 다시 래퍼에 감싸는 일만 할뿐 다른 일은 안 합니다. 그럼 입력값이 홀수인경우는 어떻게 처리할까요? null을 반환하거나 예외를 던지는 것도 방법이겠지만, 올바른 입력값이 넘어오면 유효한 숫자를, 그렇지 않으면 그냥 무시하게끔 털털하게 일을 시키는 편이 낫습니다.

class Empty {
  map (f) {
    return this;
  }
  // fmap :: (A -> B) -> Wrapper[A] -> Wrapper[B]
  fmap(_) {
    return new Empty();
  }
  toString() {
    return 'Empty()';
  }
}

// wrap :: A -> Empty(A)
const empty = (val) => new Empty(val)

이제 half 코드를 다음과 같이 고치면 짝수만 2로 나눕니다.

const half = val => isEven(val) ? wrap(val / 2) : empty();

half(4); // Wrapper(2)
half(3); // Empty

컨테이너 안으로 값을 승급하고 어떤 규칙을 정해 통제한다는 생각으로 자료형을 생성하는 것이 바로 모나드입니다. 함수자로 값을 보호하되, 합성을 할 경우 데이터를 안전하고 부수효과 없이 흘리려면 모나드가 필요합니다. 앞 예제에서도 홀수가 넘어 오면 null대신 Empty 컨테이너를 반환했죠? 이렇게 하면 에러 염려 없이 원하는 연산을 수행 할 수 있습니다.

자바스크립트처럼 무형힉 언어에서도 모나드를 쉽게 이해할 수 잇고 정적 형식 체계의 세부사항을 시시콜콜 상대하는 고통에서도 해방될 수 있습니다. 다음 두 가지 중요 개념을 이해해야 합니다.

  • 모나드 : 모나드 연산을 추상한 인터페이스를 제공합니다.
  • 모나드형 : 모나드 인터페이스를 실제로 구현한 형식입니다.

모나드마다 개성이 있고 그 목적에 따라 의미도 달라지므로 구현 로직 또한 제각각입니다. 무릇 모든 모나드형은 다음 인터페이스를 준수해야 합니다.

  • 형식 생성자 : 모나드형을 생성합니다.
  • 단위 함수 : 어떤 형식의 값을 모나드에 삽입합니다. 방금 전 wrap, empty 함수와 비슷하나, 모나드에서는 of라고 함수를 명명합니다.
  • 바인드 함수 : 연산을 서로 체이닝합니다. 함수자의 fmap에 해당하며, flatMap이라고도 합니다.
  • 조인 연산 : 모나드 자료구저의 계층을 눌려 폅니다. 모나드 반환 함수를 다중 합성할 때 특히 중요합니다.

이 인터페이스에 따라 Wrapper를 리펙터링한 코드입니다.

class Wrapper {
  // 형식 생성자
  constructor (value) {
    this._value = value
  }
  // 단위 함수
  static of(a) {
    return new Wrapper(a);
  }
  // 바인드 함수
  map (f) {
    return Wrapper.of(f(this.value));
  }
  // 조인 연산
  join() {
    if (!(this._value instanceof Wrapper)) {
      return this;
    }
    return this._value.join();
  }
  get() {
    return this._value;
  }
  toString () {
    return `Wrapper(${this._value})`
  }
}

// wrap :: A -> Wrapper(A)
const wrap = (val) => new Wrapper(val)
const findStudent = curry((db, ssn) => Wrapper.of(find(db, ssn)));
const getAddress = student => Wrapper.of(student.fmap(prop('address')));
const studentAddress = compose(getAddress, findStudent(DB('student')));
studentAddress('444-44-4444').join().get(); // Address

join 함수를 적용하면 납작한 단층 구조로 눌러 펴집니다. 배열의 flatten 연산과 비슷하지요. 모나드 자체는 추상적이고 실질적인 의미는 없습니다. 실제 형식으로 구현되어야 비로소 빛을 발하기 시작하죠. 지금부터 다재다능한 모나드 Maybe, Either, IO를 하나씩 살펴보겠습니다.

Maybe와 Either 모나드로 에러를 처리

모나드는 유효한 값을 감싸기도 하지만 값이 없는 상태, 즉 null이나 undefined를 모형화할 수 있습니다. 함수형 프로그래밍에서는 Maybe/Either형으로 에러를 구상화하여 이런 일들을 처리합니다.

  • 불순 코드를 격리
  • null 체크 로직을 정리
  • 예외를 던지지 않음
  • 함수 합성을 지원
  • 기본값 제공 로직을 한곳에 모음

null 체크를 Maybe로 일원화

Maybe 모나드는 Just, Nothing 두 하위형으로 구성된 빈 형식으로서, 주목적은 null 체크 로직을 효과적으로 통합하는 것입니다.

  • Just(value) : 존재하는 값을 감싼 컨테이너를 나타냅니다.
  • Nothing() : 값이 없는 컨테이너, 또는 추가 정보 없이 실패한 컨테이너를 나타냅니다. Nothing 값에도 얼마든지 함수를 적용할 수 있습니다.

두 하위형은 앞어 열거한 모나드 자격 요건을 모두 갖추면서 나름의 목적에 맞게 특화되어 있습니다. Maybe 모나드 및 그 하위형을 구현한 코드입니다.

class Maybe {
  static just(a) { return new Just(a); }
  static nothing() { return new Nothing(); }
  static fromNullable(a) {
    return a !== null ? Maybe.just(a) : Maybe.nothing();
  }
  static of(a) { return Maybe.just(a); }
  get isNothing() { return false; }
  get isJust() { return false; }
}

class Just extends Maybe {
  constructor(value) {
    super();
    this._value = value;
  }
  get value() {
    return this._value;
  }
  map(f) {
    return Maybe.fromNullable(f(this._value));
  }
  getOrElse() { return this._value; }
  filter(f) {
    Maybe.fromNullable(f(this._value) ? this._value : null);
  }
  chain(f) { return f(this._value); }
  toString() { return `Maybe.Just(${this._value})`}
}

class Nothing extends Maybe {
  constructor() {
    super();
  }
  get value() {
    throw new TypeError('Nothing 값을 가져올 수 없습니다.');
  }
  map(f) {
    return this;
  }
  getOrElse(other) { return other; }
  filter(f) { return this._value; }
  chain(f) { return this; }
  toString() { return `Maybe.Nothing`}
}

Maybe는 널 허용 값을 다루는 작업을 명시적으로 추상하여 개발자가 중요한 비즈니스 로직에만 전념할 수 있게 합니다. 이 모나드는 DB 쿼리, 컬렉션에서 값을 검색하거나 서버에 데이터를 요청하는 등 결과가 불확실한 호출을 할 때 자주 씁니다. 찾는 레코드가 정말 있는지 없는지 예측할 수 없으니 조회 결과를 Maybe로 감싸고 연산명 앞에 safe를 붙여 구분합니다.

const safeFindObject = curry((db, id) => Maybe.fromNullable(find(db, id)));
const safeFindStudent = safeFindObject(DB('student'));
const address = safeFindStudent('444-44-4444').map(prop('address'));
 // Just(Address()) 또는 Nothing

모나다로 결과를 감싸면 함수 서명에 부가 정보를 덧붙여 자기 문서화도 가능하며 반환값이 불확실성을 있는 그대로 나타낼 수 있습니다. 고맙게도 null 체크는 Maybe.fromNullable이 대신 해주고 safeFindStudent를 호출해서 값이 있으면 Just(Address()), 없으면 Nothing이 반환됩니다.

Maybe.fromNullable에 잘못된 값이 넘어오면 Nothing형을 내므로 결국 get()을 호출해 컨테이너를 열어보려고 하면 예외가 납니다. Maybe는 잘못된 데이터를 한곳에서 다스릴 수 있는 탁월한 수단이지만, 뭔가 일이 잘못될 경우에 아무것도 주지 않습니다. 좀 더 적극적으로 실패한 원인까지 통보받을 방안이 있으면 더 좋겠네요. 이런 일은 Either 모나드가 제격입니다.

Either로 실패를 복구

Either는 Maybe와 약간 다릅니다. Either는 절대로 동시에 발생하지 않는 두 값 a, b를 논리 적으로 구분 한 자료 구조로서, 다음 두 경우를 모형화 한 형식입니다.

  • Left (a) : 에러 메시지 또는 예외 객체를 담습니다.
  • Right (b) : 성공한 값을 담습니다.

Either는 오른쪽 괴 연산자 operand를 중심으로 작동합니다. 그래서 컨테이너에 한수를 매핑하면 항상 하위 형 Right (b)에 적용됩니다. Maybe에서 Just로 분기 한거나 마찬가지죠. 보통 Either는 어떤 계산 도중 실패 할 경우 그 원인에 관한 추가 정보를 결과와 함께 제공 할 목적으로 씁니다. 복구 불가한 예외가 발생한 경우, 던질 예외 객체를 왼쪽에 두는 것입니다. 다음은 Either 모나드를 구현 한 코드입니다.

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(a) {
    return a !== null && a !== undefined ? Either.left(a) : Either.right(a);
  }
  static of(a) { return Either.right(a); }
}

class Left extends Either {
  get value() {
    throw new TypeError('Left(a) 값을 가져올 수 없습니다.');
  }
  map(f) {
    return this;
  }
  getOrElse(other) { return other; }
  orElse(f) { return f(this._value); }
  chain(f) { return this; }
  filter(f) { return this; }
  toString() { return `Either.Left(${this._value})`}
}

class Right extends Either {
  map(f) {
    return Either.of(f(this._value));
  }
  getOrElse() { return this._value; }
  get value() {
    return this._value;
  }
  chain(f) { return f(this._value); }
  filter(f) {
    Either.fromNullable(f(this._value) ? this._value : null);
  }
  toString() { return `Either.Right(${this._value})`}
}

Maybe와 Either 두 형식 모두 쓰지 않는 연산이 있는데요, 이들은 의도적으로 추가한 자리끼우개입니다. 상대편 모나드가 작동할 때 안전하게 함수 실행을 건너뛰게 하려는 거지요. Maybe.Nothing과 달리 Either.Left는 함수 적용이 가능한 값을 담을 수 있습니다. 그래서 객체를 반환하지 않으면 orElse함수를 Left 피연산자에 적용해서 에러 로그를 남길 수 있습니다.

IO 모나드로 외부 자원과 상호작용

부수효과 문제를 바로잡거나 변이를 막을 순 없지만, 적어도 애플리케이션 관점에서 IO 연산이 불변한 것처럼 작동시킬 방법은 있습니다. IO 연산을 모나드 체인에 승급하여 데이터 흐름을 모나드가 주도하게끔 맡기는 것입니다. 일단, 다음 IO 모나드가 필요합니다.

class IO {
  constructor(effect) {
    if(!isFunction(effect)) {
      throw 'IO 사용법: 함수는 필수입니다.';
    }
    this.effect = effect;
  }
  static of(a) { return new IO(() => a); } 
  static from(fn) { return new IO(fn); }
  map(fn) {
   let self = this;
   return new IO(() => fn(self.effect()));
  }
  chain(fn) { return fn(this.effect()); }
  run() { return this.effect(); }
}

값이 아닌 effect 함수를 감싼 이 모나드는 이전 모나드와는 작동 방식부터 다릅니다. 함수는 일종의 느긋한 값, 말하자면 계산딜 때까지 기다리는 값입니다. 따라서 갖가지 DOM 연산을 이 모나드로 함께 체이닝한 다음, 하나의 참조 투명한 의사연산을 묶어 실행하면, 부수효과를 일으키는 함수의 실행 순서가 뒤바뀌거나 호출 도중에 실행되는 일은 방지할 수 있습니다.

const changeToStartCase = IO
    .from(readDOM('#student-name'))
    .map(_.startCase)
    .map(writeDOM('#student-name'));

체인 마지막의 DOM 쓰기는 불순한 연산입니다. 그럼, changeToStartCase를 실행 한 결과는 어떻게 될까요? 모나드가 정말 좋은 점은 순수 함수라는 요건이 지켜진다는 것입니다. 다른 모나드처럼 map의 출력도 모나드 자신, 즉 IO 인스턴스라서 아직이 단계에선 아무것도 실행되지 않습니다. 여기서는 IO 작업을 그냥 선언적으로 서술했을뿐입니다. 이제 코드를 돌려 봅시다

changeToStartCase.run();

바로 이것입니다. IO 작업을 참조 투명한 방향으로 처리 한 것이지요! IO 모나드의 가장 중요 한 이점은 순수한 부분과 불순한 부분을 분명하게 가른다는 점입니다.

모나드 체인 및 합성

명령형 프로그래밍으로 작성된 코드는 부수효과 발생, 모듈성 결여, 명령형 에러 처리 등으로 인해 이 프로그램은 사용하고 테스트하기 어렵습니다. 합성프로그램 흐름을 제어하고, 모나드데이터 흐름을 제어합니다. 합성과 모나드는 함수형 프로그래밍의 생태계를 이루는 양대 개념입니다.