June 20, 2021
본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Getting Started에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.
fp-ts
에서 동기적인 이펙트 있는 계산은 기본적으로 () => A
시그니처를 갖는 썽크인 IO
타입으로 표현합니다.
interface IO<A> {
(): A;
}
참고:
IO
는 절대 실패하지 않는 계산을 표현합니다.
이러한 계산의 예시는 아래와 같습니다.
localStorage
에 읽기 / 쓰기localStorage
에 읽기 / 쓰기)import type { Option } from 'fp-ts/lib/Option';
import { fromNullable } from 'fp-ts/lib/Option';
const getItem = (key: string): IO<Option<string>> => () =>
fromNullable(localStorage.getItem(key));
const setItem = (key: string, value: string): IO<void> => () =>
localStorage.setItem(key, value);
const now: IO<number> = () => new Date().getTime();
const log = (s: unknown): IO<void> => () => console.log(s);
const random: IO<number> = () => Math.random();
IO
타입은 Monad 인스턴스를 허용하므로 map
을 사용할 수 있습니다.
원문에서는
io.map
을 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며IO/map
을 사용하면 됩니다.
import { map } from 'fp-ts/lib/IO';
/** 무작위의 boolean을 반환한다. */
const randomBool: IO<boolean> = map(n => n < 0.5)(random);
또한 chain
연산을 사용할 수 있습니다.
원문에서는
io.chain
을 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며IO/chain
을 사용하면 됩니다.
import { chain } from 'fp-ts/lib/IO';
/** 무작위의 boolean을 콘솔에 출력한다. */
const program: IO<void> = chain(log)(randomBool);
program();
참고:
program()
을 호출할 때까지 아무 일도 일어나지 않습니다.
그 이유는 program
은 이펙트 있는 계산을 표현하는 값이기 때문에, 어떤 사이드 이펙트를 실행하기 위해서는 ”IO
액션을 실행”해야 하기 때문입니다.
IO
액션은 값일 뿐이므로 Monoid와 같은 유용한 추상화를 사용하여 처리할 수 있습니다.
import type { IO } from 'fp-ts/lib/IO';
import type { Monoid } from 'fp-ts/lib/Monoid';
import { log } from 'fp-ts/lib/Console';
import { chain } from 'fp-ts/lib/IO';
import { concatAll } from 'fp-ts/lib/Monoid';
import { MonoidSum } from 'fp-ts/lib/number';
import { randomInt } from 'fp-ts/lib/Random';
import { getApplicativeMonoid } from 'fp-ts/lib/Applicative';
type Die = IO<number>;
const monoidDie: Monoid<Die> = getApplicativeMonoid(Applicative)(MonoidSum);
/** 주사위를 굴린 결과의 합을 반환합니다. */
const roll: (dice: Array<Die>) => IO<number> = concatAll(monoidDie);
const D4: Die = randomInt(1, 4);
const D10: Die = randomInt(1, 10);
const D20: Die = randomInt(1, 20);
const dice = [D4, D10, D20];
chain((result) => log(`Result is: ${result}`))(roll(dice))();
/*
Result is: 11
*/
또는 유용한 콤비네이터를 정의할 수 있습니다.
/** 디버깅을 위해 콘솔에 값을 기록한다. */
const withLogging = <A>(action: IO<A>): IO<A> =>
chain<A, A>(a => map(() => a)(log(`Value is: ${a}`)))(action);
chain(result => log(`Result is: ${result}`))(roll(dice.map(withLogging)))();
/*
Value is: 4
Value is: 2
Value is: 13
Result is: 19
*/
실패할 수 있는 동기적인 이펙트 있는 계산을 표현하려면 어떻게 해야 할까요?
우리는 두 가지 이펙트가 필요합니다.
타입 생성자 | 이펙트 (해석) |
---|---|
IO<A> |
비동기적인 이펙트 있는 계산 |
Either<E, A> |
실패할 수 있는 계산 |
해결책은 Either
를 IO
내부에 두는 것입니다. 이것은 IOEither
타입으로 이어집니다.
interface IOEither<E, A> extends IO<Either<E, A>> {}
IOEither<E, A>
타입의 값을 “실행”할 때 Left
가 있으면 E
타입의 오류로 인해 계산이 실패했음을 의미하고 그렇지 않으면 Right
를 얻습니다. 이는 A
타입의 값으로 계산이 성공했음을 의미합니다.
fs.readFileSync
가 예외를 발생시킬 수 있음므로 tryCatch
헬퍼를 사용하겠습니다.
tryCatch: <E, A>(f: () => A) => IOEither<E, A>
여기서 f
는 오류(tryCatch
에 의해 자동으로 포착 됨)를 던지거나 A
타입의 값을 반환하는 썽크입니다.
import type { IOEither } from 'fp-ts/lib/IOEither';
import { toError } from 'fp-ts/lib/Either';
import { tryCatch } from 'fp-ts/lib/IOEither';
import * as fs from 'fs';
export const readFileSync = (path: string): IOEither<Error, string> =>
tryCatch(() => fs.readFileSync(path, 'utf8'), toError);
readFileSync('foo')();
// => left(Error: ENOENT: no such file or directory, open 'foo')
readFileSync(__filename)();
// => right(...)
fp-ts/lib/IOEither
모듈은 IOEither
타입의 값을 생성 할 수 있는 다른 헬퍼 함수를 제공하며, 이를 집합적으로 lifting 함수라고 합니다.
요약은 아래와 같습니다.
시작 값 | lifting 함수 |
---|---|
IO<E> |
leftIO: <E, A>(ml: IO<E>) => IOEither<E, A> |
E |
left: <E, A>(e: E) => IOEither<E, A> |
Either<E, A> |
fromEither: <E, A>(ma: Either<E, A>) => IOEither<E, A> |
A |
right: <E, A>(a: A) => IOEither<E, A> |
IO<A> |
rightIO: <E, A>(ma: IO<A>) => IOEither<E, A> |
3개 파일 1.txt
, 2.txt
, 3.txt
중 하나의 내용을 무작위로 읽는다고 가정해 보겠습니다.
randomInt: (low: number, high: number) => IO<number>
함수는 닫힌 범위 [low, high]
에 균일하게 분포된 임의의 정수를 반환합니다.
import { randomInt } from 'fp-ts/lib/Random';
위에 정의된 readFileSync
함수로 randomInt
를 연결할 수 있습니다.
원문에서는
ioEither.chain
을 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며IOEither/chain
을 사용하면 됩니다.
import { chain } from 'fp-ts/lib/IOEither';
const randomFile = chain(n => readFileSync(`${__dirname}/${n}.txt`))(
randomInt(1, 3) // 정적 오류
);
타입이 맞지 않습니다. randomInt
는 IO
컨텍스트에서 실행되고 readFileSync
는 IOEither
컨텍스트에서 실행됩니다.
그러나 rightIO
를 사용하여 randomInt
를 IOEither
컨텍스트로 들어 올릴 수 있습니다.
const randomFile = chain(n => readFileSync(`${__dirname}/${n}.txt`))(
rightIO(randomInt(1, 3))
);