abonglog logoabonglog

Chapter 09: Monadic Onions [번역] 의 썸네일

Chapter 09: Monadic Onions [번역]

mostly-adequate-guide
프로필 이미지
yonghyeun

해당 게시글은 mostly-adequate-guide 의 시리즈들을 번역했습니다.

해당 게시글의 저작권은 mostly-adequate-guide 의 저작권인 Attribution-ShareAlike 4.0 International 을 준수합니다.

🤖 AI가 요약한 글이에요!
이 장에서는 포인티드 펑터(Pointed Functor)모나드(Monad) 의 개념을 소개합니다.
포인티드 펑터는 of 메서드를 통해 값을 기본 컨텍스트에 넣는 기능을 제공합니다.
모나드는 포인티드 펑터이면서 중첩된 컨텍스트를 단일 레이어로 평탄화하는 join 메서드를 가진 타입입니다.
mapjoin을 결합한 chain (또는 flatMap, bind) 함수를 통해 중첩된 효과(effect)를 순차적으로 처리하고 함수형 방식으로 변수를 할당하는 방법을 보여줍니다.
모나드는 오류 처리, 비동기 작업, 상태 관리 등 복잡한 제어 흐름을 순수하고 선언적인 방식으로 다룰 수 있게 해줍니다.

Chapter 09: 모나딕 양파 (Monadic Onions)

포인티드 펑터 팩토리

더 진행하기 전에 고백할 것이 있습니다: 우리가 각 타입에 배치한 of 메서드에 대해 완전히 솔직하지 않았습니다. 알고 보니, 그것은 new 키워드를 피하기 위해 있는 것이 아니라, 값을 소위 기본 최소 컨텍스트(default minimal context) 에 넣기 위한 것입니다. 네, of는 실제로 생성자를 대체하는 것이 아닙니다 - 그것은 우리가 포인티드(Pointed) 라고 부르는 중요한 인터페이스의 일부입니다.

포인티드 펑터(Pointed functor)of 메서드를 가진 펑터입니다.

여기서 중요한 것은 어떤 값이든 우리 타입에 넣고 map을 시작할 수 있는 능력입니다.

다양한 펑터의 of 메서드 사용 예시
IO.of("tetris").map(concat(" master"));
// IO('tetris master')
 
Maybe.of(1336).map(add(1));
// Maybe(1337)
 
Task.of([{ id: 2 }, { id: 3 }]).map(map(prop("id")));
// Task([2,3])
 
Either.of("The past, present and future walk into a bar...").map(
  concat("it was tense.")
);
// Right('The past, present and future walk into a bar...it was tense.')

기억하신다면, IOTask의 생성자는 인수로 함수를 기대하지만, MaybeEither는 그렇지 않습니다. 이 인터페이스의 동기는 생성자의 복잡성과 특정 요구 사항 없이 값을 펑터에 넣는 공통적이고 일관된 방법입니다. "기본 최소 컨텍스트"라는 용어는 정밀성이 부족하지만, 어떤 값이든 우리 타입으로 끌어올리고(lift) 어떤 펑터든 기대되는 동작으로 평소처럼 map을 사용하고 싶다는 아이디어를 잘 포착합니다.

이 시점에서 제가 해야 할 중요한 수정 사항 하나는, 말장난이지만, Left.of는 말이 안 된다는 것입니다. 각 펑터는 값을 그 안에 넣는 한 가지 방법을 가져야 하며, Either의 경우 그것은 new Right(x)입니다. 우리는 Right를 사용하여 of를 정의합니다. 왜냐하면 우리 타입이 map할 수 있다면, map해야 하기 때문입니다. 위의 예시들을 보면, of가 일반적으로 어떻게 작동할지에 대한 직관을 가져야 하며, Left는 그 틀을 깹니다.

pure, point, unit, return과 같은 함수에 대해 들어보셨을 수도 있습니다. 이것들은 우리의 of 메서드, 즉 미스터리한 국제 함수에 대한 다양한 별명입니다. of는 모나드를 사용하기 시작할 때 중요해질 것입니다. 왜냐하면 우리가 보게 될 것처럼, 값을 타입 안으로 다시 수동으로 넣는 것은 우리의 책임이기 때문입니다.

new 키워드를 피하기 위해 여러 표준 자바스크립트 트릭이나 라이브러리가 있으므로, 그것들을 사용하고 이제부터는 책임감 있는 어른처럼 of를 사용합시다. folktale, ramda 또는 fantasy-land의 펑터 인스턴스를 사용하는 것을 추천합니다. 이들은 올바른 of 메서드뿐만 아니라 new에 의존하지 않는 멋진 생성자를 제공하기 때문입니다.

은유 섞기

모나드는 양파랑 비슷해요모나드는 양파랑 비슷해요

아시다시피, 우주 부리토 외에도 (소문을 들으셨다면), 모나드는 양파와 같습니다. 흔한 상황으로 설명해 보겠습니다:

readFile과 print 함수 정의
const fs = require("fs");
 
// readFile :: String -> IO String
const readFile = (filename) => new IO(() => fs.readFileSync(filename, "utf-8"));
 
// print :: String -> IO String
const print = (x) =>
  new IO(() => {
    console.log(x);
    return x;
  });
 
// cat :: String -> IO (IO String)
const cat = compose(map(print), readFile);
 
cat(".git/config");
// IO(IO('[core]\nrepositoryformatversion = 0\n'))

여기서 우리가 가진 것은 printmap 도중에 두 번째 IO를 도입했기 때문에 다른 IO 안에 갇힌 IO입니다. 우리의 문자열로 계속 작업하려면 map(map(f))를 해야 하고, 효과를 관찰하려면 unsafePerformIO().unsafePerformIO()를 해야 합니다.

중첩된 IO 처리 예시
// cat :: String -> IO (IO String)
const cat = compose(map(print), readFile);
 
// catFirstChar :: String -> IO (IO String)
const catFirstChar = compose(map(map(head)), cat);
 
catFirstChar(".git/config");
// IO(IO('['))

애플리케이션에 두 가지 효과가 포장되어 준비된 것을 보는 것은 좋지만, 마치 두 개의 방호복을 입고 작업하는 것 같고 불편하게 어색한 API로 끝나게 됩니다. 다른 상황을 살펴봅시다:

safeProp과 safeHead 함수 정의
// safeProp :: Key -> {Key: a} -> Maybe a
const safeProp = curry((x, obj) => Maybe.of(obj[x]));
 
// safeHead :: [a] -> Maybe a
const safeHead = safeProp(0);
 
// firstAddressStreet :: User -> Maybe (Maybe (Maybe Street))
const firstAddressStreet = compose(
  map(map(safeProp("street"))),
  map(safeHead),
  safeProp("addresses")
);
 
firstAddressStreet({
  addresses: [{ street: { name: "Mulburry", number: 8402 }, postcode: "WC2N" }],
});
// Maybe(Maybe(Maybe({name: 'Mulburry', number: 8402})))

다시 한번, 이 중첩된 펑터 상황을 봅니다. 함수에 세 가지 가능한 실패 지점이 있다는 것을 보는 것은 깔끔하지만, 호출자가 값에 접근하기 위해 세 번 map을 할 것으로 기대하는 것은 약간 건방진 일입니다 - 우리는 방금 만났을 뿐입니다. 이 패턴은 계속해서 나타날 것이며, 이것이 우리가 밤하늘에 강력한 모나드 상징을 비춰야 할 주요 상황입니다.

모나드는 양파와 같다고 말했습니다. 왜냐하면 중첩된 펑터의 각 층을 map으로 벗겨내어 내부 값에 도달할 때 눈물이 차오르기 때문입니다. 우리는 눈물을 닦고, 심호흡을 하고, join이라는 메서드를 사용할 수 있습니다.

join 메서드 사용 예시
const mmo = Maybe.of(Maybe.of("nunchucks"));
// Maybe(Maybe('nunchucks'))
 
mmo.join();
// Maybe('nunchucks')
 
const ioio = IO.of(IO.of("pizza"));
// IO(IO('pizza'))
 
ioio.join();
// IO('pizza')
 
const ttt = Task.of(Task.of(Task.of("sewers")));
// Task(Task(Task('sewers')));
 
ttt.join();
// Task(Task('sewers'))

같은 타입의 두 레이어가 있다면, join으로 합칠 수 있습니다. 이렇게 합치는 능력, 이 펑터 결합이 모나드를 모나드로 만드는 것입니다. 좀 더 정확한 것으로 전체 정의에 조금씩 다가가 봅시다:

모나드는 평탄화할 수 있는 포인티드 펑터입니다.

join 메서드를 정의하고, of 메서드를 가지며, 몇 가지 법칙을 따르는 모든 펑터는 모나드입니다. join을 정의하는 것은 그리 어렵지 않으므로 Maybe에 대해 그렇게 해봅시다:

Maybe의 join 메서드 구현
Maybe.prototype.join = function join() {
  return this.isNothing() ? Maybe.of(null) : this.$value;
};

거기, 자궁에서 쌍둥이를 소비하는 것만큼 간단합니다. Maybe(Maybe(x))가 있다면 .$value는 불필요한 추가 레이어를 제거하고 거기서부터 안전하게 map할 수 있습니다. 그렇지 않으면, 애초에 아무것도 map되지 않았을 것이므로 하나의 Maybe만 가질 것입니다.

이제 join 메서드가 있으니, firstAddressStreet 예제에 마법의 모나드 가루를 뿌리고 그것이 작동하는 것을 봅시다:

join을 사용한 firstAddressStreet 리팩토링
// join :: Monad m => m (m a) -> m a
const join = (mma) => mma.join();
 
// firstAddressStreet :: User -> Maybe Street
const firstAddressStreet = compose(
  join, // Maybe(Maybe(Maybe Street)) -> Maybe(Maybe Street)
  map(safeProp("street")), // Maybe(Maybe Address) -> Maybe(Maybe(Maybe Street))
  join, // Maybe(Maybe Address) -> Maybe Address
  map(safeHead), // Maybe [Address] -> Maybe(Maybe Address)
  safeProp("addresses") // User -> Maybe [Address]
);
 
firstAddressStreet({
  addresses: [{ street: { name: "Mulburry", number: 8402 }, postcode: "WC2N" }],
});
// Maybe({name: 'Mulburry', number: 8402})

중첩된 Maybe가 발생할 때마다 join을 추가하여 통제 불능 상태가 되지 않도록 했습니다. IO에 대해서도 동일하게 수행하여 감을 잡아봅시다.

IO의 join 메서드 구현
IO.prototype.join = function () {
  const $ = this;
  return new IO(() => $.unsafePerformIO().unsafePerformIO());
};

우리는 단순히 두 레이어의 IO를 순차적으로 실행하는 것을 묶습니다: 외부 다음 내부. 명심하세요, 우리는 순수성을 버린 것이 아니라, 단지 과도한 두 겹의 수축 포장을 열기 쉬운 하나의 패키지로 재포장했을 뿐입니다.

join을 사용한 IO 예제 리팩토링
// log :: a -> IO a
const log = (x) =>
  new IO(() => {
    console.log(x);
    return x;
  });
 
// setStyle :: Selector -> CSSProps -> IO DOM
const setStyle = curry((sel, props) => new IO(() => jQuery(sel).css(props)));
 
// getItem :: String -> IO String
const getItem = (key) => new IO(() => localStorage.getItem(key));
 
// applyPreferences :: String -> IO DOM
const applyPreferences = compose(
  join, // IO(IO DOM) -> IO DOM
  map(setStyle("#main")), // IO Object -> IO(IO DOM)
  join, // IO(IO Object) -> IO Object
  map(log), // IO String -> IO(IO Object)
  map(JSON.parse), // IO String -> IO String (parsed)
  getItem // String -> IO String
);
 
applyPreferences("preferences").unsafePerformIO();
// Object {backgroundColor: "green"}
// <div style="background-color: 'green'"/>

getItemIO String을 반환하므로 map을 사용하여 구문 분석합니다. logsetStyle 모두 자체적으로 IO를 반환하므로 중첩을 제어하기 위해 join해야 합니다.

내 체인이 내 가슴을 친다 (My Chain Hits My Chest)

체인체인

패턴을 눈치채셨을 수도 있습니다. 우리는 종종 map 직후에 join을 호출하게 됩니다. 이것을 chain이라는 함수로 추상화해 봅시다.

chain 함수 정의
// chain :: Monad m => (a -> m b) -> m a -> m b
const chain = curry((f, m) => m.map(f).join());
 
// 또는
 
// chain :: Monad m => (a -> m b) -> m a -> m b
const chain = (f) => compose(join, map(f));

이 map/join 콤보를 단일 함수로 묶을 것입니다. 이전에 모나드에 대해 읽어보셨다면, chain>>= (바인드라고 발음) 또는 flatMap이라고 불리는 것을 보셨을 수도 있습니다. 이들은 모두 같은 개념에 대한 별칭입니다. 개인적으로 flatMap이 가장 정확한 이름이라고 생각하지만, JS에서 널리 받아들여지는 이름인 chain을 고수할 것입니다. 위의 두 예제를 chain으로 리팩토링해 봅시다:

chain을 사용한 리팩토링 예시
// map/join
const firstAddressStreet = compose(
  join,
  map(safeProp("street")),
  join,
  map(safeHead),
  safeProp("addresses")
);
 
// chain
const firstAddressStreet = compose(
  chain(safeProp("street")), // Maybe Address -> Maybe Street
  chain(safeHead), // Maybe [Address] -> Maybe Address
  safeProp("addresses") // User -> Maybe [Address]
);
 
// map/join
const applyPreferences = compose(
  join,
  map(setStyle("#main")),
  join,
  map(log),
  map(JSON.parse),
  getItem
);
 
// chain
const applyPreferences = compose(
  chain(setStyle("#main")), // Object -> IO DOM
  chain(log), // String -> IO Object
  map(JSON.parse), // String -> Object
  getItem // String -> IO String
);

새로운 chain 함수로 모든 map/join을 교체하여 약간 정리했습니다. 깔끔함은 좋지만, chain에는 눈에 보이는 것 이상의 것이 있습니다 - 진공청소기라기보다는 토네이도에 가깝습니다. chain은 효과를 손쉽게 중첩시키기 때문에, 순수하게 함수적인 방식으로 순서(sequence)변수 할당(variable assignment) 을 모두 포착할 수 있습니다.

chain을 사용한 순차적/변수 할당 예시
// getJSON :: Url -> Params -> Task JSON
getJSON("/authenticate", { username: "stale", password: "crackers" }).chain(
  (user) => getJSON("/friends", { user_id: user.id })
);
// Task([{name: 'Seimith', id: 14}, {name: 'Ric', id: 39}]);
 
// querySelector :: Selector -> IO DOM
querySelector("input.username").chain(({ value: uname }) =>
  querySelector("input.email").chain(({ value: email }) =>
    IO.of(`Welcome ${uname} prepare for spam at ${email}`)
  )
);
// IO('Welcome Olivia prepare for spam at olivia@tremorcontrol.net');
 
Maybe.of(3).chain((three) => Maybe.of(2).map(add(three)));
// Maybe(5);
 
Maybe.of(null).chain(safeProp("address")).chain(safeProp("street"));
// Maybe(null);

어쨌든, 위의 예시들로 넘어가 봅시다. 첫 번째 예시에서는 두 개의 Task가 비동기 액션의 순서로 연결된 것을 봅니다 - 먼저 user를 검색한 다음, 해당 사용자의 ID로 친구를 찾습니다. Task(Task([Friend])) 상황을 피하기 위해 chain을 사용합니다.

다음으로, querySelector를 사용하여 몇 가지 다른 입력을 찾고 환영 메시지를 만듭니다. 가장 안쪽 함수에서 unameemail 모두에 접근할 수 있다는 점에 주목하세요 - 이것이 함수형 변수 할당의 정수입니다. IO가 친절하게 값을 빌려주기 때문에, 우리는 그것을 발견한 대로 다시 넣을 책임이 있습니다 - 우리는 그 신뢰(그리고 우리 프로그램)를 깨뜨리고 싶지 않습니다. IO.of는 이 작업에 완벽한 도구이며, 이것이 포인티드가 모나드 인터페이스의 중요한 전제 조건인 이유입니다. 그러나 map을 선택할 수도 있습니다. 왜냐하면 그것도 올바른 타입을 반환할 것이기 때문입니다:

map을 사용한 IO chain 예시
querySelector("input.username").chain(({ value: uname }) =>
  querySelector("input.email").map(
    ({ value: email }) => `Welcome ${uname} prepare for spam at ${email}`
  )
);
// IO('Welcome Olivia prepare for spam at olivia@tremorcontrol.net');

마지막으로, Maybe를 사용하는 두 가지 예시가 있습니다. chain은 내부적으로 매핑하고 있으므로, 어떤 값이 null이면 계산을 즉시 중단합니다.

이 예시들이 처음에는 이해하기 어렵더라도 걱정하지 마세요. 가지고 놀아보세요. 막대기로 찔러보세요. 산산조각 내고 다시 조립해보세요. "일반적인" 값을 반환할 때는 map을 사용하고 다른 펑터를 반환할 때는 chain을 사용해야 함을 기억하세요. 다음 장에서는 어플리커티브(Applicatives)에 접근하고 이러한 종류의 표현식을 더 멋지고 가독성 높게 만드는 멋진 트릭을 볼 것입니다.

참고로, 이것은 두 개의 다른 중첩된 타입에서는 작동하지 않습니다. 펑터 합성과 나중에 나올 모나드 트랜스포머가 그 상황에서 우리를 도울 수 있습니다.

권력 여행 (Power Trip)

컨테이너 스타일 프로그래밍은 때때로 혼란스러울 수 있습니다. 우리는 때때로 값이 얼마나 많은 컨테이너 깊이에 있는지 또는 map이나 chain이 필요한지 이해하는 데 어려움을 겪습니다 (곧 더 많은 컨테이너 메서드를 보게 될 것입니다). inspect 구현과 같은 트릭으로 디버깅을 크게 개선할 수 있으며, 우리가 던지는 어떤 효과든 처리할 수 있는 "스택"을 만드는 방법을 배울 것이지만, 그것이 번거로움을 감수할 가치가 있는지 의문이 들 때가 있습니다.

이 방식으로 프로그래밍하는 힘을 보여주기 위해 잠시 불타는 모나딕 검을 휘두르고 싶습니다.

파일을 읽은 다음 즉시 업로드해 봅시다:

Either와 Task를 사용한 파일 업로드 예시
// readFile :: Filename -> Either String (Task Error String)
// httpPost :: String -> String -> Task Error JSON
// upload :: Filename -> Either String (Task Error JSON)
const upload = compose(map(chain(httpPost("/uploads"))), readFile);

여기서 우리는 코드를 여러 번 분기하고 있습니다. 타입 시그니처를 보면 3가지 오류로부터 보호하고 있음을 알 수 있습니다 - readFileEither를 사용하여 입력을 검증하고 (아마도 파일 이름이 있는지 확인), readFileTask의 첫 번째 타입 매개변수에 표현된 것처럼 파일에 접근할 때 오류가 발생할 수 있으며, 업로드는 httpPostError로 표현된 어떤 이유로든 실패할 수 있습니다. 우리는 chain을 사용하여 두 개의 중첩되고 순차적인 비동기 액션을 아무렇지도 않게 처리합니다.

이 모든 것이 하나의 선형적인 왼쪽에서 오른쪽으로의 흐름으로 달성됩니다. 이 모든 것은 순수하고 선언적입니다. 등식 추론과 신뢰할 수 있는 속성을 가지고 있습니다. 불필요하고 혼란스러운 변수 이름을 추가하도록 강요받지 않습니다. 우리의 upload 함수는 특정 일회성 API가 아닌 일반적인 인터페이스에 대해 작성되었습니다. 젠장, 한 줄짜리입니다.

대조적으로, 이것을 해내는 표준적인 명령형 방식을 살펴봅시다:

명령형 파일 업로드 예시
// upload :: Filename -> (String -> a) -> Void
const upload = (filename, callback) => {
  if (!filename) {
    throw new Error("You need a filename!");
  } else {
    readFile(filename, (errF, contents) => {
      if (errF) throw errF;
      httpPost("/uploads", contents, (errH, json) => {
        if (errH) throw errH;
        callback(json);
      });
    });
  }
};

글쎄, 이건 악마의 산수가 아닌가요. 우리는 변덕스러운 광기의 미로를 통해 핀볼처럼 튕겨 다닙니다. 만약 도중에 변수를 변경하는 일반적인 앱이었다면 어떨지 상상해보세요! 우리는 정말로 타르 구덩이에 빠졌을 것입니다.

이론

우리가 살펴볼 첫 번째 법칙은 결합 법칙(associativity)이지만, 아마도 여러분이 익숙한 방식은 아닐 것입니다.

모나드 결합 법칙 (join)
// associativity
compose(join, map(join)) === compose(join, join);

이 법칙들은 모나드의 중첩된 특성에 관한 것이므로, 결합 법칙은 내부 또는 외부 타입을 먼저 결합하여 동일한 결과를 얻는 데 중점을 둡니다. 그림이 더 도움이 될 수 있습니다:

Monad Associativity DiagramMonad Associativity Diagram

왼쪽 상단에서 아래로 이동하면서, M(M(M a))의 외부 두 M을 먼저 join한 다음 다른 join으로 원하는 M a로 이동할 수 있습니다. 또는, 후드를 열고 map(join)으로 내부 두 M을 평탄화할 수 있습니다. 내부 또는 외부 M을 먼저 결합하든 상관없이 동일한 M a로 끝나며, 이것이 결합 법칙의 전부입니다. map(join) != join이라는 점은 주목할 가치가 있습니다. 중간 단계는 값이 다를 수 있지만, 마지막 join의 최종 결과는 동일합니다.

두 번째 법칙도 비슷합니다:

모나드 항등 법칙 (join)
// identity for all (M a)
(compose(join, of) === compose(join, map(of))) === id;

이것은 어떤 모나드 M에 대해서든, ofjoinid와 같다는 것을 말합니다. 우리는 또한 map(of)를 사용하여 안쪽에서부터 공격할 수도 있습니다. 이것을 시각화했을 때 삼각형 모양을 만들기 때문에 "삼각형 항등(triangle identity)"이라고 부릅니다:

Monad Identity DiagramMonad Identity Diagram

왼쪽 상단에서 오른쪽으로 이동하면, of가 실제로 우리의 M a를 다른 M 컨테이너에 넣는 것을 볼 수 있습니다. 그런 다음 아래로 이동하여 join하면, 처음에 id를 호출한 것과 동일한 결과를 얻습니다. 오른쪽에서 왼쪽으로 이동하면, map으로 덮개 아래로 몰래 들어가 일반 aof를 호출하더라도 여전히 M (M a)로 끝나고 join하면 처음으로 돌아갑니다.

제가 방금 of라고 썼지만, 이것은 우리가 사용하고 있는 어떤 모나드든 특정 M.of여야 한다는 점을 언급해야 합니다.

이제, 저는 이 법칙들, 항등과 결합 법칙을 전에 어디선가 본 적이 있습니다... 잠깐만요, 생각 중입니다... 네 물론이죠! 이것들은 카테고리에 대한 법칙입니다. 하지만 그렇다면 정의를 완성하기 위해 합성 함수가 필요하다는 의미입니다. 보십시오:

클라이슬리 합성 (mcompose)
// mcompose :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
const mcompose = (f, g) => compose(chain(f), g);
 
// left identity
mcompose(M.of, f) === f;
 
// right identity
mcompose(f, M.of) === f;
 
// associativity
mcompose(mcompose(f, g), h) === mcompose(f, mcompose(g, h));

결국 카테고리 법칙입니다. 모나드는 모든 객체가 모나드이고 사상(morphism)이 연결된 함수인 "클라이슬리 카테고리(Kleisli category)"라는 카테고리를 형성합니다. 퍼즐 조각이 어떻게 맞춰지는지에 대한 많은 설명 없이 카테고리 이론의 일부를 조금씩 보여주며 여러분을 놀리려는 의도는 없습니다. 의도는 관련성을 보여주고 우리가 매일 사용할 수 있는 실용적인 속성에 초점을 맞추면서 약간의 흥미를 유발할 만큼 표면을 긁는 것입니다.

요약

모나드는 중첩된 계산 속으로 파고들 수 있게 해줍니다. 우리는 변수를 할당하고, 순차적 효과를 실행하고, 비동기 작업을 수행할 수 있습니다. 이 모든 것을 운명의 피라미드에 벽돌 하나 놓지 않고도 할 수 있습니다. 값이 같은 타입의 여러 레이어에 갇힌 자신을 발견했을 때 구조하러 옵니다. 믿음직한 조수 "포인티드"의 도움으로, 모나드는 우리에게 포장되지 않은 값을 빌려줄 수 있고 우리가 끝났을 때 다시 넣을 수 있다는 것을 압니다.

네, 모나드는 매우 강력하지만, 여전히 몇 가지 추가 컨테이너 함수가 필요합니다. 예를 들어, API 호출 목록을 한 번에 실행한 다음 결과를 수집하고 싶다면 어떨까요? 모나드로 이 작업을 수행할 수 있지만, 다음 호출을 하기 전에 각 호출이 완료될 때까지 기다려야 합니다. 여러 유효성 검사를 결합하는 것은 어떨까요? 오류 목록을 수집하기 위해 계속 유효성 검사를 하고 싶지만, 모나드는 첫 번째 Left가 등장하면 쇼를 중단할 것입니다.

다음 장에서는 어플리커티브 펑터가 컨테이너 세계에 어떻게 들어맞는지, 그리고 많은 경우에 왜 모나드보다 선호하는지 알아볼 것입니다.

연습 문제

다음과 같은 User 객체를 고려합니다:

User 객체 예시
const user = {
  id: 1,
  name: "Albert",
  address: {
    street: {
      number: 22,
      name: "Walnut St",
    },
  },
};

이제 다음 항목들을 고려합니다:

getFile 및 pureLog 함수
// getFile :: IO String
const getFile = IO.of("/home/mostly-adequate/ch09.md");
 
// pureLog :: String -> IO ()
const pureLog = (str) => new IO(() => console.log(str));

이 연습 문제에서는 다음과 같은 시그니처를 가진 헬퍼를 고려합니다:

헬퍼 함수 시그니처
// validateEmail :: Email -> Either String Email
// addToMailingList :: Email -> IO([Email])
// emailBlast :: [Email] -> IO ()