April 21, 2021
λ³Έ ν¬μ€νΈλ fp-ts 곡μ λ¬Έμμ Learning Resourcesμ μλ Getting Startedμμ μκ°νλ λ¬Έμλ€μ λ²μνλ©° νμ΅ν λ¬Έμμ λλ€. μλ³Έ λ¬Έμλ λ§ν¬μμ νμΈν μ μμΌλ©° μμ±ν μ½λλ€μ μ¬κΈ°μμ νμΈν μ μμ΅λλ€.
λλ‘λ ν¨μν νλ‘κ·Έλλ° μ€νμΌλ‘ μμ±λμ§ μμ μ½λμ μνΈ μ΄μν΄μΌ νλ κ²½μ°κ° μμ΅λλ€. κ·Έλ¬ν μν©μμ fp-tsλ₯Ό μ΄μ©ν΄ ν΄κ²°νλ λ°©λ²μ μ΄ν΄λ³΄κ² μ΅λλ€.
Array.prototype.findIndex
Option
import { Option, none, some } from 'fp-ts/Option';
function findIndex<A>(
as: Array<A>,
predicate: (a: A) => boolean
): Option<number> {
const index = as.findIndex(predicate);
return index === -1 ? none : some(index);
}
Array.prototype.findIndex
λ κ°μ μ°Ύμ§ λͺ»νμ κ²½μ° -1
μ΄λΌλ νΉμν κ°μ λ°νν©λλ€.
Array.prototype.findIndex
ν¨μλ₯Ό Option
νμ
μ μ΄μ©ν΄ fp-tsμ ν¨κ» μ¬μ©ν μ μλλ‘ λ³κ²½νλ€. -1
μ΄ λ°νλ κ²½μ° none
μ λ°ννκ³ κ·Έλ μ§ μμ κ²½μ° some
μ λ°ννλλ‘ μμ±νμ΅λλ€.
μμ±ν findIndex
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§λ μλμ κ°μ΅λλ€.
findIndex
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§import * as N from 'fp-ts/number';
import { isSome, none, some, getEq, Option, isNone } from 'fp-ts/lib/Option';
import { findIndex } from '../findIndex';
findIndex
Β ν¨μλ₯Ό ν
μ€νΈνλ μ½λν
μ€νΈμ½λμμλ findIndex
κ° predicate
μ 쑰건μ λ§λ κ°μ μ°Ύμμ κ²½μ° isSome
ν¨μλ₯Ό μ΄μ©ν΄ some
μΈμ§ νμΈν ν ν΄λΉ κ°μ μΈλ±μ€λ₯Ό some
μΌλ‘ κ°μΈ λ°νλ κ°κ³Ό κ°μμ§ νμΈν©λλ€.
쑰건μ λ§λ κ°μ μ°Ύμ§ λͺ»νμ κ²½μ° isNone
ν¨μλ₯Ό μ΄μ©ν΄ none
μΈμ§ νμΈν ν none
κ³Ό κ°μμ§ νμΈν©λλ€.
describe('findIndexν¨μ ν
μ€νΈ (Sentinels)', () => {
let array: number[] = [1, 2, 3, 5];
let result: Option<number>;
const E = getEq(N.Eq);
it('findIndexν¨μκ° μ‘΄μ¬νλ κ°μ μ°Ύμμ κ²½μ°', () => {
result = findIndex(array, a => a === 1);
expect(isSome(result)).toBeTruthy();
expect(E.equals(result, some(0))).toBeTruthy();
});
it('findIndexν¨μκ° κ°μ μ°Ύμ§ λͺ»νμ κ²½μ°', () => {
result = findIndex(array, a => a === 4);
expect(isNone(result)).toBeTruthy();
expect(E.equals(result, none)).toBeTruthy();
});
});
undefined
μ null
undefined
λ null
μ λ°νν μ μλ APIArray.prototype.find
Option
, fromNullable
import { Option, fromNullable } from 'fp-ts/Option';
export function find<A>(as: Array<A>, predicate: (a: A) => boolean): Option<A> {
return fromNullable(as.find(predicate));
}
find
ν¨μ ν
μ€νΈνκΈ°Array.prototype
μ μλ find
ν¨μλ null
μ΄λ undefined
μ κ°μ Falsyκ°μ λ°νν μ μμ΅λλ€.
fromNullable
ν¨μλ₯Ό μ΄μ©ν΄ find
ν¨μλ₯Ό κ°μΈ Option
νμ
μ λ°ννλ ν¨μλ‘ λ§λ€μμ΅λλ€.
μμ±ν find
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§λ μλμ κ°μ΅λλ€.
find
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§import * as N from 'fp-ts/number';
import { isSome, none, some, getEq, Option, isNone } from 'fp-ts/lib/Option';
import { find } from '../find';
find
Β ν¨μλ₯Ό ν
μ€νΈνλ μ½λμλ‘ μμ±ν find
ν¨μλ κ°μ μ°Ύμμ κ²½μ° some
νκ·Έλ₯Ό κ°λ κ°μ λ°νν©λλ€. κ°μ μ°Ύμμ κ²½μ°μλ isSome
Β λ©μλκ° true
λ₯Ό λ°ννλμ§ νμΈνκ³ κ°μ΄ some
μΌλ‘ κ°μΈμ§ κ°κ³Ό κ°μμ§ νμΈν©λλ€.
λ°λ©΄μ κ°μ μ°Ύμ§ λͺ»νμ κ²½μ° none
νκ·Έλ₯Ό κ°λ κ°μ λ°νν©λλ€. κ°μ μ°Ύμ§ λͺ»νμ κ²½μ°μλ isNone
λ©μλκ° true
λ₯Ό λ°ννλμ§ νμΈνκ³ κ°μ΄ none
κ³Ό κ°μμ§ νμΈν©λλ€.
describe('findν¨μ ν
μ€νΈ (undefinedμ null)', () => {
let array: number[] = [1, 2, 3, 5];
let result: Option<number>;
const E = getEq(N.Eq);
it('findν¨μκ° μ‘΄μ¬νλ κ°μ μ°Ύμμ κ²½μ°', () => {
result = find(array, a => a === 1);
expect(isSome(result)).toBeTruthy();
expect(E.equals(result, some(1))).toBeTruthy();
});
it('findν¨μκ° κ°μ μ°Ύμ§ λͺ»νμ κ²½μ°', () => {
result = find(array, a => a === 4);
expect(isNone(result)).toBeTruthy();
expect(E.equals(result, none)).toBeTruthy();
});
});
throw
λ μ μλ APIJSON.parse
Eiter
, tryCatch
import { Either, tryCatch } from 'fp-ts/Either';
function parse(s: string): Either<Error, unknown> {
return tryCatch(
() => JSON.parse(s),
reason => new Error(String(reason))
);
}
parse
ν¨μ ν
μ€νΈνκΈ°JSON.parse
ν¨μλ μΈμλ‘ μλͺ»λ λ¬Έμμ΄μ΄ μ λ¬λ κ²½μ° μμΈλ₯Ό λ°μμν΅λλ€. fp-tsμ tryCatch
λ₯Ό μ΄μ©νλ©΄ fp-tsμ ν¨κ» μ¬μ©ν μ μλλ‘ Either
νμ
μ λ°νμν¬ μ μμ΅λλ€.
μμ±ν parse
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§λ μλμ κ°μ΅λλ€.
parse
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§import { isLeft, isRight, getOrElse } from 'fp-ts/Either';
import { parse } from '../parse';
parse
Β ν¨μλ₯Ό ν
μ€νΈνλ μ½λparse
ν¨μκ° μ±κ³΅νλ κ°κ³Ό μ€ν¨νλ κ°μ μ€λΉν ν parse
ν¨μκ° μ μμ μΌλ‘ μ€νλλ κ²½μ° Either
ν¨ν€μ§μ μλ isRight
ν¨μλ₯Ό μ¬μ©ν΄ μ μμ μΌλ‘ μ½λκ° μ€νλμλμ§ νμΈν©λλ€. κ·Έ ν getOrElse
ν¨μλ₯Ό μ΄μ©ν΄ Right
μμ κ°μ κΊΌλ΄ κΈ°λν κ°μ²΄μ κ°μ΄ κ°μμ§ νμΈν©λλ€.
λ°λ©΄μ μ€ν¨νμ λλ isLeft
ν¨μλ₯Ό μ΄μ©ν΄ Left
κ° λ°νλμλμ§ νμΈν©λλ€. getOrElse
ν¨μμ μΈμλ‘ μ λ¬λ ν¨μκ° λ°νν κ°κ³Ό κΈ°λνλ κ°μ΄ κ°μμ§ νμΈν©λλ€.
describe('parseν¨μ ν
μ€νΈ (μμΈ)', () => {
const success = '{"a": 1, "b": 2}';
const fail = '{"a": 1, "b"}';
let result;
it('parseν¨μκ° μ μμ μΌλ‘ μ€νλμ κ²½μ°', () => {
result = parse(success);
expect(isRight(result)).toBeTruthy();
expect(getOrElse(() => ({ error: true }))(result)).toMatchObject({
a: 1,
b: 2,
});
});
it('parseν¨μ μ€ν μ€ μμΈκ° λ°μνμ κ²½μ°', () => {
result = parse(fail);
expect(isLeft(result)).toBeTruthy();
expect(getOrElse(() => ({ a: 1 }))(result)).toMatchObject({
a: 1,
});
});
});
Math.random
IO
import { IO } from 'fp-ts/IO';
const random: IO<number> = () => Math.random();
random
ν¨μ ν
μ€νΈνκΈ°Math.random
ν¨μλ μ λ μ€ν¨νμ§ μμ§λ§, λΉκ²°μ λ‘ μ μΈ κ°μ λ°νν©λλ€. λ°λΌμ fp-tsμ ν¨κ» μ¬μ©νκΈ° μν΄μ IO
νμ
μΌλ‘ κ°μΈ κ°μ λ°ννλ©΄ λ©λλ€.
μμ±ν random
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§λ μλμ κ°μ΅λλ€.
random
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§import { random } from '../random';
random
Β ν¨μλ₯Ό ν
μ€νΈνλ μ½λMath.random
μ μμμ μ€λͺ
νλ―μ΄ λΉκ²°μ λ‘ μ μΈ κ°μ λ°νν©λλ€. λ°λΌμ κ°μ mockingνλλ‘ λμμ£Όλ jest.fn
ν¨μκ° λ°νν jest.Mock
μΈμ€ν΄μ€μ mockReturnValue
ν¨μλ₯Ό μ΄μ©ν΄ κ³ μ μ μΌλ‘ κ°μ λ°νν μ μκ² ν΄μ€λλ€. κ·Έ ν mockingλ κ°μ΄ μμ±ν random
ν¨μκ° λ°ννλ κ°κ³Ό κ°μμ§ νμΈν©λλ€.
describe('randomν¨μ ν
μ€νΈ (λλ€)', () => {
let result;
it('Math.randomν¨μλ₯Ό mockingν΄ ν
μ€νΈνκΈ°', () => {
Math.random = jest.fn().mockReturnValue(0.5);
result = random();
expect(result).toBe(0.5);
});
});
localStorage.getItem
IO
import { Option, fromNullable } from 'fp-ts/Option';
import { IO } from 'fp-ts/IO';
function getItem(key: string): IO<Option<string>> {
return () => fromNullable(localStorage.getItem(key));
}
getItem
ν¨μ ν
μ€νΈνκΈ°localStorage.getItem
ν¨μλ κ°μ μ°Ύμ§ λͺ»νλ©΄ null
μ λ°ννλ ν¨μμ
λλ€. λ°λΌμ fromNullable
μ μ΄μ©ν΄ κ°μ μ°Ύμ§ λͺ»νλ©΄ none
μ λ°ννκ³ κ°μ μ°Ύμμ κ²½μ° some
μ λ°ννλλ‘ κ΅¬νν μ μμ΅λλ€.
μμ±ν getItem
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§λ μλμ κ°μ΅λλ€.
getItem
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§import * as S from 'fp-ts/string';
import { isSome, isNone, none, some, getEq, Option } from 'fp-ts/Option';
import { getItem } from '../getItem';
getItem
Β ν¨μλ₯Ό ν
μ€νΈνλ μ½λlocalStorage.getItem
ν¨μλ λΈλΌμ°μ μμμ μ¬μ©ν μ μλ ν¨μμ΄λ―λ‘ jest.fn
μ μ΄μ©ν΄ μνλ key
κ° λ€μ΄μμ κ²½μ°μλ§ κ°μ λ°ννλλ‘ mockingν΄ μ€λλ€. μ¬κΈ°μ μ£Όμν΄μΌ ν μ μ ꡬνν getItem
ν¨μλ ν¨μλ₯Ό λ°ννλ ν¨μμ΄λ―λ‘ ν¨μ νΈμΆ μ°μ°μλ₯Ό ν λ² λ μ¬μ©ν΄μΌ ν©λλ€.
getItem
ν¨μκ° λ°νν ν¨μκ° μ±κ³΅μ μΌλ‘ κ°μ κ°μ Έμμ κ²½μ° some
μΈμ§ νμΈν ν κ°μ΄ κ°μμ§ λΉκ΅ν΄μ£Όκ³ κ°μ μ°Ύμ§ λͺ»ν΄ none
μ λ°νν κ²½μ° none
μΈμ§ νμΈν©λλ€.
describe('getItemν¨μ ν
μ€νΈ (λκΈ° λΆμ ν¨κ³Ό)', () => {
window.localStorage.__proto__.getItem = jest.fn(key => {
if (key === 'success') return 'success';
return null;
});
let result: Option<string>;
const E = getEq(S.Eq);
it('getItemν¨μκ° κ°μ μ μμ μΌλ‘ κ°μ Έμ¨ κ²½μ°', () => {
result = getItem('success')();
expect(isSome(result)).toBeTruthy();
expect(E.equals(result, some('success'))).toBeTruthy();
});
it('getItemν¨μκ° κ°μ κ°μ Έμ€μ§ λͺ»νμ κ²½μ°', () => {
result = getItem('fail')();
expect(isNone(result)).toBeTruthy();
expect(E.equals(result, none)).toBeTruthy();
});
});
throw
ν μμλ APIreadFileSync
IOEither
, tryCatch
import * as fs from 'fs';
import { IOEither, tryCatch } from 'fp-ts/IOEither';
function readFileSync(path: string): IOEither<Error, string> {
return tryCatch(
() => fs.readFileSync(path, 'utf8'),
reason => new Error(String(reason))
);
}
readFileSync
ν¨μ ν
μ€νΈνκΈ°fs
μ readFileSync
ν¨μλ νμΌμ λκΈ°μ μΌλ‘ μ½μ΄ λ¬Έμμ΄ κ°μ λ°ννλ ν¨μμ
λλ€. 맀κ°λ³μλ‘ μ λ¬λ κ²½λ‘μ νμΌμ΄ μμΌλ©΄ μμΈλ₯Ό λ°μμν΅λλ€.
IOEither
νμ
κ³Ό tryCatch
ν¨μλ₯Ό μ΄μ©ν΄ readFileSync
ν¨μλ₯Ό κ°μ ν μ μμ΅λλ€. νμΌμ μ μμ μΌλ‘ μ½μμ κ²½μ° Right
λ‘ string
μ λ°ννκ³ μμΈκ° λ°μν κ²½μ° Left
λ‘ Error
λ₯Ό λ°νν©λλ€.
μμ±ν readFileSync
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§λ μλμ κ°μ΅λλ€.
readFileSync
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§import * as fs from 'fs';
import { Either, isRight, getOrElse, isLeft } from 'fp-ts/lib/Either';
import { readFileSync } from '../readFileSync';
readFileSync
Β ν¨μλ₯Ό ν
μ€νΈνλ μ½λfs
μ readFileSync
ν¨μλ readonly
μ΄λ―λ‘ spyOn
μ μ΄μ©ν΄ κ°μ mockingν΄ μ€λλ€. μνλ κ²½λ‘κ° μ λ¬λμμ λλ§ λ¬Έμμ΄μ λ°ννκ³ κ·Έλ μ§ μμ κ²½μ° μμΈλ₯Ό λ°μμν΅λλ€. μ μμ μΌλ‘ readFileSync
ν¨μκ° μ€νλμμ κ²½μ° Right
νμ
μΈμ§ νμΈν ν getOrElse
λ‘ λ°νλ κ²°κ³Όκ° mockingλ λ¬Έμμ΄μΈμ§ νμΈν©λλ€. μμΈκ° λ°μνμ κ²½μ° Left
νμ
μΈμ§ νμΈν ν getOrElse
λ‘ λ°νλ κ²°κ³Όκ° getOrElse
ν¨μκ° λ°νν λ¬Έμμ΄μΈμ§ νμΈν©λλ€.
describe('readFileSyncν¨μ ν
μ€νΈ (λκΈ° λΆμ ν¨κ³Ό)', () => {
jest.spyOn(fs, 'readFileSync').mockImplementation(path => {
if (path === 'success.txt') return 'success';
throw new Error(`${path} is not found.`);
});
let result: Either<Error, string>;
it('readFileSyncκ° μ μμ μΌλ‘ κ°μ κ°μ Έμμ κ²½μ°', () => {
result = readFileSync('success.txt')();
expect(isRight(result)).toBeTruthy();
expect(getOrElse(() => 'fail')(result)).toBe('success');
});
it('readFileSyncν¨μ μ€ν μ€ μμΈκ° λ°μνμ κ²½μ°', () => {
result = readFileSync('fail.txt')();
expect(isLeft(result)).toBeTruthy();
expect(getOrElse(() => 'fail')(result)).toBe('fail');
});
});
Task
import { createInterface } from 'readline';
import { Task } from 'fp-ts/Task';
const read: Task<string> = () =>
new Promise<string>(resolve => {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('', answer => {
rl.close();
resolve(answer);
});
});
read
ν¨μ ν
μ€νΈνκΈ°Task
νμ
μ κ°μ λ°ννκ³ μ λλ‘ μ€ν¨νμ§ μλ λΉλκΈ° μμ
μ λνλΌ λ μ¬μ©ν©λλ€. read
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν΄μλ readline
μ createInterface
ν¨μλ₯Ό mockingν΄μΌ ν©λλ€. createInterface
ν¨μμ qeustion
ν¨μμ close
ν¨μλ₯Ό μλμ²λΌ mocking ν©λλ€.
read
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν createInterface
ν¨μ mocking
__mocks__/readline.js
module.exports = {
createInterface: jest.fn().mockReturnValue({
question: jest.fn().mockImplementationOnce((questionText, cb) => {
cb('success');
}),
close: () => undefined,
}),
};
μμ±ν read
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§λ μλμ κ°μ΅λλ€.
read
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§import { read } from '../read';
read
Β ν¨μλ₯Ό ν
μ€νΈνλ μ½λjest.mock
ν¨μλ₯Ό νΈμΆν΄ readline
ν¨ν€μ§λ₯Ό mockingν©λλ€. read
ν¨μλ μ λλ‘ μ€ν¨νμ§ μμΌλ©° λΉλκΈ° μμ
μ μ²λ¦¬νλ―λ‘ async
, await
ꡬ문μ μ΄μ©ν΄ read
ν¨μλ₯Ό νΈμΆν΄ κ°μ μ»μ΄ mockingν κ°κ³Ό κ°μμ§ νμΈν©λλ€.
jest.mock('readline');
describe('readν¨μ ν
μ€νΈ (λΉλκΈ° λΆμ ν¨κ³Ό)', () => {
it('readκ° μ μμ μΌλ‘ κ°μ μ½μμ κ²½μ°', async () => {
const result = await read();
expect(result).toBe('success');
});
});
fetch
TaskEither
, tryCatch
import { TaskEither, tryCatch } from 'fp-ts/TaskEither';
function get(url: string): TaskEither<Error, string> {
return tryCatch(
() => fetch(url).then(res => res.text()),
reason => new Error(String(reason))
);
}
get
ν¨μ ν
μ€νΈνκΈ°fetch
ν¨μλ λΉλκΈ° μμ
μ μ²λ¦¬νλ©° μμΈλ₯Ό λ°μμν¬ μ μμ΅λλ€. fetch
ν¨μλ₯Ό κ°μ νκΈ° μν΄ TaskEither
νμ
μ μ΄μ©ν΄ get
ν¨μλ₯Ό μμ±νμ΅λλ€.
μμ±ν get
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§λ μλμ κ°μ΅λλ€.
get
ν¨μλ₯Ό ν
μ€νΈνκΈ° μν ν¨ν€μ§import { Either, getOrElse, isLeft, isRight } from 'fp-ts/lib/Either';
import { get } from '../get';
get
Β ν¨μλ₯Ό ν
μ€νΈνλ μ½λfetch
ν¨μλ Promise<Response>
λ₯Ό λ°ννλ©° get
ν¨μ λ΄λΆμμ μ¬μ©ν text
ν¨μ λν Promise
λ₯Ό λ°νν©λλ€. jest.fn
μ μ΄μ©ν΄ ν¨μλ₯Ό mockingνκ³ νΉμ λ¬Έμμ΄μ΄ λ€μ΄μ¬ κ²½μ° Promise.resolve
λ‘ κ°μ λ°νν΄ μ€λλ€. μνλ λ¬Έμμ΄μ΄ μλ κ²½μ° Promise.reject
λ‘ κ±°λΆν©λλ€.
async
, await
ꡬ문μ μ΄μ©ν΄ get
ν¨μλ‘λΆν° ν¨μλ₯Ό λ°μ νΈμΆμν΅λλ€. μ μμ μΌλ‘ κ°μ κ°μ Έμ¨ κ²½μ° isRight
ν¨μλ‘ Right
νμ
μΈμ§ νμΈν ν getOrElse
ν¨μλ‘ Right
μμ κ°μ κ°μ Έμ€λμ§ νμΈν©λλ€. κ±°λΆλΉν κ²½μ° Left
μΈμ§ νμΈν ν getOrElse
ν¨μμ μ λ¬λ κ°μ΄ λ°νλμλμ§ νμΈν©λλ€.
describe('getν¨μ ν
μ€νΈ (λΉλκΈ° λΆμ ν¨κ³Ό)', () => {
global.fetch = jest.fn(url => {
if (url === 'https://success.com') {
return Promise.resolve({
text: () => Promise.resolve('success'),
}) as Promise<Response>;
}
return Promise.reject('fail');
});
let result: Either<Error, string>;
it('getν¨μκ° μ μμ μΌλ‘ κ°μ κ°μ Έμμ κ²½μ°', async () => {
result = await get('https://success.com')();
expect(isRight(result)).toBeTruthy();
expect(getOrElse(() => 'fail')(result)).toBe('success');
});
it('getν¨μ μ€ν μ€ μμΈκ° λ°μνμ κ²½μ°', async () => {
result = await get('https://fail.com')();
expect(isLeft(result)).toBeTruthy();
expect(getOrElse(() => 'fail')(result)).toBe('fail');
});
});