April 21, 2021
λ³Έ ν¬μ€νΈλ fp-ts 곡μ λ¬Έμμ Learning Resourcesμ μλ Getting Startedμμ μκ°νλ λ¬Έμλ€μ λ²μνλ©° νμ΅ν λ¬Έμμ λλ€. μλ³Έ λ¬Έμλ λ§ν¬μμ νμΈν μ μμΌλ©° μμ±ν μ½λλ€μ μ¬κΈ°μμ νμΈν μ μμ΅λλ€.
λλ‘λ ν¨μν νλ‘κ·Έλλ° μ€νμΌλ‘ μμ±λμ§ μμ μ½λμ μνΈ μ΄μν΄μΌ νλ κ²½μ°κ° μμ΅λλ€. κ·Έλ¬ν μν©μμ fp-tsλ₯Ό μ΄μ©ν΄ ν΄κ²°νλ λ°©λ²μ μ΄ν΄λ³΄κ² μ΅λλ€.
Array.prototype.findIndexOptionimport { 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μ nullundefinedλ nullμ λ°νν μ μλ APIArray.prototype.findOption, fromNullableimport { 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.parseEiter, tryCatchimport { 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.randomIOimport { 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.getItemIOimport { 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ν μμλ APIreadFileSyncIOEither, tryCatchimport * 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');
});
});Taskimport { 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');
});
});fetchTaskEither, tryCatchimport { 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');
});
});