├── .editorconfig ├── .eslintignore ├── .github └── workflows │ └── action.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── __mocks__ └── readline.js ├── eslintrc.json ├── jest.config.js ├── logo.png ├── package.json ├── src ├── functional_design_series │ ├── 1_combinator_one │ │ ├── User.ts │ │ ├── contramap.ts │ │ ├── eqNumber.ts │ │ ├── eqNumbers.ts │ │ ├── eqNumbersNumbers.ts │ │ ├── eqNumbersNumbersNumbers.ts │ │ ├── eqUser.ts │ │ ├── eqUsers.ts │ │ ├── fib.ts │ │ ├── getEq.ts │ │ ├── getMonoid.ts │ │ ├── log.ts │ │ ├── monoidVoid.ts │ │ ├── printFib.ts │ │ ├── printFibs.ts │ │ ├── randomInt.ts │ │ ├── replicateIO.ts │ │ ├── test │ │ │ ├── contramap.test.ts │ │ │ ├── eqNumber.test.ts │ │ │ ├── eqNumbers.test.ts │ │ │ ├── eqNumbersNumbers.test.ts │ │ │ ├── eqNumbersNumbersNumbers.test.ts │ │ │ ├── eqUser.test.ts │ │ │ ├── eqUsers.test.ts │ │ │ ├── fib.test.ts │ │ │ ├── getEq.test.ts │ │ │ ├── getMonoid.test.ts │ │ │ ├── log.test.ts │ │ │ ├── monoidVoid.test.ts │ │ │ ├── printFib.test.ts │ │ │ ├── printFibs.test.ts │ │ │ ├── randomInt.test.ts │ │ │ ├── replicateIO.test.ts │ │ │ └── time.test.ts │ │ └── time.ts │ ├── 2_combinator_two │ │ ├── fastest.ts │ │ ├── ignoreSnd.ts │ │ ├── program.ts │ │ ├── test │ │ │ ├── fastest.test.ts │ │ │ ├── ignoreSnd.test.ts │ │ │ ├── program.test.ts │ │ │ ├── time.test.ts │ │ │ └── withLogging.test.ts │ │ ├── time.ts │ │ └── withLogging.ts │ ├── 3_tagless_final │ │ ├── ioTime.ts │ │ ├── monadIO.ts │ │ ├── monadIOIO.ts │ │ ├── monadIOTask.ts │ │ ├── monadIOTime.ts │ │ ├── taskTime.ts │ │ ├── test │ │ │ ├── ioTime.test.ts │ │ │ ├── monadIOIO.test.ts │ │ │ ├── monadIOTask.test.ts │ │ │ ├── monadIOTime.test.ts │ │ │ ├── taskTime.test.ts │ │ │ ├── timeIO.test.ts │ │ │ └── timeTask.test.ts │ │ ├── timeIO.ts │ │ └── timeTask.ts │ ├── 4_smart_constructors │ │ ├── int.ts │ │ ├── isInt.ts │ │ ├── isNonEmptyString.ts │ │ ├── makeInt.ts │ │ ├── makeNonEmptyString.ts │ │ ├── nonEmptyString.ts │ │ ├── person.ts │ │ └── test │ │ │ ├── isInt.test.ts │ │ │ ├── isNonEmptyString.test.ts │ │ │ ├── makeInt.test.ts │ │ │ ├── makeNonEmptyString.test.ts │ │ │ └── person.test.ts │ ├── 5_tdd_in_typescript │ │ ├── liftA2.ts │ │ ├── push.ts │ │ ├── pushPromise.ts │ │ ├── sequence.ts │ │ └── test │ │ │ ├── liftA2.test.ts │ │ │ ├── push.test.ts │ │ │ ├── pushPromise.test.ts │ │ │ └── sequence.test.ts │ ├── 6_property_based_testing │ │ ├── M.ts │ │ ├── S.ts │ │ ├── associativity.ts │ │ ├── leftIdentity.ts │ │ ├── rightIdentity.ts │ │ └── test │ │ │ ├── M.test.ts │ │ │ ├── S.test.ts │ │ │ ├── associativity.test.ts │ │ │ ├── leftIdentity.test.ts │ │ │ └── rightIdentity.test.ts │ └── 7_algebraic_data_types │ │ ├── either.ts │ │ ├── head.ts │ │ ├── length.ts │ │ ├── list.ts │ │ ├── option.ts │ │ ├── productTypes.ts │ │ ├── readFileWithEither.ts │ │ ├── sumTypes.ts │ │ └── test │ │ ├── either.test.ts │ │ ├── head.test.ts │ │ ├── length.test.ts │ │ ├── list.test.ts │ │ ├── option.test.ts │ │ ├── readFileWithEither.test.ts │ │ └── sumTypes.test.ts ├── getting_started_series │ ├── 0_interoperability │ │ ├── 1_eq │ │ │ ├── __test__ │ │ │ │ ├── elem.test.ts │ │ │ │ ├── eqArrayOfPoints.test.ts │ │ │ │ ├── eqNumber.test.ts │ │ │ │ ├── eqPoint.test.ts │ │ │ │ ├── eqUser.test.ts │ │ │ │ └── eqVector.test.ts │ │ │ ├── elem.ts │ │ │ ├── eq.ts │ │ │ ├── eqArrayOfPoints.ts │ │ │ ├── eqNumber.ts │ │ │ ├── eqPoint.ts │ │ │ ├── eqUser.ts │ │ │ └── eqVector.ts │ │ ├── __test__ │ │ │ ├── find.test.ts │ │ │ ├── findIndex.test.ts │ │ │ ├── get.test.ts │ │ │ ├── getItem.test.ts │ │ │ ├── parse.test.ts │ │ │ ├── random.test.ts │ │ │ ├── read.test.ts │ │ │ └── readFileSync.test.ts │ │ ├── find.ts │ │ ├── findIndex.ts │ │ ├── get.ts │ │ ├── getItem.ts │ │ ├── parse.ts │ │ ├── random.ts │ │ ├── read.ts │ │ └── readFileSync.ts │ ├── 10_io │ │ ├── IO.ts │ │ ├── __test__ │ │ │ ├── localStorage.test.ts │ │ │ ├── log.test.ts │ │ │ ├── now.test.ts │ │ │ ├── program.test.ts │ │ │ ├── random.test.ts │ │ │ ├── randomBool.test.ts │ │ │ ├── randomFile.test.ts │ │ │ ├── readFileSync.test.ts │ │ │ ├── roll.test.ts │ │ │ └── withLogging.test.ts │ │ ├── localStorage.ts │ │ ├── log.ts │ │ ├── now.ts │ │ ├── program.ts │ │ ├── random.ts │ │ ├── randomBool.ts │ │ ├── randomFile.ts │ │ ├── readFileSync.ts │ │ ├── roll.ts │ │ └── withLogging.ts │ ├── 11_reader │ │ ├── Dependencies.ts │ │ ├── Reader.ts │ │ ├── __test__ │ │ │ ├── askExample.test.ts │ │ │ ├── compositionExample.test.ts │ │ │ ├── depsExample.test.ts │ │ │ ├── example.test.ts │ │ │ └── readerDepsExample.test.ts │ │ ├── askExample.ts │ │ ├── compositionExample.ts │ │ ├── depsExample.ts │ │ ├── example.ts │ │ └── readerDepsExample.ts │ ├── 2_ord │ │ ├── __test__ │ │ │ ├── byAge.test.ts │ │ │ ├── getOlder.test.ts │ │ │ ├── getYounger.test.ts │ │ │ ├── min.test.ts │ │ │ └── ordNumber.test.ts │ │ ├── byAge.ts │ │ ├── getOlder.ts │ │ ├── getYounger.ts │ │ ├── min.ts │ │ ├── ord.ts │ │ └── ordNumber.ts │ ├── 3_semigroup │ │ ├── __test__ │ │ │ ├── appliedSemigroup.test.ts │ │ │ ├── getArraySemigroup.test.ts │ │ │ ├── getFirstSemigroup.test.ts │ │ │ ├── getLastSemigroup.test.ts │ │ │ ├── isPositiveXY.test.ts │ │ │ ├── of.test.ts │ │ │ ├── product.test.ts │ │ │ ├── semigroupCustomer.test.ts │ │ │ ├── semigroupMax.test.ts │ │ │ ├── semigroupMin.test.ts │ │ │ ├── semigroupPoint.test.ts │ │ │ ├── semigroupPredicate.test.ts │ │ │ ├── semigroupProduct.test.ts │ │ │ ├── semigroupString.test.ts │ │ │ ├── semigroupSum.test.ts │ │ │ ├── semigroupVector.test.ts │ │ │ └── sum.test.ts │ │ ├── appliedSemigroup.ts │ │ ├── getArraySemigroup.ts │ │ ├── getFirstSemigroup.ts │ │ ├── getLastSemigroup.ts │ │ ├── isPositiveXY.ts │ │ ├── of.ts │ │ ├── product.ts │ │ ├── semigroup.ts │ │ ├── semigroupCustomer.ts │ │ ├── semigroupMax.ts │ │ ├── semigroupMin.ts │ │ ├── semigroupPoint.ts │ │ ├── semigroupPredicate.ts │ │ ├── semigroupProduct.ts │ │ ├── semigroupString.ts │ │ ├── semigroupSum.ts │ │ ├── semigroupVector.ts │ │ └── sum.ts │ ├── 4_monoid │ │ ├── __test__ │ │ │ ├── appliedMonoidSum.test.ts │ │ │ ├── firstMonoid.test.ts │ │ │ ├── foldMonoidAll.test.ts │ │ │ ├── foldMonoidAny.test.ts │ │ │ ├── foldMonoidProduct.test.ts │ │ │ ├── foldMonoidString.test.ts │ │ │ ├── foldMonoidSum.test.ts │ │ │ ├── lastMonoid.test.ts │ │ │ ├── monoidAll.test.ts │ │ │ ├── monoidAny.test.ts │ │ │ ├── monoidPoint.test.ts │ │ │ ├── monoidProduct.test.ts │ │ │ ├── monoidSettings.test.ts │ │ │ ├── monoidString.test.ts │ │ │ ├── monoidSum.test.ts │ │ │ └── monoidVectore.test.ts │ │ ├── appliedMonoidSum.ts │ │ ├── firstMonoid.ts │ │ ├── foldMonoidAll.ts │ │ ├── foldMonoidAny.ts │ │ ├── foldMonoidProduct.ts │ │ ├── foldMonoidString.ts │ │ ├── foldMonoidSum.ts │ │ ├── lastMonoid.ts │ │ ├── monoid.ts │ │ ├── monoidAll.ts │ │ ├── monoidAny.ts │ │ ├── monoidPoint.ts │ │ ├── monoidProduct.ts │ │ ├── monoidSettings.ts │ │ ├── monoidString.ts │ │ ├── monoidSum.ts │ │ └── monoidVector.ts │ ├── 5_category │ │ ├── __test__ │ │ │ ├── compose.test.ts │ │ │ └── fgh.test.ts │ │ ├── compose.ts │ │ └── fgh.ts │ ├── 6_functor │ │ ├── __test__ │ │ │ ├── arrayLift.test.ts │ │ │ ├── functorResponse.test.ts │ │ │ ├── optionLift.test.ts │ │ │ └── taskLift.test.ts │ │ ├── arrayLift.ts │ │ ├── functorResponse.ts │ │ ├── optionLift.ts │ │ └── taskLift.ts │ ├── 7_applicative │ │ ├── __test__ │ │ │ ├── applicativeArray.test.ts │ │ │ ├── applicativeOption.test.ts │ │ │ ├── applicativeTask.test.ts │ │ │ ├── liftA2.test.ts │ │ │ └── liftA3.test.ts │ │ ├── applicative.ts │ │ ├── applicativeArray.ts │ │ ├── applicativeOption.ts │ │ ├── applicativeTask.ts │ │ ├── apply.ts │ │ ├── liftA2.ts │ │ └── liftA3.ts │ ├── 8_monad │ │ ├── __test__ │ │ │ ├── flatten.test.ts │ │ │ ├── flattenFollowersOfFollowers.test.ts │ │ │ ├── flattenInverseHead.test.ts │ │ │ ├── followersOfFollowers.test.ts │ │ │ ├── getFollowers.test.ts │ │ │ ├── headInverse.test.ts │ │ │ ├── mapFollowersOfFollowers.test.ts │ │ │ └── mapInverseHead.test.ts │ │ ├── flatten.ts │ │ ├── flattenFollowersOfFollowers.ts │ │ ├── flattenInverseHead.ts │ │ ├── followersOfFollowers.ts │ │ ├── getFollowers.ts │ │ ├── headInverse.ts │ │ ├── inverse.ts │ │ ├── mapFollowersOfFollowers.ts │ │ ├── mapInverseHead.ts │ │ └── user.ts │ └── 9_either_vs_validation │ │ ├── __test__ │ │ ├── chainValidatePassword.test.ts │ │ ├── minLength.test.ts │ │ ├── minLengthV.test.ts │ │ ├── oneCapital.test.ts │ │ ├── oneCapitalV.test.ts │ │ ├── oneNumber.test.ts │ │ ├── oneNumberV.test.ts │ │ ├── toPerson.test.ts │ │ ├── validateAge.test.ts │ │ ├── validateName.test.ts │ │ ├── validatePassword.test.ts │ │ └── validatePerson.test.ts │ │ ├── applicativeValidation.ts │ │ ├── chainValidatePassword.ts │ │ ├── lift.ts │ │ ├── minLength.ts │ │ ├── minLengthV.ts │ │ ├── oneCapital.ts │ │ ├── oneCapitalV.ts │ │ ├── oneNumber.ts │ │ ├── oneNumberV.ts │ │ ├── person.ts │ │ ├── toPerson.ts │ │ ├── validateAge.ts │ │ ├── validateName.ts │ │ ├── validatePassword.ts │ │ └── validatePerson.ts └── index.ts ├── tsconfig.json ├── tsconfig.release.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | insert_final_newline = false 11 | trim_trailing_whitespace = false 12 | 13 | [*.{js,jsx,json,ts,tsx,yml}] 14 | indent_size = 2 15 | indent_style = space -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /**/*.js 2 | -------------------------------------------------------------------------------- /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Setup Node.js environment 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: '14' 16 | 17 | - uses: actions/cache@v2 18 | id: yarn-cache 19 | with: 20 | path: | 21 | node_modules 22 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 23 | restore-keys: | 24 | ${{ runner.os }}-yarn- 25 | 26 | - name: Install dependencies 27 | if: steps.yarn-cache.outputs.cache-hit != 'true' 28 | run: yarn install 29 | 30 | - name: Run test 31 | run: yarn test 32 | 33 | - name: Upload coverage to Codecov 34 | uses: codecov/codecov-action@v1 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependencies 7 | node_modules/ 8 | 9 | # Coverage 10 | coverage 11 | 12 | # Transpiled files 13 | build/ 14 | 15 | # VS Code 16 | .vscode 17 | !.vscode/tasks.js 18 | 19 | # JetBrains IDEs 20 | .idea/ 21 | 22 | # Optional npm cache directory 23 | .npm 24 | 25 | # Optional eslint cache 26 | .eslintcache 27 | 28 | # Misc 29 | .DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "overrides": [ 5 | { 6 | "files": "*.ts", 7 | "options": { 8 | "parser": "typescript" 9 | } 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Minsu Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__mocks__/readline.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | createInterface: jest.fn().mockReturnValue({ 3 | question: jest.fn().mockImplementationOnce((questionText, cb) => { 4 | cb('success'); 5 | }), 6 | close: () => undefined, 7 | }), 8 | }; 9 | -------------------------------------------------------------------------------- /eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "project": "tsconfig.json", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint", "jest"], 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/eslint-recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:jest/recommended", 18 | "prettier" 19 | ], 20 | "rules": {} 21 | } 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 7 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)x?$', 8 | coverageDirectory: 'coverage', 9 | collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}', '!src/**/*.d.ts'], 10 | }; 11 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/getting-started-fp-ts/033d5916a9d8afa3bfb61bc21c41a0c6885755ac/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting-started-fp-ts", 3 | "version": "0.0.0", 4 | "description": "Typescript Functional Programming with fp-ts", 5 | "author": "Minsu Kim ", 6 | "dependencies": { 7 | "tslib": "~2.1.0" 8 | }, 9 | "devDependencies": { 10 | "@types/jest": "~26.0.20", 11 | "@types/node": "~14.14.31", 12 | "@typescript-eslint/eslint-plugin": "~4.16.1", 13 | "@typescript-eslint/parser": "~4.16.1", 14 | "eslint": "~7.21.0", 15 | "eslint-config-prettier": "~8.1.0", 16 | "eslint-plugin-jest": "~24.1.5", 17 | "fast-check": "^2.17.0", 18 | "fp-ts": "^2.10.2", 19 | "jest": "~26.6.3", 20 | "prettier": "~2.2.1", 21 | "rimraf": "~3.0.2", 22 | "ts-jest": "~26.5.2", 23 | "tsutils": "~3.20.0", 24 | "typescript": "~4.2.2" 25 | }, 26 | "scripts": { 27 | "start": "node build/src/index.js", 28 | "clean": "rimraf coverage build tmp", 29 | "build": "tsc -p tsconfig.release.json", 30 | "build:watch": "tsc -w -p tsconfig.release.json", 31 | "lint": "eslint . --ext .ts,.tsx", 32 | "pretty": "prettier --write \"./**/*.{ts,tsx,js,jsx,json}\"", 33 | "test": "TZ=UTC jest --coverage", 34 | "test:watch": "jest --watch" 35 | }, 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/User.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/contramap.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | import { fromEquals } from 'fp-ts/lib/Eq'; 3 | 4 | export const contramap = (f: (b: B) => A) => (E: Eq): Eq => 5 | fromEquals((x, y) => E.equals(f(x), f(y))); 6 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/eqNumber.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | 3 | /** `number`를 위한 원시적인 `Eq` 인스턴스 */ 4 | export const eqNumber: Eq = { 5 | equals: (x, y) => x === y, 6 | }; 7 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/eqNumbers.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | import { getEq } from './getEq'; 3 | import { eqNumber } from './eqNumber'; 4 | 5 | // 파생된 6 | export const eqNumbers: Eq> = getEq(eqNumber); 7 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/eqNumbersNumbers.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | import { getEq } from './getEq'; 3 | import { eqNumbers } from './eqNumbers'; 4 | 5 | // 파생된 6 | export const eqNumbersNumbers: Eq>> = getEq( 7 | eqNumbers, 8 | ); 9 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/eqNumbersNumbersNumbers.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | import { getEq } from './getEq'; 3 | import { eqNumbersNumbers } from './eqNumbersNumbers'; 4 | 5 | // 파생된 6 | export const eqNumbersNumbersNumbers: Eq< 7 | ReadonlyArray>> 8 | > = getEq(eqNumbersNumbers); 9 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/eqUser.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | import type { User } from './User'; 3 | import { contramap } from 'fp-ts/lib/Eq'; 4 | import { pipe } from 'fp-ts/lib/function'; 5 | import * as N from 'fp-ts/lib/number'; 6 | 7 | export const eqUser: Eq = pipe( 8 | N.Eq, 9 | contramap((user: User) => user.id), 10 | ); 11 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/eqUsers.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | import type { User } from './User'; 3 | import * as RA from 'fp-ts/lib/ReadonlyArray'; 4 | import { eqUser } from './eqUser'; 5 | 6 | export const eqUsers: Eq> = RA.getEq(eqUser); 7 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/fib.ts: -------------------------------------------------------------------------------- 1 | export function fib(n: number): number { 2 | return n <= 1 ? 1 : fib(n - 1) + fib(n - 2); 3 | } 4 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/getEq.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | import { fromEquals } from 'fp-ts/lib/Eq'; 3 | 4 | export function getEq(E: Eq): Eq> { 5 | return fromEquals( 6 | (xs, ys) => 7 | xs.length === ys.length && xs.every((x, i) => E.equals(x, ys[i])), 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/getMonoid.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import type { Monoid } from 'fp-ts/lib/Monoid'; 3 | 4 | export function getMonoid(M: Monoid): Monoid> { 5 | return { 6 | concat: (x, y) => () => M.concat(x(), y()), 7 | empty: () => M.empty, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/log.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | 3 | /** 콘솔에 메세지를 출력하는 함수 */ 4 | export function log(message: unknown): IO { 5 | return () => console.log(message); 6 | } 7 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/monoidVoid.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from 'fp-ts/lib/Monoid'; 2 | 3 | /** `void`를 위한 원시적인 `Monoid` 인스턴스 */ 4 | export const monoidVoid: Monoid = { 5 | concat: () => undefined, 6 | empty: undefined, 7 | }; 8 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/printFib.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { chain } from 'fp-ts/lib/IO'; 3 | import { pipe } from 'fp-ts/lib/function'; 4 | import { fib } from './fib'; 5 | import { log } from './log'; 6 | import { randomInt } from './randomInt'; 7 | 8 | /** 임의의 피보나치 수열을 계산하고 결과를 콘솔에 출력하는 함수 */ 9 | export const printFib: IO = pipe( 10 | randomInt(30, 35), 11 | chain((n) => log(fib(n))), 12 | ); 13 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/printFibs.ts: -------------------------------------------------------------------------------- 1 | import { printFib } from './printFib'; 2 | import { replicateIO } from './replicateIO'; 3 | 4 | export const printFibs = (n: number) => replicateIO(n, printFib); 5 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/randomInt.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | 3 | /** `low`와 `high` 사이의 임의의 정수를 반환하는 함수. */ 4 | export const randomInt = (low: number, high: number): IO => { 5 | return () => Math.floor((high - low + 1) * Math.random() + low); 6 | }; 7 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/replicateIO.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { concatAll } from 'fp-ts/lib/Monoid'; 3 | import { replicate } from 'fp-ts/lib/ReadonlyArray'; 4 | import { getMonoid } from './getMonoid'; 5 | import { monoidVoid } from './monoidVoid'; 6 | 7 | export function replicateIO(n: number, mv: IO): IO { 8 | return concatAll(getMonoid(monoidVoid))(replicate(n, mv)); 9 | } 10 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/contramap.test.ts: -------------------------------------------------------------------------------- 1 | import * as N from 'fp-ts/lib/number'; 2 | import * as S from 'fp-ts/lib/string'; 3 | import { contramap } from '../contramap'; 4 | 5 | describe('B => A 함수를 받아 Eq => Eq를 만드는 contramap 테스트', () => { 6 | const identity = (x: T): T => x; 7 | const numberToString = (n: number): string => n.toString(); 8 | const pointToNumber = (p: { x: number; y: number }): number => p.x; 9 | it('B와 A가 타입이 동일하게 number인 경우 테스트', () => { 10 | const contramapWithIdentity = contramap(identity); 11 | expect(contramapWithIdentity(N.Eq).equals(1, 1)).toBeTruthy(); 12 | expect(contramapWithIdentity(N.Eq).equals(1, 2)).toBeFalsy(); 13 | }); 14 | it('B가 number A가 string인 경우 테스트', () => { 15 | const contramapWithNumberToString = contramap(numberToString); 16 | expect(contramapWithNumberToString(S.Eq).equals(1, 1)).toBeTruthy(); 17 | expect(contramapWithNumberToString(S.Eq).equals(-1, 1)).toBeFalsy(); 18 | }); 19 | it('조금 더 복잡한 타입인 객체일 경우 테스트', () => { 20 | const contramapWithPointToNumber = contramap(pointToNumber); 21 | expect( 22 | contramapWithPointToNumber(N.Eq).equals({ x: 1, y: 1 }, { x: 1, y: 2 }), 23 | ).toBeTruthy(); 24 | expect( 25 | contramapWithPointToNumber(N.Eq).equals({ x: -1, y: 1 }, { x: 1, y: 1 }), 26 | ).toBeFalsy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/eqNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { eqNumber } from '../eqNumber'; 2 | 3 | describe('number 타입을 위한 원시적인 Eq 인스턴스 eqNumber 테스트', () => { 4 | it('eqNumber 인스턴스 equals 함수 테스트', () => { 5 | expect(eqNumber.equals(1, 1)).toBeTruthy(); 6 | expect(eqNumber.equals(1, 2)).toBeFalsy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/eqNumbers.test.ts: -------------------------------------------------------------------------------- 1 | import { eqNumbers } from '../eqNumbers'; 2 | 3 | describe('ReadonlyArray 타입을 위한 Eq 인스턴스 eqNumbers 테스트', () => { 4 | it('eqNumbers 인스턴스 equals 함수 테스트', () => { 5 | expect(eqNumbers.equals([1], [1])).toBeTruthy(); 6 | expect(eqNumbers.equals([1, 2], [1, 2])).toBeTruthy(); 7 | expect(eqNumbers.equals([1], [2])).toBeFalsy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/eqNumbersNumbers.test.ts: -------------------------------------------------------------------------------- 1 | import { eqNumbersNumbers } from '../eqNumbersNumbers'; 2 | 3 | describe('ReadonlyArray> 타입을 위한 Eq 인스턴스 eqNumbersNumbers 테스트', () => { 4 | it('eqNumbersNumbers 인스턴스 equals 함수 테스트', () => { 5 | expect(eqNumbersNumbers.equals([[1]], [[1]])).toBeTruthy(); 6 | expect( 7 | eqNumbersNumbers.equals( 8 | [ 9 | [1, 2], 10 | [2, 3], 11 | ], 12 | [ 13 | [1, 2], 14 | [2, 3], 15 | ], 16 | ), 17 | ).toBeTruthy(); 18 | expect(eqNumbersNumbers.equals([[1]], [[2]])).toBeFalsy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/eqNumbersNumbersNumbers.test.ts: -------------------------------------------------------------------------------- 1 | import { eqNumbersNumbersNumbers } from '../eqNumbersNumbersNumbers'; 2 | 3 | describe('ReadonlyArray>> 타입을 위한 Eq 인스턴스 eqNumbersNumbersNumbers 테스트', () => { 4 | it('eqNumbersNumbersNumbers 인스턴스 equals 함수 테스트', () => { 5 | expect(eqNumbersNumbersNumbers.equals([[[1]]], [[[1]]])).toBeTruthy(); 6 | expect( 7 | eqNumbersNumbersNumbers.equals( 8 | [ 9 | [[1], [2]], 10 | [[2], [3]], 11 | ], 12 | [ 13 | [[1], [2]], 14 | [[2], [3]], 15 | ], 16 | ), 17 | ).toBeTruthy(); 18 | expect(eqNumbersNumbersNumbers.equals([[[1]]], [[[2]]])).toBeFalsy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/eqUser.test.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '../User'; 2 | import { eqUser } from '../eqUser'; 3 | 4 | describe('User 타입을 위한 Eq 인스턴스 eqUser 테스트', () => { 5 | const user0: User = { id: 1, name: 'test' }; 6 | const user1: User = { id: 1, name: 'test1' }; 7 | const user2: User = { id: 2, name: 'test2' }; 8 | const user3: User = { id: 3, name: 'test1' }; 9 | it('eqUser 인스턴스 equals 함수 테스트 (id가 같은 경우)', () => { 10 | expect(eqUser.equals(user0, user1)).toBeTruthy(); 11 | }); 12 | it('eqUser 인스턴스 equals 함수 테스트 (name이 같은 경우)', () => { 13 | expect(eqUser.equals(user1, user3)).toBeFalsy(); 14 | }); 15 | it('eqUser 인스턴스 equals 함수 테스트 (id와 name이 다른 경우)', () => { 16 | expect(eqUser.equals(user2, user3)).toBeFalsy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/eqUsers.test.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '../User'; 2 | import { eqUsers } from '../eqUsers'; 3 | 4 | describe('Array 타입을 위한 Eq 인스턴스 eqUsers 테스트', () => { 5 | const user0: User = { id: 1, name: 'test' }; 6 | const user1: User = { id: 1, name: 'test1' }; 7 | const user2: User = { id: 2, name: 'test2' }; 8 | const user3: User = { id: 3, name: 'test1' }; 9 | it('eqUsers 인스턴스 equals 함수 테스트 (모두 id가 같은 경우)', () => { 10 | expect(eqUsers.equals([user0, user1], [user1, user0])).toBeTruthy(); 11 | }); 12 | it('eqUsers 인스턴스 equals 함수 테스트 (하나만 id가 같은 경우)', () => { 13 | expect(eqUsers.equals([user1, user0], [user3, user1])).toBeFalsy(); 14 | }); 15 | it('eqUsers 인스턴스 equals 함수 테스트 (모두 id가 다른 경우)', () => { 16 | expect(eqUsers.equals([user2, user0], [user3, user2])).toBeFalsy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/fib.test.ts: -------------------------------------------------------------------------------- 1 | import { fib } from '../fib'; 2 | 3 | describe('피보나치 수열의 n번째 값을 반환하는 fib 함수 테스트', () => { 4 | it('n이 1일 경우 fib 함수 테스트', () => { 5 | expect(fib(1)).toBe(1); 6 | }); 7 | it('n이 2일 경우 fib 함수 테스트', () => { 8 | expect(fib(2)).toBe(2); 9 | }); 10 | it('n이 3일 경우 fib 함수 테스트', () => { 11 | expect(fib(3)).toBe(3); 12 | expect(fib(3)).toBe(fib(1) + fib(2)); 13 | }); 14 | it('n이 10일 경우 fib 함수 테스트', () => { 15 | expect(fib(10)).toBe(89); 16 | expect(fib(10)).toBe(fib(8) + fib(9)); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/getEq.test.ts: -------------------------------------------------------------------------------- 1 | import { getEq } from '../getEq'; 2 | import * as N from 'fp-ts/lib/number'; 3 | import * as S from 'fp-ts/lib/string'; 4 | 5 | describe('Eq => Eq> 시그니처의 getEq 콤비네이터 테스트', () => { 6 | const numberEq = getEq(N.Eq); 7 | const stringEq = getEq(S.Eq); 8 | it('A타입이 number이며 값이 같은 경우', () => { 9 | expect(numberEq.equals([1, 2, 3], [1, 2, 3])).toBeTruthy(); 10 | }); 11 | it('A타입이 number이며 값이 다른 경우', () => { 12 | expect(numberEq.equals([1, 2, 3], [1, 2, 4])).toBeFalsy(); 13 | }); 14 | it('A타입이 number이며 길이와 값 다른 경우', () => { 15 | expect(numberEq.equals([1, 2, 3], [1, 2])).toBeFalsy(); 16 | }); 17 | it('A타입이 string이며 값이 같은 경우', () => { 18 | expect(stringEq.equals(['a', 'b', 'c'], ['a', 'b', 'c'])).toBeTruthy(); 19 | }); 20 | it('A타입이 string이며 값이 다른 경우', () => { 21 | expect(stringEq.equals(['a', 'b', 'c'], ['a', 'b', 'd'])).toBeFalsy(); 22 | }); 23 | it('A타입이 string이며 길이와 값 다른 경우', () => { 24 | expect(stringEq.equals(['a', 'b', 'c'], ['a', 'b'])).toBeFalsy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/getMonoid.test.ts: -------------------------------------------------------------------------------- 1 | import * as N from 'fp-ts/lib/number'; 2 | import * as S from 'fp-ts/lib/string'; 3 | import { of } from 'fp-ts/lib/IO'; 4 | import { getMonoid } from '../getMonoid'; 5 | 6 | describe('Monoid 인스턴스를 받아 IO 인스턴스를 반환하는 getMonoid 함수 테스트', () => { 7 | const numberMonoidSum = N.MonoidSum; 8 | const numberMonoidProduct = N.MonoidProduct; 9 | const stringMonoid = S.Monoid; 10 | it('인자로 전달된 Monoid가 MonoidSum일 경우 테스트', () => { 11 | const { concat, empty } = getMonoid(numberMonoidSum); 12 | expect(concat(of(1), of(2))()).toBe(3); 13 | expect(empty()).toBe(0); 14 | }); 15 | it('인자로 전달된 Monoid가 MonoidProduct일 경우 테스트', () => { 16 | const { concat, empty } = getMonoid(numberMonoidProduct); 17 | expect(concat(of(1), of(2))()).toBe(2); 18 | expect(empty()).toBe(1); 19 | }); 20 | it('인자로 전달된 Monoid가 문자열 Monoid일 경우 테스트', () => { 21 | const { concat, empty } = getMonoid(stringMonoid); 22 | expect(concat(of('aaa'), of('bbb'))()).toBe('aaabbb'); 23 | expect(empty()).toBe(''); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/log.test.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../log'; 2 | 3 | describe('IO를 반환하는 log 함수 테스트', () => { 4 | const spy = jest.spyOn(console, 'log').mockImplementation(); 5 | beforeEach(() => { 6 | spy.mockClear(); 7 | }); 8 | it('입력으로 전달된 값이 정상적으로 콘솔에 출력되는지 테스트', () => { 9 | log('Hello World!')(); 10 | expect(console.log).toBeCalledTimes(1); 11 | expect(console.log).toHaveBeenCalledWith('Hello World!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/monoidVoid.test.ts: -------------------------------------------------------------------------------- 1 | import { monoidVoid } from '../monoidVoid'; 2 | 3 | describe('void 타입을 위한 Monoid 인스턴스 monoidVoid 테스트', () => { 4 | it('monoidVoid 인스턴스 concat 함수 테스트', () => { 5 | expect(monoidVoid.concat()).toBe(undefined); 6 | }); 7 | it('monoidVoid 인스턴스 empty 속성 테스트', () => { 8 | expect(monoidVoid.empty).toBe(undefined); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/printFib.test.ts: -------------------------------------------------------------------------------- 1 | import { printFib } from '../printFib'; 2 | 3 | describe('30에서 35번째 사이의 임의의 피보나치 수열 값을 출력하는 printFib 함수 테스트', () => { 4 | const spy = jest.spyOn(console, 'log').mockImplementation(); 5 | beforeEach(() => { 6 | spy.mockClear(); 7 | }); 8 | it('Math.random 함수가 최솟값을 반환할 경우 30번째 수열의 값을 출력하는지 테스트', () => { 9 | global.Math.random = jest.fn().mockReturnValue(0); 10 | printFib(); 11 | expect(console.log).toBeCalledTimes(1); 12 | expect(console.log).toHaveBeenCalledWith(1346269); 13 | }); 14 | it('Math.random 함수가 최댓값을 반환할 경우 35번째 수열의 값을 출력하는지 테스트', () => { 15 | global.Math.random = jest.fn().mockReturnValue(0.99); 16 | printFib(); 17 | expect(console.log).toBeCalledTimes(1); 18 | expect(console.log).toHaveBeenCalledWith(14930352); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/printFibs.test.ts: -------------------------------------------------------------------------------- 1 | import { printFibs } from '../printFibs'; 2 | 3 | describe('30에서 35번째 사이의 임의의 피보나치 수열 값을 n번 출력하는 printFibs 함수 테스트', () => { 4 | const spy = jest.spyOn(console, 'log').mockImplementation(); 5 | beforeEach(() => { 6 | spy.mockClear(); 7 | }); 8 | it('Math.random 함수가 최솟값을 반환할 경우 30번째 수열의 값을 n번 출력하는지 테스트', () => { 9 | global.Math.random = jest.fn().mockReturnValue(0); 10 | printFibs(3)(); 11 | expect(console.log).toBeCalledTimes(3); 12 | expect(console.log).toHaveBeenCalledWith(1346269); 13 | }); 14 | it('Math.random 함수가 최댓값을 반환할 경우 35번째 수열의 값을 n번 출력하는지 테스트', () => { 15 | global.Math.random = jest.fn().mockReturnValue(0.99); 16 | printFibs(3)(); 17 | expect(console.log).toBeCalledTimes(3); 18 | expect(console.log).toHaveBeenCalledWith(14930352); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/randomInt.test.ts: -------------------------------------------------------------------------------- 1 | import { randomInt } from '../randomInt'; 2 | 3 | describe('low에서 hight 사이의 정수를 반환하는 IO를 반환하는 randomInt 함수 테스트', () => { 4 | it('Math.random 함수가 최댓값을 반환 했을 때 randomInt 함수 테스트', () => { 5 | global.Math.random = jest.fn().mockReturnValue(0.99); 6 | expect(randomInt(5, 10)()).toBe(10); 7 | }); 8 | it('Math.random 함수가 최솟값을 반환 했을 때 randomInt 함수 테스트', () => { 9 | global.Math.random = jest.fn().mockReturnValue(0); 10 | expect(randomInt(5, 10)()).toBe(5); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/replicateIO.test.ts: -------------------------------------------------------------------------------- 1 | import { replicateIO } from '../replicateIO'; 2 | 3 | describe('n개의 IO를 복제하는 replicateIO 함수 테스트', () => { 4 | const spy = jest.spyOn(console, 'log').mockImplementation(); 5 | beforeEach(() => { 6 | spy.mockClear(); 7 | }); 8 | it('콘솔에 출력을하는 IO가 n번 만큼 실행되는지 테스트', () => { 9 | replicateIO(3, () => console.log('Hello World!'))(); 10 | expect(console.log).toBeCalledTimes(3); 11 | expect(console.log).toHaveBeenCalledWith('Hello World!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/test/time.test.ts: -------------------------------------------------------------------------------- 1 | import { time } from '../time'; 2 | 3 | describe('IO를 받아 실행시간을 측정하는 time 함수 테스트', () => { 4 | const spy = jest.spyOn(console, 'log').mockImplementation(); 5 | const baseTime = new Date().getTime(); 6 | beforeEach(() => { 7 | let callCount = 0; 8 | spyOn(window, 'Date').and.callFake(function () { 9 | return { 10 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 11 | }; 12 | }); 13 | spy.mockClear(); 14 | }); 15 | it('타입 매개변수 A가 number인 경우 테스트', () => { 16 | expect(time(() => 1)()).toBe(1); 17 | expect(console.log).toBeCalledTimes(1); 18 | expect(console.log).toHaveBeenCalledWith(`Elapsed: 10`); 19 | }); 20 | it('타입 매개변수 A가 string인 경우 테스트', () => { 21 | expect(time(() => 'test')()).toBe('test'); 22 | expect(console.log).toBeCalledTimes(1); 23 | expect(console.log).toHaveBeenCalledWith(`Elapsed: 10`); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/functional_design_series/1_combinator_one/time.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { Monad } from 'fp-ts/lib/IO'; 3 | import { now } from 'fp-ts/lib/Date'; 4 | import { log } from 'fp-ts/lib/Console'; 5 | 6 | export function time(ma: IO): IO { 7 | return Monad.chain(now, (start) => 8 | Monad.chain(ma, (a) => 9 | Monad.chain(now, (end) => 10 | Monad.map(log(`Elapsed: ${end - start}`), () => a), 11 | ), 12 | ), 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/fastest.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { Apply } from 'fp-ts/lib/IO'; 3 | import { Ord } from 'fp-ts/lib/number'; 4 | import { contramap } from 'fp-ts/lib/Ord'; 5 | import { getApplySemigroup } from 'fp-ts/lib/Apply'; 6 | import { concatAll, min } from 'fp-ts/lib/Semigroup'; 7 | import { time } from './time'; 8 | import { ignoreSnd } from './ignoreSnd'; 9 | 10 | export function fastest(head: IO, tail: Array>): IO { 11 | const ordTuple = contramap(([_, elapsed]: [A, number]) => elapsed)(Ord); 12 | const semigroupTuple = min(ordTuple); 13 | const semigroupIO = getApplySemigroup(Apply)(semigroupTuple); 14 | const fastest = concatAll(semigroupIO)(time(head))(tail.map(time)); 15 | return ignoreSnd(fastest); 16 | } 17 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/ignoreSnd.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { Monad } from 'fp-ts/lib/IO'; 3 | 4 | export function ignoreSnd(ma: IO<[A, unknown]>): IO { 5 | return Monad.map(ma, ([a]) => a); 6 | } 7 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/program.ts: -------------------------------------------------------------------------------- 1 | import { randomInt } from 'fp-ts/lib/Random'; 2 | import { map } from 'fp-ts/lib/IO'; 3 | import { withLogging } from './withLogging'; 4 | 5 | function fib(n: number): number { 6 | return n <= 1 ? 1 : fib(n - 1) + fib(n - 2); 7 | } 8 | 9 | export const program = withLogging(map(fib)(randomInt(30, 35))); 10 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/test/fastest.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'fp-ts/lib/IO'; 2 | import { fastest } from '../fastest'; 3 | 4 | describe('여러개의 IO 중 가장 빨리 실행된 것을 반환하는 fastest 함수 테스트', () => { 5 | const baseTime = new Date().getTime(); 6 | it('IO타입이 IO일 경우 테스트 (처음 실행된 것이 가장 빠른 경우)', () => { 7 | let callCount = 0; 8 | spyOn(window, 'Date').and.callFake(function () { 9 | return { 10 | getTime: () => (callCount++ % 2 ? baseTime + callCount : baseTime), 11 | }; 12 | }); 13 | expect(fastest(of(30), [of(20), of(10)])()).toBe(30); 14 | }); 15 | it('IO타입이 IO일 경우 테스트 (모두 동일하게 실행된 경우)', () => { 16 | let callCount = 0; 17 | spyOn(window, 'Date').and.callFake(function () { 18 | return { 19 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 20 | }; 21 | }); 22 | expect(fastest(of(10), [of(20), of(30)])()).toBe(10); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/test/ignoreSnd.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'fp-ts/lib/IO'; 2 | import { ignoreSnd } from '../ignoreSnd'; 3 | 4 | describe('IO<[A, unknown]>이 반환되면 A타입 값만 반환하는 ignoreSnd 함수 테스트', () => { 5 | it('A타입이 number인 경우', () => { 6 | expect(ignoreSnd(of([10, 20]))()).toBe(10); 7 | }); 8 | it('A타입이 string인 경우', () => { 9 | expect(ignoreSnd(of(['test', 20]))()).toBe('test'); 10 | }); 11 | it('A타입이 Array인 경우', () => { 12 | expect(ignoreSnd(of([[1, 2, 3], 20]))()).toMatchObject([1, 2, 3]); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/test/program.test.ts: -------------------------------------------------------------------------------- 1 | import { program } from '../program'; 2 | 3 | describe('30에서 35번째 사이의 랜덤한 피보나치 수를 반환하는 program 함수 테스트', () => { 4 | const spy = jest.spyOn(console, 'log').mockImplementation(); 5 | const baseTime = new Date().getTime(); 6 | beforeEach(() => { 7 | let callCount = 0; 8 | spyOn(window, 'Date').and.callFake(function () { 9 | return { 10 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 11 | }; 12 | }); 13 | spy.mockClear(); 14 | }); 15 | it('Math.random 함수가 최소값을 반환할 경우 경우 테스트', () => { 16 | global.Math.random = jest.fn().mockReturnValue(0); 17 | expect(program()).toBe(1346269); 18 | expect(console.log).toBeCalledTimes(1); 19 | expect(console.log).toHaveBeenCalledWith(`Result: 1346269, Elapsed: 10`); 20 | }); 21 | it('Math.random 함수가 최대값을 반환할 경우 경우 테스트', () => { 22 | global.Math.random = jest.fn().mockReturnValue(0.99); 23 | expect(program()).toBe(14930352); 24 | expect(console.log).toBeCalledTimes(1); 25 | expect(console.log).toHaveBeenCalledWith(`Result: 14930352, Elapsed: 10`); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/test/time.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'fp-ts/lib/IO'; 2 | import { time } from '../time'; 3 | 4 | describe('IO를 받아 실행시간을 측정하는 유연하게 개선한 time 함수 테스트', () => { 5 | const baseTime = new Date().getTime(); 6 | beforeEach(() => { 7 | let callCount = 0; 8 | spyOn(window, 'Date').and.callFake(function () { 9 | return { 10 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 11 | }; 12 | }); 13 | }); 14 | it('타입 매개변수 A가 number인 경우 테스트', () => { 15 | expect(time(of(1))()).toMatchObject([1, 10]); 16 | }); 17 | it('타입 매개변수 A가 string인 경우 테스트', () => { 18 | expect(time(of('test'))()).toMatchObject(['test', 10]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/test/withLogging.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'fp-ts/lib/IO'; 2 | import { withLogging } from '../withLogging'; 3 | 4 | describe('time 함수를 조합한 withLogging 함수 테스트', () => { 5 | const spy = jest.spyOn(console, 'log').mockImplementation(); 6 | const baseTime = new Date().getTime(); 7 | beforeEach(() => { 8 | let callCount = 0; 9 | spyOn(window, 'Date').and.callFake(function () { 10 | return { 11 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 12 | }; 13 | }); 14 | spy.mockClear(); 15 | }); 16 | it('타입 매개변수 A가 number인 경우 테스트', () => { 17 | expect(withLogging(of(1))()).toBe(1); 18 | expect(console.log).toBeCalledTimes(1); 19 | expect(console.log).toHaveBeenCalledWith(`Result: 1, Elapsed: 10`); 20 | }); 21 | it('타입 매개변수 A가 string인 경우 테스트', () => { 22 | expect(withLogging(of('test'))()).toBe('test'); 23 | expect(console.log).toBeCalledTimes(1); 24 | expect(console.log).toHaveBeenCalledWith(`Result: test, Elapsed: 10`); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/time.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { now } from 'fp-ts/lib/Date'; 3 | import { Monad } from 'fp-ts/lib/IO'; 4 | 5 | export function time(ma: IO): IO<[A, number]> { 6 | return Monad.chain(now, (start) => 7 | Monad.chain(ma, (a) => Monad.map(now, (end) => [a, end - start])), 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/functional_design_series/2_combinator_two/withLogging.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { Monad } from 'fp-ts/lib/IO'; 3 | import { log } from 'fp-ts/lib/Console'; 4 | import { time } from './time'; 5 | 6 | export function withLogging(ma: IO): IO { 7 | return Monad.chain(time(ma), ([a, millis]) => 8 | Monad.map(log(`Result: ${a}, Elapsed: ${millis}`), () => a), 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/ioTime.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { now } from 'fp-ts/lib/Date'; 3 | import { Monad } from 'fp-ts/lib/IO'; 4 | 5 | export function time(ma: IO): IO<[A, number]> { 6 | return Monad.chain(now, (start) => 7 | Monad.chain(ma, (a) => Monad.map(now, (end) => [a, end - start])), 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/monadIO.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import type { Monad1 } from 'fp-ts/lib/Monad'; 3 | import type { Kind, URIS } from 'fp-ts/lib/HKT'; 4 | 5 | export interface MonadIO extends Monad1 { 6 | readonly fromIO: (fa: IO) => Kind; 7 | } 8 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/monadIOIO.ts: -------------------------------------------------------------------------------- 1 | import type { URI } from 'fp-ts/lib/IO'; 2 | import type { MonadIO } from './monadIO'; 3 | import { Monad } from 'fp-ts/lib/IO'; 4 | import { identity } from 'fp-ts/lib/function'; 5 | 6 | export const monadIOIO: MonadIO = { 7 | ...Monad, 8 | fromIO: identity, 9 | }; 10 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/monadIOTask.ts: -------------------------------------------------------------------------------- 1 | import type { URI } from 'fp-ts/lib/Task'; 2 | import type { MonadIO } from './monadIO'; 3 | import { Monad, fromIO } from 'fp-ts/lib/Task'; 4 | 5 | export const monadIOTask: MonadIO = { 6 | ...Monad, 7 | fromIO: fromIO, 8 | }; 9 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/monadIOTime.ts: -------------------------------------------------------------------------------- 1 | import type { Kind, URIS } from 'fp-ts/lib/HKT'; 2 | import type { MonadIO } from './monadIO'; 3 | import * as D from 'fp-ts/lib/Date'; 4 | 5 | export function time( 6 | M: MonadIO, 7 | ): (ma: Kind) => Kind { 8 | const now = M.fromIO(D.now); // 들어올리기 9 | return (ma) => 10 | M.chain(now, (start) => 11 | M.chain(ma, (a) => M.map(now, (end) => [a, end - start])), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/taskTime.ts: -------------------------------------------------------------------------------- 1 | import type { Task } from 'fp-ts/lib/Task'; 2 | import { task as M, fromIO } from 'fp-ts/lib/Task'; 3 | import * as D from 'fp-ts/lib/Date'; 4 | 5 | export function time(ma: Task): Task<[A, number]> { 6 | const now = fromIO(D.now); 7 | return M.chain(now, (start) => 8 | M.chain(ma, (a) => M.map(now, (end) => [a, end - start])), 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/test/ioTime.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'fp-ts/lib/IO'; 2 | import { time } from '../ioTime'; 3 | 4 | describe('IO를 받아 실행시간을 측정하는 time 함수 테스트', () => { 5 | const baseTime = new Date().getTime(); 6 | beforeEach(() => { 7 | let callCount = 0; 8 | spyOn(window, 'Date').and.callFake(function () { 9 | return { 10 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 11 | }; 12 | }); 13 | }); 14 | it('타입 매개변수 A가 number인 경우 테스트', () => { 15 | expect(time(of(1))()).toMatchObject([1, 10]); 16 | }); 17 | it('타입 매개변수 A가 string인 경우 테스트', () => { 18 | expect(time(of('test'))()).toMatchObject(['test', 10]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/test/monadIOIO.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'fp-ts/lib/IO'; 2 | import { monadIOIO } from '../monadIOIO'; 3 | 4 | describe('MonadIO를 구현한 MonadIOIO 인스턴스 테스트', () => { 5 | it('monadIOIO 인스턴스 of 함수 테스트', () => { 6 | const result = monadIOIO.of(1); 7 | expect(result()).toBe(1); 8 | }); 9 | it('monadIOIO 인스턴스 map 함수 테스트', () => { 10 | const result = monadIOIO.map(of(1), (n) => n * 2); 11 | expect(result()).toBe(2); 12 | }); 13 | it('monadIOIO 인스턴스 ap 함수 테스트', () => { 14 | const result = monadIOIO.ap( 15 | of((n: number) => n * 2), 16 | of(1), 17 | ); 18 | expect(result()).toBe(2); 19 | }); 20 | it('monadIOIO 인스턴스 chain 함수 테스트', () => { 21 | const result = monadIOIO.chain(of(1), (n) => of(n * 2)); 22 | expect(result()).toBe(2); 23 | }); 24 | it('monadIOIO 인스턴스 fromIO 함수 테스트', () => { 25 | const result = monadIOIO.fromIO(of(1)); 26 | expect(result()).toBe(1); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/test/monadIOTask.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'fp-ts/lib/Task'; 2 | import { monadIOTask } from '../monadIOTask'; 3 | 4 | describe('MonadIO를 구현한 MonadIOTask 인스턴스 테스트', () => { 5 | it('monadIOTask 인스턴스 of 함수 테스트', async () => { 6 | const result = monadIOTask.of(1); 7 | expect(await result()).toBe(1); 8 | }); 9 | it('monadIOTask 인스턴스 map 함수 테스트', async () => { 10 | const result = monadIOTask.map(of(1), (n) => n * 2); 11 | expect(await result()).toBe(2); 12 | }); 13 | it('monadIOTask 인스턴스 ap 함수 테스트', async () => { 14 | const result = monadIOTask.ap( 15 | of((n: number) => n * 2), 16 | of(1), 17 | ); 18 | expect(await result()).toBe(2); 19 | }); 20 | it('monadIOTask 인스턴스 chain 함수 테스트', async () => { 21 | const result = monadIOTask.chain(of(1), (n) => of(n * 2)); 22 | expect(await result()).toBe(2); 23 | }); 24 | it('monadIOTask 인스턴스 fromIO 함수 테스트', async () => { 25 | const result = monadIOTask.fromIO(of(1)); 26 | expect(await result()).toBe(1); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/test/monadIOTime.test.ts: -------------------------------------------------------------------------------- 1 | import { time } from '../monadIOTime'; 2 | import { MonadIO, of as ioOf } from 'fp-ts/lib/IO'; 3 | import { MonadTask, of as taskOf } from 'fp-ts/lib/Task'; 4 | 5 | describe('MonadIO를 받아 Kind를 받아 실행시간을 측정하는 time 함수 테스트', () => { 6 | const baseTime = new Date().getTime(); 7 | beforeEach(() => { 8 | let callCount = 0; 9 | spyOn(window, 'Date').and.callFake(function () { 10 | return { 11 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 12 | }; 13 | }); 14 | }); 15 | it('Kind가 Kind인 경우 테스트', () => { 16 | expect(time(MonadIO)(ioOf(1))()).toMatchObject([1, 10]); 17 | }); 18 | it('Kind가 Kind인 경우 테스트', async () => { 19 | expect(await time(MonadTask)(taskOf('test'))()).toMatchObject(['test', 10]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/test/taskTime.test.ts: -------------------------------------------------------------------------------- 1 | import { time } from '../taskTime'; 2 | import { of } from 'fp-ts/lib/Task'; 3 | 4 | describe('Task를 받아 실행시간을 측정하는 time 함수 테스트', () => { 5 | const baseTime = new Date().getTime(); 6 | beforeEach(() => { 7 | let callCount = 0; 8 | spyOn(window, 'Date').and.callFake(function () { 9 | return { 10 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 11 | }; 12 | }); 13 | }); 14 | it('타입 매개변수 A가 number인 경우 테스트', async () => { 15 | expect(await time(of(1))()).toMatchObject([1, 10]); 16 | }); 17 | it('타입 매개변수 A가 string인 경우 테스트', async () => { 18 | expect(await time(of('test'))()).toMatchObject(['test', 10]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/test/timeIO.test.ts: -------------------------------------------------------------------------------- 1 | import { timeIO } from '../timeIO'; 2 | import { of } from 'fp-ts/lib/IO'; 3 | 4 | describe('MonadIO를 사용해 IO를 받아 실행시간을 측정하는 time 함수 테스트', () => { 5 | const baseTime = new Date().getTime(); 6 | beforeEach(() => { 7 | let callCount = 0; 8 | spyOn(window, 'Date').and.callFake(function () { 9 | return { 10 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 11 | }; 12 | }); 13 | }); 14 | it('타입 매개변수 A가 number인 경우 테스트', () => { 15 | expect(timeIO(of(1))()).toMatchObject([1, 10]); 16 | }); 17 | it('타입 매개변수 A가 string인 경우 테스트', () => { 18 | expect(timeIO(of('test'))()).toMatchObject(['test', 10]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/test/timeTask.test.ts: -------------------------------------------------------------------------------- 1 | import { timeTask } from '../timeTask'; 2 | import { of } from 'fp-ts/lib/Task'; 3 | 4 | describe('MonadIO를 사용해 Task를 받아 실행시간을 측정하는 time 함수 테스트', () => { 5 | const baseTime = new Date().getTime(); 6 | beforeEach(() => { 7 | let callCount = 0; 8 | spyOn(window, 'Date').and.callFake(function () { 9 | return { 10 | getTime: () => (callCount++ % 2 ? baseTime + 10 : baseTime), 11 | }; 12 | }); 13 | }); 14 | it('타입 매개변수 A가 number인 경우 테스트', async () => { 15 | expect(await timeTask(of(1))()).toMatchObject([1, 10]); 16 | }); 17 | it('타입 매개변수 A가 string인 경우 테스트', async () => { 18 | expect(await timeTask(of('test'))()).toMatchObject(['test', 10]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/timeIO.ts: -------------------------------------------------------------------------------- 1 | import { time } from './monadIOTime'; 2 | import { monadIOIO } from './monadIOIO'; 3 | 4 | // timeIO: (ma: IO) => IO<[A, number]> 5 | export const timeIO = time(monadIOIO); 6 | -------------------------------------------------------------------------------- /src/functional_design_series/3_tagless_final/timeTask.ts: -------------------------------------------------------------------------------- 1 | import { time } from './monadIOTime'; 2 | import { monadIOTask } from './monadIOTask'; 3 | 4 | // timeTask: (ma: Task) => Task<[A, number]> 5 | export const timeTask = time(monadIOTask); 6 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/int.ts: -------------------------------------------------------------------------------- 1 | export interface IntBrand { 2 | readonly Int: unique symbol; 3 | } 4 | 5 | export type Int = number & IntBrand; 6 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/isInt.ts: -------------------------------------------------------------------------------- 1 | import type { Int } from './int'; 2 | 3 | export function isInt(n: number): n is Int { 4 | return Number.isInteger(n) && n >= 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/isNonEmptyString.ts: -------------------------------------------------------------------------------- 1 | import type { NonEmptyString } from './nonEmptyString'; 2 | 3 | // 사용자 지정 타입 가드로 구현된 런타임 검사 4 | export function isNonEmptyString(s: string): s is NonEmptyString { 5 | return s.length > 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/makeInt.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import type { Int } from './int'; 3 | import { none, some } from 'fp-ts/lib/Option'; 4 | import { isInt } from './isInt'; 5 | 6 | export function makeInt(n: number): Option { 7 | return isInt(n) ? some(n) : none; 8 | } 9 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/makeNonEmptyString.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import type { NonEmptyString } from './nonEmptyString'; 3 | import { none, some } from 'fp-ts/lib/Option'; 4 | import { isNonEmptyString } from './isNonEmptyString'; 5 | 6 | export function makeNonEmptyString(s: string): Option { 7 | return isNonEmptyString(s) ? some(s) : none; 8 | } 9 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/nonEmptyString.ts: -------------------------------------------------------------------------------- 1 | export interface NonEmptyStringBrand { 2 | readonly NonEmptyString: unique symbol; // 모듈/패키지에서의 고유함을 보장 3 | } 4 | 5 | export type NonEmptyString = string & NonEmptyStringBrand; 6 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/person.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import type { NonEmptyString } from './nonEmptyString'; 3 | import type { Int } from './int'; 4 | import { makeNonEmptyString } from './makeNonEmptyString'; 5 | import { makeInt } from './makeInt'; 6 | 7 | export interface Person { 8 | name: Option; 9 | age: Option; 10 | } 11 | 12 | export function person(name: string, age: number): Person { 13 | return { name: makeNonEmptyString(name), age: makeInt(age) }; 14 | } 15 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/test/isInt.test.ts: -------------------------------------------------------------------------------- 1 | import { isInt } from '../isInt'; 2 | 3 | describe('사용자 정의 Int 타입 타입 가드 isInt 함수 테스트', () => { 4 | it('인자로 전달된 값이 number 이면서 정수인 경우', () => { 5 | expect(isInt(1)).toBeTruthy(); 6 | }); 7 | it('인자로 전달된 값이 number 이면서 실수인 경우', () => { 8 | expect(isInt(0.1)).toBeFalsy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/test/isNonEmptyString.test.ts: -------------------------------------------------------------------------------- 1 | import { isNonEmptyString } from '../isNonEmptyString'; 2 | 3 | describe('사용자 정의 NonEmptyString 타입 타입 가드 isNonEmptyString 함수 테스트', () => { 4 | it('인자로 전달된 값이 빈 문자열인 경우', () => { 5 | expect(isNonEmptyString('')).toBeFalsy(); 6 | }); 7 | it('인자로 전달된 값이 빈 문자열이 아닌 경우', () => { 8 | expect(isNonEmptyString('success')).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/test/makeInt.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import type { Int } from '../int'; 3 | import { isSome, isNone, some, none } from 'fp-ts/lib/Option'; 4 | import { makeInt } from '../makeInt'; 5 | 6 | describe('isInt를 이용해 Option를 반환하는 makeInt 함수 테스트', () => { 7 | let result: Option; 8 | it('isInt가 true를 반환할 경우', () => { 9 | result = makeInt(1); 10 | expect(isSome(result)).toBeTruthy(); 11 | expect(result).toMatchObject(some(1)); 12 | }); 13 | it('isInt가 false를 반환할 경우', () => { 14 | result = makeInt(0.1); 15 | expect(isNone(result)).toBeTruthy(); 16 | expect(result).toMatchObject(none); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/test/makeNonEmptyString.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import type { NonEmptyString } from '../nonEmptyString'; 3 | import { isSome, isNone, some, none } from 'fp-ts/lib/Option'; 4 | import { makeNonEmptyString } from '../makeNonEmptyString'; 5 | 6 | describe('isNonEmptyString를 이용해 Option를 반환하는 makeNonEmptyString 함수 테스트', () => { 7 | let result: Option; 8 | it('isNonEmptyString이 true를 반환할 경우', () => { 9 | result = makeNonEmptyString('success'); 10 | expect(isSome(result)).toBeTruthy(); 11 | expect(result).toMatchObject(some('success')); 12 | }); 13 | it('isNonEmptyString이 false를 반환할 경우', () => { 14 | result = makeNonEmptyString(''); 15 | expect(isNone(result)).toBeTruthy(); 16 | expect(result).toMatchObject(none); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/functional_design_series/4_smart_constructors/test/person.test.ts: -------------------------------------------------------------------------------- 1 | import type { Person } from '../person'; 2 | import { isSome, isNone, some, none } from 'fp-ts/lib/Option'; 3 | import { person } from '../person'; 4 | 5 | describe('NonEmptyString과 Int를 필드로 갖는 Person 객체를 반환하는 person 함수 테스트', () => { 6 | let result: Person; 7 | it('name과 age 인자가 모두 유효할 경우', () => { 8 | result = person('minsu', 24); 9 | expect(isSome(result.name)).toBeTruthy(); 10 | expect(isSome(result.age)).toBeTruthy(); 11 | expect(result.name).toMatchObject(some('minsu')); 12 | expect(result.age).toMatchObject(some(24)); 13 | }); 14 | it('name만 유효할 경우', () => { 15 | result = person('minsu', 24.1); 16 | expect(isSome(result.name)).toBeTruthy(); 17 | expect(isNone(result.age)).toBeTruthy(); 18 | expect(result.name).toMatchObject(some('minsu')); 19 | expect(result.age).toMatchObject(none); 20 | }); 21 | it('age만 유효할 경우', () => { 22 | result = person('', 24); 23 | expect(isNone(result.name)).toBeTruthy(); 24 | expect(isSome(result.age)).toBeTruthy(); 25 | expect(result.name).toMatchObject(none); 26 | expect(result.age).toMatchObject(some(24)); 27 | }); 28 | it('name과 age 모두 유효하지 않을 경우', () => { 29 | result = person('', 24.1); 30 | expect(isNone(result.name)).toBeTruthy(); 31 | expect(isNone(result.age)).toBeTruthy(); 32 | expect(result.name).toMatchObject(none); 33 | expect(result.age).toMatchObject(none); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/functional_design_series/5_tdd_in_typescript/liftA2.ts: -------------------------------------------------------------------------------- 1 | export function liftA2( 2 | f: (a: A, b: B) => C, 3 | ): (fa: Promise, fb: Promise) => Promise { 4 | return (a, b) => a.then((aa) => b.then((bb) => f(aa, bb))); 5 | } 6 | -------------------------------------------------------------------------------- /src/functional_design_series/5_tdd_in_typescript/push.ts: -------------------------------------------------------------------------------- 1 | export function push(x: Array, y: T): Array { 2 | return x.concat([y]); 3 | } 4 | -------------------------------------------------------------------------------- /src/functional_design_series/5_tdd_in_typescript/pushPromise.ts: -------------------------------------------------------------------------------- 1 | import { push } from './push'; 2 | import { liftA2 } from './liftA2'; 3 | 4 | export function pushPromise( 5 | acc: Promise>, 6 | x: Promise, 7 | ): Promise> { 8 | return liftA2, T, Array>(push)(acc, x); 9 | } 10 | -------------------------------------------------------------------------------- /src/functional_design_series/5_tdd_in_typescript/sequence.ts: -------------------------------------------------------------------------------- 1 | import { pushPromise } from './pushPromise'; 2 | 3 | export function sequence(promises: Array>): Promise> { 4 | const init: Promise> = Promise.resolve([]); 5 | return promises.reduce(pushPromise, init); 6 | } 7 | -------------------------------------------------------------------------------- /src/functional_design_series/5_tdd_in_typescript/test/liftA2.test.ts: -------------------------------------------------------------------------------- 1 | import { liftA2 } from '../liftA2'; 2 | 3 | describe('(A, B) => C 함수를 받아 (Promise, Promise) => Promise 로 들어올리는 함수를 반환하는 liftA2 함수 테스트', () => { 4 | it('A: string, B: number, C: string인 경우 테스트', async () => { 5 | const f = (a: string, b: number): string => a + b.toString(); 6 | const liftA2WithFunction = liftA2(f); 7 | const fa = new Promise((resolve) => resolve('string')); 8 | const fb = new Promise((resolve) => resolve(1)); 9 | const result = await liftA2WithFunction(fa, fb); 10 | expect(result).toBe('string1'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/functional_design_series/5_tdd_in_typescript/test/push.test.ts: -------------------------------------------------------------------------------- 1 | import { push } from '../push'; 2 | 3 | describe('T 타입 배열에 값을 삽입하는 push 함수 테스트', () => { 4 | it('T타입이 string인 경우 테스트', () => { 5 | const x = ['a', 'b', 'c']; 6 | const y = 'd'; 7 | expect(push(x, y)).toMatchObject(['a', 'b', 'c', 'd']); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/functional_design_series/5_tdd_in_typescript/test/pushPromise.test.ts: -------------------------------------------------------------------------------- 1 | import { pushPromise } from '../pushPromise'; 2 | 3 | describe('Promise>에 Promise를 삽입하는 pushPromise 함수 테스트', () => { 4 | it('T 타입이 string인 경우 테스트', () => { 5 | const acc = new Promise>((resolve) => 6 | resolve(['a', 'b', 'c']), 7 | ); 8 | const x = new Promise((resolve) => resolve('d')); 9 | expect(pushPromise(acc, x)).toMatchObject( 10 | new Promise((resolve) => resolve(['a', 'b', 'c', 'd'])), 11 | ); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/functional_design_series/5_tdd_in_typescript/test/sequence.test.ts: -------------------------------------------------------------------------------- 1 | import { sequence } from '../sequence'; 2 | 3 | describe('Promise 배열을 resolve해 Promise>를 만드는 sequence 함수 테스트', () => { 4 | it('T 타입이 string인 경우 테스트', async () => { 5 | const promises = [ 6 | new Promise((resolve) => resolve('a')), 7 | new Promise((resolve) => resolve('b')), 8 | new Promise((resolve) => resolve('c')), 9 | ]; 10 | const result = await sequence(promises); 11 | expect(result).toMatchObject(['a', 'b', 'c']); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/M.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from 'fp-ts/lib/Monoid'; 2 | import { S } from './S'; 3 | 4 | export const M: Monoid = { 5 | ...S, 6 | empty: '', 7 | }; 8 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/S.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from 'fp-ts/lib/Semigroup'; 2 | 3 | export const S: Semigroup = { 4 | concat: (x, y) => x + ' ' + y, 5 | }; 6 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/associativity.ts: -------------------------------------------------------------------------------- 1 | import { S } from './S'; 2 | 3 | export const associativity = (x: string, y: string, z: string) => 4 | S.concat(S.concat(x, y), z) === S.concat(x, S.concat(y, z)); 5 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/leftIdentity.ts: -------------------------------------------------------------------------------- 1 | import { M } from './M'; 2 | 3 | export const leftIdentity = (x: string) => M.concat(M.empty, x) === x; 4 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/rightIdentity.ts: -------------------------------------------------------------------------------- 1 | import { M } from './M'; 2 | 3 | export const rightIdentity = (x: string) => M.concat(x, M.empty) === x; 4 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/test/M.test.ts: -------------------------------------------------------------------------------- 1 | import { M } from '../M'; 2 | 3 | describe('string 타입의 Monoid 인스턴스 테스트', () => { 4 | it('Monoid의 empty 필드 테스트', () => { 5 | expect(M.empty).toBe(''); 6 | }); 7 | it('Monoid의 concat 매서드 테스트', () => { 8 | expect(M.concat('Hello', 'World!')).toBe('Hello World!'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/test/S.test.ts: -------------------------------------------------------------------------------- 1 | import { S } from '../S'; 2 | 3 | describe('string 타입의 Semigroup 인스턴스 테스트', () => { 4 | it('Semigroup의 concat 매서드 테스트', () => { 5 | expect(S.concat('Hello', 'World!')).toBe('Hello World!'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/test/associativity.test.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { associativity } from '../associativity'; 3 | 4 | describe('fast-check을 이용한 속성 기반 테스트', () => { 5 | it('(x + y) + z === x + (y + z)인지 확인하는 associativity 함수 테스트', () => { 6 | const arb: fc.Arbitrary = fc.string(); 7 | fc.assert(fc.property(arb, arb, arb, associativity)); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/test/leftIdentity.test.ts: -------------------------------------------------------------------------------- 1 | import { leftIdentity } from '../leftIdentity'; 2 | 3 | describe('fast-check을 이용한 속성 기반 테스트', () => { 4 | it('concat(empty, x) == x인지 확인하는 leftIdentity 함수 테스트', () => { 5 | // fc.assert(fc.property(arb, leftIdentity)); 6 | // Counterexample: [""] 7 | // Got error: Property failed by returning false 8 | expect(leftIdentity('')).toBeFalsy(); 9 | // M.concat(empty, "") -> " " 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/functional_design_series/6_property_based_testing/test/rightIdentity.test.ts: -------------------------------------------------------------------------------- 1 | import { rightIdentity } from '../rightIdentity'; 2 | 3 | describe('fast-check을 이용한 속성 기반 테스트', () => { 4 | it('concat(x, empty) == x인지 확인하는 rightIdentity 함수 테스트', () => { 5 | // fc.assert(fc.property(arb, rightIdentity)); 6 | // Counterexample: [""] 7 | // Got error: Property failed by returning false 8 | expect(rightIdentity('')).toBeFalsy(); 9 | // M.concat("", empty) -> " " 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/either.ts: -------------------------------------------------------------------------------- 1 | export type Either = 2 | | { type: 'Left'; left: L } // 실패를 표현한다. 3 | | { type: 'Right'; right: A }; // 성공을 표현한다. 4 | 5 | export const left = (left: L): Either => ({ type: 'Left', left }); 6 | 7 | export const right = (right: A): Either => ({ 8 | type: 'Right', 9 | right, 10 | }); 11 | 12 | export const fold = ( 13 | fa: Either, 14 | onLeft: (left: L) => R, 15 | onRight: (right: A) => R, 16 | ): R => (fa.type === 'Left' ? onLeft(fa.left) : onRight(fa.right)); 17 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/head.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from './option'; 2 | import { some, none } from './option'; 3 | 4 | export const head = (as: Array): Option => { 5 | return as.length === 0 ? none : some(as[0]); 6 | }; 7 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/length.ts: -------------------------------------------------------------------------------- 1 | import type { List } from './list'; 2 | import { fold } from './list'; 3 | 4 | export const length = (fa: List): number => 5 | fold( 6 | fa, 7 | () => 0, 8 | (_, tail) => 1 + length(tail), 9 | ); 10 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/list.ts: -------------------------------------------------------------------------------- 1 | // ↓ 타입 인자 2 | export type List = 3 | | { type: 'Nil' } 4 | | { type: 'Cons'; head: A; tail: List }; 5 | // ↑ 재귀 6 | 7 | export const fold = ( 8 | fa: List, 9 | onNil: () => R, 10 | onCons: (head: A, tail: List) => R, 11 | ): R => (fa.type === 'Nil' ? onNil() : onCons(fa.head, fa.tail)); 12 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/option.ts: -------------------------------------------------------------------------------- 1 | export type Option = 2 | | { type: 'None' } 3 | | { 4 | type: 'Some'; 5 | value: A; 6 | }; 7 | 8 | export const none: Option = { type: 'None' }; 9 | 10 | export const some = (value: A): Option => ({ type: 'Some', value }); 11 | 12 | export const fold = ( 13 | fa: Option, 14 | onNone: () => R, 15 | onSome: (a: A) => R, 16 | ): R => (fa.type === 'None' ? onNone() : onSome(fa.value)); 17 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/productTypes.ts: -------------------------------------------------------------------------------- 1 | export type Hour = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; 2 | export type Period = 'AM' | 'PM'; 3 | export type Clock = [Hour, Period]; 4 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/readFileWithEither.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from './either'; 2 | import { fold, left, right } from './either'; 3 | 4 | const readFile = ( 5 | filePath: string, 6 | callback: (err: string, data: string) => string, 7 | ) => { 8 | let err: string | undefined, data: string | undefined; 9 | if (!filePath.includes('success.txt')) { 10 | err = `${filePath} is not found.`; 11 | } else { 12 | data = 'success'; 13 | } 14 | return callback(err, data); 15 | } 16 | 17 | export const readFileWithEither = (filePath: string) => 18 | readFile(filePath, (err, data) => { 19 | const either: Either = err 20 | ? left(err) 21 | : right(data.toString()); 22 | 23 | return fold( 24 | either, 25 | (err) => `Error: ${err}`, 26 | (data) => `Data: ${data}`, 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/sumTypes.ts: -------------------------------------------------------------------------------- 1 | export type Action = 2 | | { 3 | type: 'ADD_TODO'; 4 | text: string; 5 | } 6 | | { 7 | type: 'UPDATE_TODO'; 8 | id: number; 9 | text: string; 10 | completed: boolean; 11 | } 12 | | { 13 | type: 'DELETE_TODO'; 14 | id: number; 15 | }; 16 | 17 | export const add = (text: string): Action => ({ 18 | type: 'ADD_TODO', 19 | text, 20 | }); 21 | 22 | export const del = (id: number): Action => ({ 23 | type: 'DELETE_TODO', 24 | id, 25 | }); 26 | 27 | export const update = ( 28 | id: number, 29 | text: string, 30 | completed: boolean, 31 | ): Action => ({ 32 | type: 'UPDATE_TODO', 33 | id, 34 | text, 35 | completed, 36 | }); 37 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/test/either.test.ts: -------------------------------------------------------------------------------- 1 | import { left, right, fold } from '../either'; 2 | 3 | describe('Left 타입과 Right 타입으로 이루어진 Sum 타입인 Either 타입 테스트', () => { 4 | const onLeft = () => 'This is left.'; 5 | const onRight = (a: string) => a; 6 | it('Left 타입을 반환하는 left 함수 테스트', () => { 7 | const result = left(1); 8 | expect(result.type).toBe('Left'); 9 | expect(result).toMatchObject({ type: 'Left', left: 1 }); 10 | }); 11 | it('Right 타입을 반환하는 right 함수 테스트', () => { 12 | const result = right(1); 13 | expect(result.type).toBe('Right'); 14 | expect(result).toMatchObject({ type: 'Right', right: 1 }); 15 | }); 16 | it('Either 타입 값을 처리하는 fold 함수 테스트 (Left일 경우)', () => { 17 | const fa = left('some value'); 18 | const result = fold(fa, onLeft, onRight); 19 | expect(result).toBe('This is left.'); 20 | }); 21 | it('Either 타입 값을 처리하는 fold 함수 테스트 (Right일 경우)', () => { 22 | const fa = right('right'); 23 | const result = fold(fa, onLeft, onRight); 24 | expect(result).toBe('right'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/test/head.test.ts: -------------------------------------------------------------------------------- 1 | import { head } from '../head'; 2 | import { none, some } from '../option'; 3 | 4 | describe('배열의 맨 처음 값을 Option 타입으로 반환하는 head 함수 테스트', () => { 5 | const emptyArr = []; 6 | const nonEmptyArr = [1, 2, 3]; 7 | let result; 8 | it('배열이 비어있을 경우 테스트', () => { 9 | result = head(emptyArr); 10 | expect(result).toMatchObject(none); 11 | }); 12 | it('배열이 비어있지 않은 경우 테스트', () => { 13 | result = head(nonEmptyArr); 14 | expect(result).toMatchObject(some(1)); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/test/length.test.ts: -------------------------------------------------------------------------------- 1 | import type { List } from '../list'; 2 | import { length } from '../length'; 3 | 4 | describe('List의 길이를 반환하는 length 함수 테스트', () => { 5 | const emptyList: List = { type: 'Nil' }; 6 | const node2: List = { type: 'Cons', head: 2, tail: emptyList }; 7 | const node1: List = { type: 'Cons', head: 1, tail: node2 }; 8 | const nonEmptyList: List = { type: 'Cons', head: 0, tail: node1 }; 9 | let result: number; 10 | it('List가 비어있는 경우 테스트', () => { 11 | result = length(emptyList); 12 | expect(result).toBe(0); 13 | }); 14 | it('List가 비어있지 않은 경우 테스트', () => { 15 | result = length(nonEmptyList); 16 | expect(result).toBe(3); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/test/list.test.ts: -------------------------------------------------------------------------------- 1 | import type { List } from '../list'; 2 | import { fold } from '../list'; 3 | 4 | describe('List의 값을 처리하는 fold 함수 테스트', () => { 5 | const nillList: List = { type: 'Nil' }; 6 | const consList2: List = { type: 'Cons', head: 1, tail: nillList }; 7 | const consList1: List = { type: 'Cons', head: 2, tail: consList2 }; 8 | const onNil = () => 0; 9 | const onCons = (head: number, tail: List): number => 10 | head + fold(tail, onNil, onCons); 11 | let result: number; 12 | it('type이 Nil일때 List의 값을 처리하는 경우', () => { 13 | result = fold(nillList, onNil, onCons); 14 | expect(result).toBe(0); 15 | }); 16 | it('type이 Cons일떄 List의 값을 처리하는 경우', () => { 17 | result = fold(consList1, onNil, onCons); 18 | expect(result).toBe(3); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/test/option.test.ts: -------------------------------------------------------------------------------- 1 | import { none, some, fold } from '../option'; 2 | 3 | describe('None 타입과 Some 타입으로 이루어진 Sum 타입인 Option 타입 테스트', () => { 4 | const onNone = () => 'This is None.'; 5 | const onSome = (a: string) => a; 6 | it('Some 타입을 반환하는 some 함수 테스트', () => { 7 | const result = some(1); 8 | expect(result.type).toBe('Some'); 9 | expect(result).toMatchObject({ type: 'Some', value: 1 }); 10 | }); 11 | it('None 타입을 표현하는 none 값 테스트', () => { 12 | const result = none; 13 | expect(result.type).toBe('None'); 14 | }); 15 | it('Option 타입 값을 처리하는 fold 함수 테스트 (Some일 경우)', () => { 16 | const fa = some('some value'); 17 | const result = fold(fa, onNone, onSome); 18 | expect(result).toBe('some value'); 19 | }); 20 | it('Option 타입 값을 처리하는 fold 함수 테스트 (None일 경우)', () => { 21 | const fa = none; 22 | const result = fold(fa, onNone, onSome); 23 | expect(result).toBe('This is None.'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/test/readFileWithEither.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileWithEither } from '../readFileWithEither'; 2 | 3 | describe('Either 타입을 이용해 예외 처리를 하는 readFileWithEither 함수 테스트', () => { 4 | it('readFile 함수가 파일을 성공적으로 읽었을 경우', () => { 5 | const result = readFileWithEither('success.txt'); 6 | expect(result).toBe('Data: success') 7 | }); 8 | it('readFile 함수가 파일 읽기를 실패한 경우', () => { 9 | const result = readFileWithEither('fail.txt'); 10 | expect(result).toBe('Error: fail.txt is not found.') 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/functional_design_series/7_algebraic_data_types/test/sumTypes.test.ts: -------------------------------------------------------------------------------- 1 | import { add, del, update } from '../sumTypes'; 2 | 3 | describe('Redux 액션을 표현할 수 있는 Sum 타입 액션 테스트', () => { 4 | let result; 5 | it('Todo 리스트에 값을 추가하는 액션인 add 함수 테스트', () => { 6 | result = add('add data'); 7 | expect(result).toMatchObject({ type: 'ADD_TODO', text: 'add data' }); 8 | }); 9 | it('Todo 리스트에 값을 지우는 액션인 del 함수 테스트', () => { 10 | result = del(1); 11 | expect(result).toMatchObject({ type: 'DELETE_TODO', id: 1 }); 12 | }); 13 | it('Todo 리스트에 값을 업데이트하는 액션인 update 함수 테스트', () => { 14 | result = update(1, 'update data', true); 15 | expect(result).toMatchObject({ 16 | type: 'UPDATE_TODO', 17 | id: 1, 18 | completed: true, 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/__test__/elem.test.ts: -------------------------------------------------------------------------------- 1 | import { elem } from '../elem'; 2 | import { eqNumber } from '../eqNumber'; 3 | import { eqPoint } from '../eqPoint'; 4 | import { eqVector } from '../eqVector'; 5 | import { eqArrayOfPoints } from '../eqArrayOfPoints'; 6 | import { eqUser } from '../eqUser'; 7 | 8 | describe('elem 함수 테스트', () => { 9 | it('eqNumber를 이용한 elem 함수 테스트 (요소가 있는 경우)', () => { 10 | expect(elem(eqNumber)(1, [1, 2, 3])).toBeTruthy(); 11 | }); 12 | it('eqNumber를 이용한 elem 함수 테스트 (요소가 없는 경우)', () => { 13 | expect(elem(eqNumber)(4, [1, 2, 3])).toBeFalsy(); 14 | }); 15 | it('eqPoint를 이용한 elem 함수 테스트 (요소가 있는 경우)', () => { 16 | expect( 17 | elem(eqPoint)({ x: 1, y: 2 }, [ 18 | { x: 1, y: 2 }, 19 | { x: 2, y: 3 }, 20 | { x: 3, y: 4 }, 21 | ]), 22 | ).toBeTruthy(); 23 | }); 24 | it('eqPoint를 이용한 elem 함수 테스트 (요소가 없는 경우)', () => { 25 | expect( 26 | elem(eqPoint)({ x: 2, y: 1 }, [ 27 | { x: 1, y: 2 }, 28 | { x: 2, y: 3 }, 29 | { x: 3, y: 4 }, 30 | ]), 31 | ).toBeFalsy(); 32 | }); 33 | it('eqVector를 이용한 elem 함수 테스트 (요소가 있는 경우)', () => { 34 | expect( 35 | elem(eqVector)({ from: { x: 1, y: 2 }, to: { x: 1, y: 3 } }, [ 36 | { from: { x: 1, y: 2 }, to: { x: 1, y: 3 } }, 37 | { from: { x: 2, y: 3 }, to: { x: 1, y: 3 } }, 38 | { from: { x: 3, y: 4 }, to: { x: 1, y: 3 } }, 39 | ]), 40 | ).toBeTruthy(); 41 | }); 42 | it('eqVector를 이용한 elem 함수 테스트 (요소가 없는 경우)', () => { 43 | expect( 44 | elem(eqVector)({ from: { x: 2, y: 1 }, to: { x: 2, y: 2 } }, [ 45 | { from: { x: 1, y: 2 }, to: { x: 3, y: 5 } }, 46 | { from: { x: 2, y: 3 }, to: { x: 3, y: 5 } }, 47 | { from: { x: 3, y: 4 }, to: { x: 3, y: 5 } }, 48 | ]), 49 | ).toBeFalsy(); 50 | }); 51 | it('eqArrayOfPoints를 이용한 elem 함수 테스트 (요소가 있는 경우)', () => { 52 | expect( 53 | elem(eqArrayOfPoints)( 54 | [ 55 | { x: 1, y: 2 }, 56 | { x: 2, y: 3 }, 57 | ], 58 | [ 59 | [ 60 | { x: 1, y: 2 }, 61 | { x: 2, y: 3 }, 62 | ], 63 | [ 64 | { x: 2, y: 3 }, 65 | { x: 3, y: 4 }, 66 | ], 67 | ], 68 | ), 69 | ).toBeTruthy(); 70 | }); 71 | it('eqArrayOfPoints를 이용한 elem 함수 테스트 (요소가 없는 경우)', () => { 72 | expect( 73 | elem(eqArrayOfPoints)( 74 | [ 75 | { x: 0, y: 1 }, 76 | { x: 1, y: 2 }, 77 | ], 78 | [ 79 | [ 80 | { x: 1, y: 2 }, 81 | { x: 2, y: 3 }, 82 | ], 83 | [ 84 | { x: 2, y: 3 }, 85 | { x: 3, y: 4 }, 86 | ], 87 | ], 88 | ), 89 | ).toBeFalsy(); 90 | }); 91 | it('eqUser를 이용한 elem 함수 테스트 (요소가 있는 경우)', () => { 92 | expect( 93 | elem(eqUser)({ userId: 1, name: 'Minsu' }, [ 94 | { userId: 1, name: 'Minsu Kim' }, 95 | { userId: 2, name: 'Stevy' }, 96 | ]), 97 | ).toBeTruthy(); 98 | }); 99 | it('eqUser를 이용한 elem 함수 테스트 (요소가 없는 경우)', () => { 100 | expect( 101 | elem(eqUser)({ userId: 1, name: 'Minsu' }, [ 102 | { userId: 2, name: 'Minsu' }, 103 | { userId: 3, name: 'Stevy' }, 104 | ]), 105 | ).toBeFalsy(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/__test__/eqArrayOfPoints.test.ts: -------------------------------------------------------------------------------- 1 | import { eqArrayOfPoints } from '../eqArrayOfPoints'; 2 | 3 | describe('Eq 인터페이스를 구현한 eqArrayOfPoints 인스턴스 테스트', () => { 4 | it('eqArrayOfPoints 인스턴스 equals 함수 테스트', () => { 5 | expect( 6 | eqArrayOfPoints.equals( 7 | [ 8 | { x: 1, y: 2 }, 9 | { x: 2, y: 3 }, 10 | ], 11 | [ 12 | { x: 1, y: 2 }, 13 | { x: 2, y: 3 }, 14 | ], 15 | ), 16 | ).toBeTruthy(); 17 | expect( 18 | eqArrayOfPoints.equals( 19 | [ 20 | { x: 1, y: 2 }, 21 | { x: 2, y: 3 }, 22 | ], 23 | [ 24 | { x: 2, y: 3 }, 25 | { x: 3, y: 4 }, 26 | ], 27 | ), 28 | ).toBeFalsy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/__test__/eqNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { eqNumber } from '../eqNumber'; 2 | 3 | describe('Eq 인터페이스를 구현한 eqNumber 인스턴스 테스트', () => { 4 | it('eqNumber 인스턴스 equals 함수 테스트', () => { 5 | expect(eqNumber.equals(1, 1)).toBeTruthy(); 6 | expect(eqNumber.equals(1, 2)).toBeFalsy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/__test__/eqPoint.test.ts: -------------------------------------------------------------------------------- 1 | import { eqPoint } from '../eqPoint'; 2 | 3 | describe('Eq 인터페이스를 구현한 eqPoint 인스턴스 테스트', () => { 4 | it('eqPoint 인스턴스 equals 함수 테스트', () => { 5 | expect(eqPoint.equals({ x: 1, y: 2 }, { x: 1, y: 2 })).toBeTruthy(); 6 | expect(eqPoint.equals({ x: 1, y: 2 }, { x: 2, y: 1 })).toBeFalsy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/__test__/eqUser.test.ts: -------------------------------------------------------------------------------- 1 | import { eqUser } from '../eqUser'; 2 | 3 | describe('Eq 인터페이스를 구현한 eqUser 인스턴스 테스트', () => { 4 | it('eqUser 인스턴스 equals 함수 테스트', () => { 5 | expect( 6 | eqUser.equals( 7 | { userId: 1, name: 'Giulio' }, 8 | { userId: 1, name: 'Giulio Canti' }, 9 | ), 10 | ).toBeTruthy(); 11 | expect( 12 | eqUser.equals( 13 | { userId: 1, name: 'Giulio' }, 14 | { userId: 2, name: 'Giulio' }, 15 | ), 16 | ).toBeFalsy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/__test__/eqVector.test.ts: -------------------------------------------------------------------------------- 1 | import { eqVector } from '../eqVector'; 2 | 3 | describe('Eq 인터페이스를 구현한 eqVector 인스턴스 테스트', () => { 4 | it('eqVector 인스턴스 equals 함수 테스트', () => { 5 | expect( 6 | eqVector.equals( 7 | { from: { x: 1, y: 2 }, to: { x: 2, y: 3 } }, 8 | { from: { x: 1, y: 2 }, to: { x: 2, y: 3 } }, 9 | ), 10 | ).toBeTruthy(); 11 | expect( 12 | eqVector.equals( 13 | { from: { x: 1, y: 2 }, to: { x: 2, y: 3 } }, 14 | { from: { x: 2, y: 1 }, to: { x: 2, y: 3 } }, 15 | ), 16 | ).toBeFalsy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/elem.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from './eq'; 2 | 3 | export function elem(E: Eq): (a: A, as: Array) => boolean { 4 | return (a, as) => as.some((item) => E.equals(item, a)); 5 | } 6 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/eq.ts: -------------------------------------------------------------------------------- 1 | export interface Eq { 2 | /** `x`와 `y`가 같을 경우 `true`를 반환한다. */ 3 | readonly equals: (x: A, y: A) => boolean; 4 | } 5 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/eqArrayOfPoints.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from './eq'; 2 | import type { Point } from './eqPoint'; 3 | import { getEq } from 'fp-ts/lib/Array'; 4 | import { eqPoint } from './eqPoint'; 5 | 6 | export const eqArrayOfPoints: Eq> = getEq(eqPoint); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/eqNumber.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from './eq'; 2 | 3 | export const eqNumber: Eq = { 4 | equals: (x, y) => x === y, 5 | }; 6 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/eqPoint.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from './eq'; 2 | import { struct } from 'fp-ts/lib/Eq'; 3 | import { eqNumber } from './eqNumber'; 4 | 5 | export type Point = { 6 | x: number; 7 | y: number; 8 | }; 9 | 10 | // export const eqPoint: Eq = { 11 | // equals: (p1, p2) => p1.x === p2.x && p1.y === p2.y, 12 | // }; 13 | // const eqPoint: Eq = { 14 | // equals: (p1, p2) => p1 === p2 || (p1.x === p2.x && p1.y === p2.y), 15 | // }; 16 | 17 | export const eqPoint: Eq = struct({ 18 | x: eqNumber, 19 | y: eqNumber, 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/eqUser.ts: -------------------------------------------------------------------------------- 1 | import { contramap } from 'fp-ts/lib/Eq'; 2 | import { eqNumber } from './eqNumber'; 3 | 4 | type User = { 5 | userId: number; 6 | name: string; 7 | }; 8 | 9 | /** 두 User는`userId` 필드가 같으면 같습니다. */ 10 | export const eqUser = contramap((user: User) => user.userId)(eqNumber); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/1_eq/eqVector.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from './eq'; 2 | import type { Point } from './eqPoint'; 3 | import { struct } from 'fp-ts/lib/Eq'; 4 | import { eqPoint } from './eqPoint'; 5 | 6 | type Vector = { 7 | from: Point; 8 | to: Point; 9 | }; 10 | 11 | // const eqVector: Eq = getStructEq({ 12 | // from: eqPoint, 13 | // to: eqPoint, 14 | // }); 15 | 16 | export const eqVector: Eq = struct({ 17 | from: eqPoint, 18 | to: eqPoint, 19 | }); 20 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/__test__/find.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import * as N from 'fp-ts/lib/number'; 3 | import { isSome, none, some, getEq, isNone } from 'fp-ts/lib/Option'; 4 | import { find } from '../find'; 5 | 6 | describe('find함수 테스트 (undefined와 null)', () => { 7 | let array: number[] = [1, 2, 3, 5]; 8 | let result: Option; 9 | const E = getEq(N.Eq); 10 | it('find함수가 존재하는 값을 찾았을 경우', () => { 11 | result = find(array, (a) => a === 1); 12 | expect(isSome(result)).toBeTruthy(); 13 | expect(E.equals(result, some(1))).toBeTruthy(); 14 | }); 15 | it('find함수가 값을 찾지 못했을 경우', () => { 16 | result = find(array, (a) => a === 4); 17 | expect(isNone(result)).toBeTruthy(); 18 | expect(E.equals(result, none)).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/__test__/findIndex.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import * as N from 'fp-ts/lib/number'; 3 | import { isSome, none, some, getEq, isNone } from 'fp-ts/lib/Option'; 4 | import { findIndex } from '../findIndex'; 5 | 6 | describe('findIndex함수 테스트 (Sentinels)', () => { 7 | let array: number[] = [1, 2, 3, 5]; 8 | let result: Option; 9 | const E = getEq(N.Eq); 10 | it('findIndex함수가 존재하는 값을 찾았을 경우', () => { 11 | result = findIndex(array, (a) => a === 1); 12 | expect(isSome(result)).toBeTruthy(); 13 | expect(E.equals(result, some(0))).toBeTruthy(); 14 | }); 15 | it('findIndex함수가 값을 찾지 못했을 경우', () => { 16 | result = findIndex(array, (a) => a === 4); 17 | expect(isNone(result)).toBeTruthy(); 18 | expect(E.equals(result, none)).toBeTruthy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/__test__/get.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { getOrElse, isLeft, isRight } from 'fp-ts/lib/Either'; 3 | import { get } from '../get'; 4 | 5 | describe('get함수 테스트 (비동기 부수 효과)', () => { 6 | global.fetch = jest.fn((url) => { 7 | if (url === 'https://success.com') { 8 | return Promise.resolve({ 9 | text: () => Promise.resolve('success'), 10 | }) as Promise; 11 | } 12 | return Promise.reject('fail'); 13 | }); 14 | let result: Either; 15 | it('get함수가 정상적으로 값을 가져왔을 경우', async () => { 16 | result = await get('https://success.com')(); 17 | expect(isRight(result)).toBeTruthy(); 18 | expect(getOrElse(() => 'fail')(result)).toBe('success'); 19 | }); 20 | it('get함수 실행 중 예외가 발생했을 경우', async () => { 21 | result = await get('https://fail.com')(); 22 | expect(isLeft(result)).toBeTruthy(); 23 | expect(getOrElse(() => 'fail')(result)).toBe('fail'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/__test__/getItem.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import * as S from 'fp-ts/lib/string'; 3 | import { isSome, isNone, none, some, getEq } from 'fp-ts/lib/Option'; 4 | import { getItem } from '../getItem'; 5 | 6 | describe('getItem함수 테스트 (동기 부수 효과)', () => { 7 | window.localStorage.__proto__.getItem = jest.fn((key) => { 8 | if (key === 'success') return 'success'; 9 | return null; 10 | }); 11 | let result: Option; 12 | const E = getEq(S.Eq); 13 | 14 | it('getItem함수가 값을 정상적으로 가져온 경우', () => { 15 | result = getItem('success')(); 16 | expect(isSome(result)).toBeTruthy(); 17 | expect(E.equals(result, some('success'))).toBeTruthy(); 18 | }); 19 | 20 | it('getItem함수가 값을 가져오지 못했을 경우', () => { 21 | result = getItem('fail')(); 22 | expect(isNone(result)).toBeTruthy(); 23 | expect(E.equals(result, none)).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/__test__/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { isLeft, isRight, getOrElse } from 'fp-ts/lib/Either'; 2 | import { parse } from '../parse'; 3 | 4 | describe('parse함수 테스트 (예외)', () => { 5 | const success = '{"a": 1, "b": 2}'; 6 | const fail = '{"a": 1, "b"}'; 7 | let result; 8 | it('parse함수가 정상적으로 실행됐을 경우', () => { 9 | result = parse(success); 10 | expect(isRight(result)).toBeTruthy(); 11 | expect(getOrElse(() => ({ error: true }))(result)).toMatchObject({ 12 | a: 1, 13 | b: 2, 14 | }); 15 | }); 16 | it('parse함수 실행 중 예외가 발생했을 경우', () => { 17 | result = parse(fail); 18 | expect(isLeft(result)).toBeTruthy(); 19 | expect(getOrElse(() => ({ a: 1 }))(result)).toMatchObject({ 20 | a: 1, 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/__test__/random.test.ts: -------------------------------------------------------------------------------- 1 | import { random } from '../random'; 2 | 3 | describe('random함수 테스트 (랜덤)', () => { 4 | let result; 5 | it('Math.random함수를 mocking해 테스트하기', () => { 6 | Math.random = jest.fn().mockReturnValue(0.5); 7 | result = random(); 8 | expect(result).toBe(0.5); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/__test__/read.test.ts: -------------------------------------------------------------------------------- 1 | import { read } from '../read'; 2 | 3 | jest.mock('readline'); 4 | 5 | describe('read함수 테스트 (비동기 부수 효과)', () => { 6 | it('read가 정상적으로 값을 읽었을 경우', async () => { 7 | const result = await read(); 8 | expect(result).toBe('success'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/__test__/readFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import * as fs from 'fs'; 3 | import { isRight, getOrElse, isLeft } from 'fp-ts/lib/Either'; 4 | import { readFileSync } from '../readFileSync'; 5 | 6 | describe('readFileSync함수 테스트 (동기 부수 효과)', () => { 7 | jest.spyOn(fs, 'readFileSync').mockImplementation((path) => { 8 | if (path === 'success.txt') return 'success'; 9 | throw new Error(`${path} is not found.`); 10 | }); 11 | let result: Either; 12 | it('readFileSync가 정상적으로 값을 가져왔을 경우', () => { 13 | result = readFileSync('success.txt')(); 14 | expect(isRight(result)).toBeTruthy(); 15 | expect(getOrElse(() => 'fail')(result)).toBe('success'); 16 | }); 17 | it('readFileSync함수 실행 중 예외가 발생했을 경우', () => { 18 | result = readFileSync('fail.txt')(); 19 | expect(isLeft(result)).toBeTruthy(); 20 | expect(getOrElse(() => 'fail')(result)).toBe('fail'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/find.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { fromNullable } from 'fp-ts/lib/Option'; 3 | 4 | export function find(as: Array, predicate: (a: A) => boolean): Option { 5 | return fromNullable(as.find(predicate)); 6 | } 7 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/findIndex.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { none, some } from 'fp-ts/lib/Option'; 3 | 4 | export function findIndex( 5 | as: Array, 6 | predicate: (a: A) => boolean, 7 | ): Option { 8 | const index = as.findIndex(predicate); 9 | return index === -1 ? none : some(index); 10 | } 11 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/get.ts: -------------------------------------------------------------------------------- 1 | import type { TaskEither } from 'fp-ts/lib/TaskEither'; 2 | import { tryCatch } from 'fp-ts/lib/TaskEither'; 3 | 4 | export function get(url: string): TaskEither { 5 | return tryCatch( 6 | () => fetch(url).then((res) => res.text()), 7 | (reason) => new Error(String(reason)), 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/getItem.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import type { Option } from 'fp-ts/lib/Option'; 3 | import { fromNullable } from 'fp-ts/lib/Option'; 4 | 5 | export function getItem(key: string): IO> { 6 | return () => fromNullable(localStorage.getItem(key)); 7 | } 8 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/parse.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { tryCatch } from 'fp-ts/lib/Either'; 3 | 4 | export function parse(s: string): Either { 5 | return tryCatch( 6 | () => JSON.parse(s), 7 | (reason) => new Error(String(reason)), 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/random.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | 3 | export const random: IO = () => Math.random(); 4 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/read.ts: -------------------------------------------------------------------------------- 1 | import { createInterface } from 'readline'; 2 | import type { Task } from 'fp-ts/lib/Task'; 3 | 4 | export const read: Task = () => 5 | new Promise((resolve) => { 6 | const rl = createInterface({ 7 | input: process.stdin, 8 | output: process.stdout, 9 | }); 10 | rl.question('', (answer) => { 11 | rl.close(); 12 | resolve(answer); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/getting_started_series/0_interoperability/readFileSync.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import type { IOEither } from 'fp-ts/lib/IOEither'; 3 | import { tryCatch } from 'fp-ts/lib/IOEither'; 4 | 5 | export function readFileSync(path: string): IOEither { 6 | return tryCatch( 7 | () => fs.readFileSync(path, 'utf8'), 8 | (reason) => new Error(String(reason)), 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/IO.ts: -------------------------------------------------------------------------------- 1 | export interface IO { 2 | (): A; 3 | } 4 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/localStorage.test.ts: -------------------------------------------------------------------------------- 1 | import { isNone, isSome, none, some } from 'fp-ts/lib/Option'; 2 | import { getItem, setItem } from '../localStorage'; 3 | 4 | describe('실패하지 않는 연산을 표현하는 추상화 IO를 사용한 localStorage 메서드 테스트', () => { 5 | beforeEach(() => { 6 | jest.clearAllMocks(); 7 | const store = {}; 8 | jest 9 | .spyOn(Storage.prototype, 'getItem') 10 | .mockImplementation((key: string) => store[key]); 11 | jest 12 | .spyOn(Storage.prototype, 'setItem') 13 | .mockImplementation((key: string, value: string) => { 14 | store[key] = value; 15 | }); 16 | }); 17 | let result; 18 | it('localStorage에 값을 설정하는 setItem 함수 테스트', () => { 19 | setItem('key1', 'value1')(); 20 | expect(localStorage.setItem).toBeCalledTimes(1); 21 | expect(localStorage.setItem).toBeCalledWith('key1', 'value1'); 22 | }); 23 | it('localStorage에 값이 없을 경우 getItem 함수 테스트', () => { 24 | result = getItem('empty')(); 25 | expect(localStorage.getItem).toBeCalledTimes(1); 26 | expect(localStorage.getItem).toBeCalledWith('empty'); 27 | expect(isNone(result)).toBeTruthy(); 28 | expect(result).toMatchObject(none); 29 | }); 30 | it('localStorage에 값이 있을 경우 getItem 함수 테스트', () => { 31 | setItem('key1', 'value1')(); 32 | result = getItem('key1')(); 33 | expect(localStorage.getItem).toBeCalledTimes(1); 34 | expect(localStorage.getItem).toBeCalledWith('key1'); 35 | expect(isSome(result)).toBeTruthy(); 36 | expect(result).toMatchObject(some('value1')); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/log.test.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../log'; 2 | 3 | describe('실패하지 않는 연산을 표현하는 추상화 IO를 사용한 log 함수 테스트', () => { 4 | const spy = jest.spyOn(console, 'log').mockImplementation(); 5 | beforeEach(() => { 6 | spy.mockClear(); 7 | }); 8 | it('입력으로 전달된 값이 정상적으로 콘솔에 출력되는지 테스트', () => { 9 | log('Hello World!')(); 10 | expect(console.log).toBeCalledTimes(1); 11 | expect(console.log).toHaveBeenCalledWith('Hello World!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/now.test.ts: -------------------------------------------------------------------------------- 1 | import { now } from '../now'; 2 | 3 | describe('실패하지 않는 연산을 표현하는 추상화 IO를 사용한 now함수 테스트', () => { 4 | beforeEach(() => { 5 | jest 6 | .useFakeTimers('modern') 7 | .setSystemTime(new Date(2021, 6, 13, 0, 0, 0, 0)); 8 | }); 9 | afterEach(() => { 10 | jest.useRealTimers(); 11 | }); 12 | it('getTime 함수가 mocking된 값을 정상적으로 반환하는지 테스트', () => { 13 | expect(now()).toBe(1626134400000); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/program.test.ts: -------------------------------------------------------------------------------- 1 | import { program } from '../program'; 2 | 3 | describe('실패하지 않는 연산을 표현하는 추상화 IO를 사용한 program 함수 테스트', () => { 4 | const spy = jest.spyOn(console, 'log').mockImplementation(); 5 | beforeEach(() => { 6 | spy.mockClear(); 7 | }); 8 | it('random 함수의 반환값이 0.5로 mocking됐을 경우 테스트', () => { 9 | Math.random = jest.fn().mockReturnValue(0.5); 10 | program(); 11 | expect(console.log).toBeCalledTimes(1); 12 | expect(console.log).toHaveBeenCalledWith(false); 13 | }); 14 | it('random 함수의 반환값이 0.49로 mocking됐을 경우 테스트', () => { 15 | Math.random = jest.fn().mockReturnValue(0.49); 16 | program(); 17 | expect(console.log).toBeCalledTimes(1); 18 | expect(console.log).toHaveBeenCalledWith(true); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/random.test.ts: -------------------------------------------------------------------------------- 1 | import { random } from '../random'; 2 | 3 | describe('실패하지 않는 연산을 표현하는 추상화 IO를 사용한 random함수 테스트', () => { 4 | let result; 5 | it('Math.random 함수가 mocking된 값을 정상적으로 반환하는지 테스트', () => { 6 | global.Math.random = jest.fn().mockReturnValue(0.5); 7 | result = random(); 8 | expect(result).toBe(0.5); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/randomBool.test.ts: -------------------------------------------------------------------------------- 1 | import { randomBool } from '../randomBool'; 2 | 3 | describe('실패하지 않는 연산을 표현하는 추상화 IO를 사용한 randomBool함수 테스트', () => { 4 | let result; 5 | it('random 함수의 반환값이 0.5로 mocking됐을 경우 테스트', () => { 6 | global.Math.random = jest.fn().mockReturnValue(0.5); 7 | result = randomBool(); 8 | expect(result).toBeFalsy(); 9 | }); 10 | it('random 함수의 반환값이 0.49로 mocking됐을 경우 테스트', () => { 11 | global.Math.random = jest.fn().mockReturnValue(0.49); 12 | result = randomBool(); 13 | expect(result).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/randomFile.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import * as fs from 'fs'; 3 | import { isRight, getOrElse, isLeft } from 'fp-ts/lib/Either'; 4 | import { randomFile } from '../randomFile'; 5 | 6 | describe('임의의 파일을 읽는 randomFile 함수 테스트', () => { 7 | jest.spyOn(fs, 'readFileSync').mockImplementation((path) => { 8 | if (path.toString().endsWith('/1.txt')) return 'success'; 9 | throw new Error(`${path} is not found.`); 10 | }); 11 | let result: Either; 12 | it('randomInt 함수가 1을 반환해 파일을 성공적으로 읽었을 경우', () => { 13 | global.Math.random = jest.fn().mockReturnValue(0); 14 | result = randomFile(); 15 | expect(isRight(result)).toBeTruthy(); 16 | expect(getOrElse(() => 'fail')(result)).toBe('success'); 17 | }); 18 | it('randomInt 함수가 1을 반환하지 않아 파일 읽기를 실패한 경우', () => { 19 | global.Math.random = jest.fn().mockReturnValue(0.9); 20 | result = randomFile(); 21 | expect(isLeft(result)).toBeTruthy(); 22 | expect(getOrElse(() => 'fail')(result)).toBe('fail'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/readFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import * as fs from 'fs'; 3 | import { isRight, getOrElse, isLeft } from 'fp-ts/lib/Either'; 4 | import { readFileSync } from '../readFileSync'; 5 | 6 | describe('tryCatch를 이용해 IOEither를 반환하는 readFileSync함수 테스트', () => { 7 | jest.spyOn(fs, 'readFileSync').mockImplementation((path) => { 8 | if (path === 'success.txt') return 'success'; 9 | throw new Error(`${path} is not found.`); 10 | }); 11 | let result: Either; 12 | it('readFileSync가 정상적으로 값을 가져왔을 경우', () => { 13 | result = readFileSync('success.txt')(); 14 | expect(isRight(result)).toBeTruthy(); 15 | expect(getOrElse(() => 'fail')(result)).toBe('success'); 16 | }); 17 | it('readFileSync함수 실행 중 예외가 발생했을 경우', () => { 18 | result = readFileSync('fail.txt')(); 19 | expect(isLeft(result)).toBeTruthy(); 20 | expect(getOrElse(() => 'fail')(result)).toBe('fail'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/roll.test.ts: -------------------------------------------------------------------------------- 1 | import { log } from 'fp-ts/lib/Console'; 2 | import { chain } from 'fp-ts/lib/IO'; 3 | import { randomInt } from 'fp-ts/lib/Random'; 4 | import { roll } from '../roll'; 5 | import { withLogging } from '../withLogging'; 6 | 7 | describe('IO 추상화를 사용한 주사위의 합을 더하는 roll 함수 테스트', () => { 8 | const spy = jest.spyOn(console, 'log').mockImplementation(); 9 | beforeEach(() => { 10 | spy.mockClear(); 11 | }); 12 | it('random 함수를 mocking해 roll 함수가 정상적으로 주사위의 합을 반환하는지 테스트', () => { 13 | global.Math.random = jest.fn().mockReturnValue(0.99); 14 | const D4 = randomInt(1, 4); 15 | const D10 = randomInt(1, 10); 16 | const D20 = randomInt(1, 20); 17 | const dice = [D4, D10, D20]; 18 | expect(roll(dice)()).toBe(34); 19 | }); 20 | it('IO의 chain을 이용해 log 함수를 함께 이용하는 경우 테스트', () => { 21 | global.Math.random = jest.fn().mockReturnValue(0.99); 22 | const D4 = randomInt(1, 4); 23 | const D10 = randomInt(1, 10); 24 | const D20 = randomInt(1, 20); 25 | const dice = [D4, D10, D20]; 26 | chain((result) => log(`Result is: ${result}`))(roll(dice))(); 27 | expect(spy).toBeCalledTimes(1); 28 | expect(spy).toBeCalledWith('Result is: 34'); 29 | }); 30 | it('IO의 chain을 이용해 withLogging 함수를 함께 이용하는 경우 테스트', () => { 31 | global.Math.random = jest.fn().mockReturnValue(0.99); 32 | const D4 = randomInt(1, 4); 33 | const D10 = randomInt(1, 10); 34 | const D20 = randomInt(1, 20); 35 | const dice = [D4, D10, D20]; 36 | chain((result) => log(`Result is: ${result}`))( 37 | roll(dice.map(withLogging)), 38 | )(); 39 | expect(spy).toBeCalledTimes(4); 40 | expect(spy).toBeCalledWith('Value is: 4'); 41 | expect(spy).toBeCalledWith('Value is: 10'); 42 | expect(spy).toBeCalledWith('Value is: 20'); 43 | expect(spy).toBeCalledWith('Result is: 34'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/__test__/withLogging.test.ts: -------------------------------------------------------------------------------- 1 | import { log } from 'fp-ts/lib/Console'; 2 | import { chain } from 'fp-ts/lib/IO'; 3 | import { randomInt } from 'fp-ts/lib/Random'; 4 | import { roll } from '../roll'; 5 | import { withLogging } from '../withLogging'; 6 | 7 | describe('IO 추상화를 사용한 주사위의 합을 더하는 roll 함수 테스트', () => { 8 | const spy = jest.spyOn(console, 'log').mockImplementation(); 9 | beforeEach(() => { 10 | spy.mockClear(); 11 | }); 12 | it('IO의 chain을 이용해 withLogging 함수를 함께 이용하는 경우 테스트', () => { 13 | global.Math.random = jest.fn().mockReturnValue(0.99); 14 | const D4 = randomInt(1, 4); 15 | const D10 = randomInt(1, 10); 16 | const D20 = randomInt(1, 20); 17 | const dice = [D4, D10, D20]; 18 | chain((result) => log(`Result is: ${result}`))( 19 | roll(dice.map(withLogging)), 20 | )(); 21 | expect(spy).toBeCalledTimes(4); 22 | expect(spy).toBeCalledWith('Value is: 4'); 23 | expect(spy).toBeCalledWith('Value is: 10'); 24 | expect(spy).toBeCalledWith('Value is: 20'); 25 | expect(spy).toBeCalledWith('Result is: 34'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/localStorage.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import type { IO } from './IO'; 3 | import { fromNullable } from 'fp-ts/lib/Option'; 4 | 5 | export const getItem = (key: string): IO> => () => 6 | fromNullable(localStorage.getItem(key)); 7 | 8 | export const setItem = (key: string, value: string): IO => () => 9 | localStorage.setItem(key, value); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/log.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from './IO'; 2 | 3 | export const log = (s: unknown): IO => () => console.log(s); 4 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/now.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from './IO'; 2 | 3 | export const now: IO = () => new Date().getTime(); 4 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/program.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from './IO'; 2 | import { chain as ioChain } from 'fp-ts/lib/IO'; 3 | import { log } from './log'; 4 | import { randomBool } from './randomBool'; 5 | 6 | /** 무작위의 boolean을 콘솔에 출력한다. */ 7 | export const program: IO = ioChain(log)(randomBool); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/random.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from './IO'; 2 | 3 | export const random: IO = () => Math.random(); 4 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/randomBool.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from './IO'; 2 | import { map as ioMap } from 'fp-ts/lib/IO'; 3 | import { random } from './random'; 4 | 5 | /** 무작위의 boolean을 반환한다. */ 6 | export const randomBool: IO = ioMap((n) => n < 0.5)(random); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/randomFile.ts: -------------------------------------------------------------------------------- 1 | import { chain, rightIO } from 'fp-ts/lib/IOEither'; 2 | import { randomInt } from 'fp-ts/lib/Random'; 3 | import { readFileSync } from './readFileSync'; 4 | 5 | export const randomFile = chain((n) => readFileSync(`${__dirname}/${n}.txt`))( 6 | rightIO(randomInt(1, 3)), 7 | ); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/readFileSync.ts: -------------------------------------------------------------------------------- 1 | import type { IOEither } from 'fp-ts/lib/IOEither'; 2 | import { toError } from 'fp-ts/lib/Either'; 3 | import { tryCatch } from 'fp-ts/lib/IOEither'; 4 | import * as fs from 'fs'; 5 | 6 | export const readFileSync = (path: string): IOEither => 7 | tryCatch(() => fs.readFileSync(path, 'utf8'), toError); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/roll.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import type { Monoid } from 'fp-ts/lib/Monoid'; 3 | import { getApplicativeMonoid } from 'fp-ts/lib/Applicative'; 4 | import { Applicative } from 'fp-ts/lib/IO'; 5 | import { concatAll } from 'fp-ts/lib/Monoid'; 6 | import { MonoidSum } from 'fp-ts/lib/number'; 7 | 8 | type Die = IO; 9 | 10 | const monoidDie: Monoid = getApplicativeMonoid(Applicative)(MonoidSum); 11 | 12 | /** 주사위를 굴린 결과의 합을 반환합니다. */ 13 | export const roll: (dice: Array) => IO = concatAll(monoidDie); 14 | -------------------------------------------------------------------------------- /src/getting_started_series/10_io/withLogging.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from 'fp-ts/lib/IO'; 2 | import { chain, map } from 'fp-ts/lib/IO'; 3 | import { log } from 'fp-ts/lib/Console'; 4 | 5 | /** 디버깅을 위해 콘솔에 값을 기록한다. */ 6 | export const withLogging = (action: IO): IO => 7 | chain((a) => map(() => a)(log(`Value is: ${a}`)))(action); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/Dependencies.ts: -------------------------------------------------------------------------------- 1 | export interface Dependencies { 2 | i18n: { 3 | true: string; 4 | false: string; 5 | }; 6 | } 7 | 8 | export interface Dependencies2 extends Dependencies { 9 | lowerBound: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/Reader.ts: -------------------------------------------------------------------------------- 1 | export interface Reader { 2 | (r: R): A; 3 | } 4 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/__test__/askExample.test.ts: -------------------------------------------------------------------------------- 1 | import type { Dependencies2 } from '../Dependencies'; 2 | import { g, h } from '../askExample'; 3 | 4 | describe('ask와 chain 함수를 조합한 의존성이 있는 g 함수 테스트', () => { 5 | const deps: Dependencies2 = { 6 | i18n: { 7 | true: 'true', 8 | false: 'false', 9 | }, 10 | lowerBound: 2, 11 | }; 12 | it('number와 deps를 받아 문자열을 반환하는 g 함수 테스트', () => { 13 | expect(g(3)(deps)).toBe('true'); 14 | expect(g(2)(deps)).toBe('false'); 15 | expect(g(1)(deps)).toBe('false'); 16 | expect(g(3)({ ...deps, lowerBound: 3 })).toBe('false'); 17 | }); 18 | it('string을 받아 문자열을 반환하는 h 함수 테스트', () => { 19 | expect(h('aaa')(deps)).toBe('true'); 20 | expect(h('aa')(deps)).toBe('true'); 21 | expect(h('a')(deps)).toBe('false'); 22 | expect(h('aaa')({ ...deps, lowerBound: 4 })).toBe('false'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/__test__/compositionExample.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | compositionWithFlow, 3 | compositionWithPipe, 4 | } from '../compositionExample'; 5 | 6 | describe('flow를 이용해 조합한 함수와 map과 pipe를 이용해 조합한 함수 테스트', () => { 7 | it('flow를 이용해 함수를 조합한 compositionWithFlow 함수 테스트', () => { 8 | expect(compositionWithFlow('aa')).toBe(true); 9 | expect(compositionWithFlow('a')).toBe(false); 10 | }); 11 | it('map과 pipe를 이용해 함수를 조합한 compositionWithPipe 함수 테스트', () => { 12 | expect(compositionWithPipe('aa')).toBe(true); 13 | expect(compositionWithPipe('a')).toBe(false); 14 | }); 15 | it('compositionWithFlow와 compositionWithPipe가 같은 결과를 반환하는지 테스트', () => { 16 | expect(compositionWithPipe('aa')).toBe(compositionWithFlow('aa')); 17 | expect(compositionWithFlow('aa')).toBe(compositionWithPipe('aa')); 18 | expect(compositionWithPipe('a')).toBe(compositionWithFlow('a')); 19 | expect(compositionWithFlow('a')).toBe(compositionWithPipe('a')); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/__test__/depsExample.test.ts: -------------------------------------------------------------------------------- 1 | import type { Dependencies } from '../Dependencies'; 2 | import { f, g, h } from '../depsExample'; 3 | 4 | describe('Reader를 사용하지 않은 의존성이 있는 f, g, h 함수 테스트', () => { 5 | const deps: Dependencies = { 6 | i18n: { 7 | true: 'true', 8 | false: 'false', 9 | }, 10 | }; 11 | it('boolean와 deps를 받아 문자열을 반환하는 f 함수 테스트', () => { 12 | expect(f(true, deps)).toBe('true'); 13 | expect(f(false, deps)).toBe('false'); 14 | }); 15 | it('number와 deps를 받아 문자열을 반환하는 g 함수 테스트', () => { 16 | expect(g(3, deps)).toBe('true'); 17 | expect(g(2, deps)).toBe('false'); 18 | expect(g(1, deps)).toBe('false'); 19 | }); 20 | it('string과 deps를 받아 문자열을 반환하는 h 함수 테스트', () => { 21 | expect(h('aaa', deps)).toBe('true'); 22 | expect(h('aa', deps)).toBe('true'); 23 | expect(h('a', deps)).toBe('false'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/__test__/example.test.ts: -------------------------------------------------------------------------------- 1 | import { f, g, h } from '../example'; 2 | 3 | describe('Reader를 사용하지 않은 f, g, h 함수 테스트', () => { 4 | it('boolean을 받아 문자열을 반환하는 f 함수 테스트', () => { 5 | expect(f(true)).toBe('true'); 6 | expect(f(false)).toBe('false'); 7 | }); 8 | it('number를 받아 문자열을 반환하는 g 함수 테스트', () => { 9 | expect(g(3)).toBe('true'); 10 | expect(g(2)).toBe('false'); 11 | expect(g(1)).toBe('false'); 12 | }); 13 | it('string를 받아 문자열을 반환하는 h 함수 테스트', () => { 14 | expect(h('aaa')).toBe('true'); 15 | expect(h('aa')).toBe('true'); 16 | expect(h('a')).toBe('false'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/__test__/readerDepsExample.test.ts: -------------------------------------------------------------------------------- 1 | import type { Dependencies } from '../Dependencies'; 2 | import { f, g, h } from '../readerDepsExample'; 3 | 4 | describe('Reader를 사용하는 의존성이 있는 f, g, h 함수 테스트', () => { 5 | const deps: Dependencies = { 6 | i18n: { 7 | true: 'true', 8 | false: 'false', 9 | }, 10 | }; 11 | it('boolean을 받아 deps의존성을 받는아 문자열을 반환하는 함수를 반환하는 f 함수 테스트', () => { 12 | expect(f(true)(deps)).toBe('true'); 13 | expect(f(false)(deps)).toBe('false'); 14 | }); 15 | it('number를 받아 deps의존성을 받아 문자열을 반환하는 함수를 반환하는 g 함수 테스트', () => { 16 | expect(g(3)(deps)).toBe('true'); 17 | expect(g(2)(deps)).toBe('false'); 18 | expect(g(1)(deps)).toBe('false'); 19 | }); 20 | it('string을 받아 deps의존성을 받아 문자열을 반환하는 함수를 반환하는 h 함수 테스트', () => { 21 | expect(h('aaa')(deps)).toBe('true'); 22 | expect(h('aa')(deps)).toBe('true'); 23 | expect(h('a')(deps)).toBe('false'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/askExample.ts: -------------------------------------------------------------------------------- 1 | import type { Dependencies2 } from './Dependencies'; 2 | import type { Reader } from 'fp-ts/lib/Reader'; 3 | import { pipe } from 'fp-ts/lib/function'; 4 | import { ask, chain } from 'fp-ts/lib/Reader'; 5 | 6 | import { f } from './readerDepsExample'; 7 | 8 | export const g = (n: number): Reader => 9 | pipe( 10 | ask(), 11 | chain((deps) => f(n > deps.lowerBound)), 12 | ); 13 | 14 | export const h = (s: string): Reader => g(s.length + 1); 15 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/compositionExample.ts: -------------------------------------------------------------------------------- 1 | import { flow, pipe } from 'fp-ts/lib/function'; 2 | import { map } from 'fp-ts/lib/Reader'; 3 | 4 | const len = (s: string): number => s.length; 5 | const double = (n: number): number => n * 2; 6 | const gt2 = (n: number): boolean => n > 2; 7 | 8 | export const compositionWithFlow = flow(len, double, gt2); 9 | 10 | export const compositionWithPipe = pipe(len, map(double), map(gt2)); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/depsExample.ts: -------------------------------------------------------------------------------- 1 | import type { Dependencies } from './Dependencies'; 2 | 3 | export const f = (b: boolean, deps: Dependencies): string => 4 | b ? deps.i18n.true : deps.i18n.false; 5 | 6 | export const g = (n: number, deps: Dependencies): string => f(n > 2, deps); 7 | 8 | export const h = (s: string, deps: Dependencies): string => 9 | g(s.length + 1, deps); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/example.ts: -------------------------------------------------------------------------------- 1 | export const f = (b: boolean): string => (b ? 'true' : 'false'); 2 | 3 | export const g = (n: number): string => f(n > 2); 4 | 5 | export const h = (s: string): string => g(s.length + 1); 6 | -------------------------------------------------------------------------------- /src/getting_started_series/11_reader/readerDepsExample.ts: -------------------------------------------------------------------------------- 1 | import type { Reader } from 'fp-ts/lib/Reader'; 2 | import type { Dependencies } from './Dependencies'; 3 | 4 | export const f = (b: boolean): Reader => (deps) => 5 | b ? deps.i18n.true : deps.i18n.false; 6 | 7 | export const g = (n: number): Reader => f(n > 2); 8 | 9 | export const h = (s: string): Reader => g(s.length + 1); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/__test__/byAge.test.ts: -------------------------------------------------------------------------------- 1 | import { byAge } from '../byAge'; 2 | 3 | describe('Ord 인터페이스를 구현한 byAge 함수 테스트', () => { 4 | it('byAge 인스턴스 compare 함수 테스트 ', () => { 5 | expect( 6 | byAge.compare({ name: 'Minsu', age: 22 }, { name: 'Minsu', age: 22 }), 7 | ).toBe(0); 8 | expect( 9 | byAge.compare({ name: 'Minsu', age: 23 }, { name: 'Minsu', age: 22 }), 10 | ).toBe(1); 11 | expect( 12 | byAge.compare({ name: 'Minsu', age: 22 }, { name: 'Minsu', age: 23 }), 13 | ).toBe(-1); 14 | }); 15 | it('byAge 인스턴스 equals 함수 테스트 ', () => { 16 | expect( 17 | byAge.equals({ name: 'Minsu', age: 22 }, { name: 'Minsu', age: 22 }), 18 | ).toBeTruthy(); 19 | expect( 20 | byAge.equals({ name: 'Minsu', age: 23 }, { name: 'Minsu', age: 22 }), 21 | ).toBeFalsy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/__test__/getOlder.test.ts: -------------------------------------------------------------------------------- 1 | import { getOlder } from '../getOlder'; 2 | 3 | describe('byAge, min, reverse 함수를 사용하는 getOlder 함수 테스트', () => { 4 | it('getOlder 함수 테스트 ', () => { 5 | expect( 6 | getOlder({ name: 'Guido', age: 48 }, { name: 'Giulio', age: 45 }), 7 | ).toMatchObject({ name: 'Guido', age: 48 }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/__test__/getYounger.test.ts: -------------------------------------------------------------------------------- 1 | import { getYounger } from '../getYounger'; 2 | 3 | describe('byAge 함수와 min 함수를 사용하는 getYounger 함수 테스트', () => { 4 | it('getYounger 함수 테스트 ', () => { 5 | expect( 6 | getYounger({ name: 'Guido', age: 48 }, { name: 'Giulio', age: 45 }), 7 | ).toMatchObject({ name: 'Giulio', age: 45 }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/__test__/min.test.ts: -------------------------------------------------------------------------------- 1 | import { min } from '../min'; 2 | import { ordNumber } from '../ordNumber'; 3 | 4 | describe('Ord 인터페이스를 이용한 min 함수 테스트', () => { 5 | it('ordNumber를 이용한 min 함수 테스트 ', () => { 6 | expect(min(ordNumber)(2, 1)).toBe(1); 7 | expect(min(ordNumber)(2, 3)).toBe(2); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/__test__/ordNumber.test.ts: -------------------------------------------------------------------------------- 1 | import { ordNumber } from '../ordNumber'; 2 | 3 | describe('Ord 인터페이스를 구현한 ordNumber 인스턴스 테스트', () => { 4 | it('ordNumber 인스턴스 equals 함수 테스트', () => { 5 | expect(ordNumber.equals(1, 1)).toBeTruthy(); 6 | expect(ordNumber.equals(1, 2)).toBeFalsy(); 7 | }); 8 | it('ordNumber 인스턴스 compare 함수 테스트', () => { 9 | expect(ordNumber.compare(3, 3)).toBe(0); 10 | expect(ordNumber.compare(1, 2)).toBe(-1); 11 | expect(ordNumber.compare(2, 1)).toBe(1); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/byAge.ts: -------------------------------------------------------------------------------- 1 | import type { Ord } from 'fp-ts/lib/Ord'; 2 | import { contramap } from 'fp-ts/lib/Ord'; 3 | import { ordNumber } from './ordNumber'; 4 | 5 | type User = { 6 | name: string; 7 | age: number; 8 | }; 9 | 10 | export const byAge: Ord = contramap((user: User) => user.age)(ordNumber); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/getOlder.ts: -------------------------------------------------------------------------------- 1 | import type { Ord } from 'fp-ts/lib/Ord'; 2 | import { reverse } from 'fp-ts/lib/Ord'; 3 | import { byAge } from './byAge'; 4 | import { min } from './min'; 5 | 6 | function max(O: Ord): (x: A, y: A) => A { 7 | return min(reverse(O)); 8 | } 9 | 10 | export const getOlder = max(byAge); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/getYounger.ts: -------------------------------------------------------------------------------- 1 | import { min } from './min'; 2 | import { byAge } from './byAge'; 3 | 4 | export const getYounger = min(byAge); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/min.ts: -------------------------------------------------------------------------------- 1 | import type { Ord } from 'fp-ts/lib/Ord'; 2 | 3 | export function min(O: Ord): (x: A, y: A) => A { 4 | return (x, y) => (O.compare(x, y) === 1 ? y : x); 5 | } 6 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/ord.ts: -------------------------------------------------------------------------------- 1 | import type { Eq } from 'fp-ts/lib/Eq'; 2 | 3 | type Ordering = -1 | 0 | 1; 4 | 5 | export interface Ord extends Eq { 6 | readonly compare: (x: A, y: A) => Ordering; 7 | } 8 | -------------------------------------------------------------------------------- /src/getting_started_series/2_ord/ordNumber.ts: -------------------------------------------------------------------------------- 1 | import type { Ord } from 'fp-ts/lib/Ord'; 2 | import { fromCompare } from 'fp-ts/lib/Ord'; 3 | 4 | // export const ordNumber: Ord = { 5 | // equals: (x, y) => x === y, 6 | // compare: (x, y) => (x < y ? -1 : x > y ? 1 : 0), 7 | // }; 8 | 9 | export const ordNumber: Ord = fromCompare((x, y) => 10 | x < y ? -1 : x > y ? 1 : 0, 11 | ); 12 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/appliedSemigroup.test.ts: -------------------------------------------------------------------------------- 1 | import { isNone, isSome, none, some } from 'fp-ts/lib/Option'; 2 | import { appliedSemigroup } from '../appliedSemigroup'; 3 | 4 | describe('Option타입을 지원하는 appliedSemigroup 인스턴스 테스트', () => { 5 | let result; 6 | it('appliedSemigroup 테스트 (none + none)', () => { 7 | result = appliedSemigroup.concat(none, none); 8 | expect(result).toBe(none); 9 | expect(isNone(result)).toBeTruthy(); 10 | }); 11 | it('appliedSemigroup 테스트 (some + none)', () => { 12 | result = appliedSemigroup.concat(some(1), none); 13 | expect(result).toBe(none); 14 | expect(isNone(result)).toBeTruthy(); 15 | }); 16 | it('appliedSemigroup 테스트 (some + some)', () => { 17 | result = appliedSemigroup.concat(some(1), some(2)); 18 | expect(result).toMatchObject(some(3)); 19 | expect(isSome(result)).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/getArraySemigroup.test.ts: -------------------------------------------------------------------------------- 1 | import { getArraySemigroup } from '../getArraySemigroup'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 getArraySemigroup 인스턴스 테스트', () => { 4 | it('getArraySemigroup 인스턴스 concat 함수 테스트 (string 타입)', () => { 5 | expect( 6 | getArraySemigroup().concat(['Hello'], ['World!']), 7 | ).toMatchObject(['Hello', 'World!']); 8 | }); 9 | it('getArraySemigroup 인스턴스 concat 함수 테스트 (number 타입)', () => { 10 | expect(getArraySemigroup().concat([1, 2], [3, 4])).toMatchObject([ 11 | 1, 12 | 2, 13 | 3, 14 | 4, 15 | ]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/getFirstSemigroup.test.ts: -------------------------------------------------------------------------------- 1 | import { getFirstSemigroup } from '../getFirstSemigroup'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 getFirstSemigroup 인스턴스 테스트', () => { 4 | it('getFirstSemigroup 인스턴스 concat 함수 테스트', () => { 5 | expect(getFirstSemigroup().concat(1, 10)).toBe(1); 6 | expect(getFirstSemigroup().concat('a', 'b')).toBe('a'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/getLastSemigroup.test.ts: -------------------------------------------------------------------------------- 1 | import { getLastSemigroup } from '../getLastSemigroup'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 getLastSemigroup 인스턴스 테스트', () => { 4 | it('getLastSemigroup 인스턴스 concat 함수 테스트', () => { 5 | expect(getLastSemigroup().concat(1, 10)).toBe(10); 6 | expect(getLastSemigroup().concat('a', 'b')).toBe('b'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/isPositiveXY.test.ts: -------------------------------------------------------------------------------- 1 | import { isPositiveXY } from '../isPositiveXY'; 2 | 3 | describe('semigroupPredicate 인스턴스를 이용해 만든 isPositiveXY 테스트', () => { 4 | it('isPositiveXY 함수 테스트', () => { 5 | expect(isPositiveXY({ x: 1, y: 1 })).toBeTruthy(); 6 | expect(isPositiveXY({ x: 1, y: -1 })).toBeFalsy(); 7 | expect(isPositiveXY({ x: -1, y: 1 })).toBeFalsy(); 8 | expect(isPositiveXY({ x: -1, y: -1 })).toBeFalsy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/of.test.ts: -------------------------------------------------------------------------------- 1 | import { getArraySemigroup } from '../getArraySemigroup'; 2 | import { of } from '../of'; 3 | 4 | describe('Free Semigroup 인스턴스에 사용되는 of 함수 테스트', () => { 5 | it('string 타입의 Semigroup of 함수 테스트', () => { 6 | expect( 7 | getArraySemigroup().concat(of('Hello'), of('World!')), 8 | ).toMatchObject(['Hello', 'World!']); 9 | }); 10 | 11 | it('number 타입의 Semigroup of 함수 테스트', () => { 12 | expect(getArraySemigroup().concat(of(1), of(2))).toMatchObject([ 13 | 1, 14 | 2, 15 | ]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/product.test.ts: -------------------------------------------------------------------------------- 1 | import { product } from '../product'; 2 | 3 | describe('concatAll, SemigroupProduct를 사용한 product 함수 테스트', () => { 4 | it('product함수 테스트', () => { 5 | expect(product(1)([1, 2, 3, 4])).toBe(24); 6 | expect(product(10)([1, 2, 3, 4])).toBe(240); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupCustomer.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupCustomer } from '../semigroupCustomer'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupCustomer 인스턴스 테스트', () => { 4 | it('semigroupCustomer 인스턴스 concat 함수 테스트', () => { 5 | expect( 6 | semigroupCustomer.concat( 7 | { 8 | name: 'Giulio', 9 | favouriteThings: ['math', 'climbing'], 10 | registeredAt: new Date(2018, 1, 20).getTime(), 11 | lastUpdatedAt: new Date(2018, 2, 18).getTime(), 12 | hasMadePurchase: false, 13 | }, 14 | { 15 | name: 'Giulio Canti', 16 | favouriteThings: ['functional programming'], 17 | registeredAt: new Date(2018, 1, 22).getTime(), 18 | lastUpdatedAt: new Date(2018, 2, 9).getTime(), 19 | hasMadePurchase: true, 20 | }, 21 | ), 22 | ).toMatchObject({ 23 | name: 'Giulio Canti', 24 | favouriteThings: ['math', 'climbing', 'functional programming'], 25 | registeredAt: new Date(2018, 1, 20).getTime(), 26 | lastUpdatedAt: new Date(2018, 2, 18).getTime(), 27 | hasMadePurchase: true, 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupMax.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupMax } from '../semigroupMax'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupMax 인스턴스 테스트', () => { 4 | it('semigroupMax 인스턴스 concat 함수 테스트', () => { 5 | expect(semigroupMax.concat(2, 1)).toBe(2); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupMin.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupMin } from '../semigroupMin'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupMin 인스턴스 테스트', () => { 4 | it('semigroupMin 인스턴스 concat 함수 테스트', () => { 5 | expect(semigroupMin.concat(2, 1)).toBe(1); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupPoint.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupPoint } from '../semigroupPoint'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupPoint 인스턴스 테스트', () => { 4 | it('semigroupPoint 인스턴스 concat 함수 테스트', () => { 5 | expect( 6 | semigroupPoint.concat({ x: 1, y: 2 }, { x: 2, y: 3 }), 7 | ).toMatchObject({ x: 3, y: 5 }); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupPredicate.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupPredicate } from '../semigroupPredicate'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupPredicate 인스턴스 테스트', () => { 4 | it('semigroupPredicate 인스턴스 concat 함수 테스트', () => { 5 | expect( 6 | semigroupPredicate.concat( 7 | (p) => p.x === 1, 8 | (p) => p.y === 2, 9 | )({ x: 1, y: 2 }), 10 | ).toBeTruthy(); 11 | }); 12 | it('semigroupPredicate 인스턴스 concat 함수 테스트', () => { 13 | expect( 14 | semigroupPredicate.concat( 15 | (p) => p.x === 1, 16 | (p) => p.y !== 2, 17 | )({ x: 1, y: 2 }), 18 | ).toBeFalsy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupProduct.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupString } from '../semigroupString'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupString 인스턴스 테스트', () => { 4 | it('semigroupString 인스턴스 concat 함수 테스트', () => { 5 | expect(semigroupString.concat('Hello ', 'World!')).toBe('Hello World!'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupString.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupSum } from '../semigroupSum'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupSum 인스턴스 테스트', () => { 4 | it('semigroupSum 인스턴스 concat 함수 테스트', () => { 5 | expect(semigroupSum.concat(1, 10)).toBe(11); 6 | expect(semigroupSum.concat(2, 10)).toBe(12); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupSum.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupProduct } from '../semigroupProduct'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupProduct 인스턴스 테스트', () => { 4 | it('semigroupProduct 인스턴스 concat 함수 테스트', () => { 5 | expect(semigroupProduct.concat(1, 10)).toBe(10); 6 | expect(semigroupProduct.concat(2, 10)).toBe(20); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/semigroupVector.test.ts: -------------------------------------------------------------------------------- 1 | import { semigroupVector } from '../semigroupVector'; 2 | 3 | describe('Semigroup 인터페이스를 구현한 semigroupVector 인스턴스 테스트', () => { 4 | it('semigroupVector 인스턴스 concat 함수 테스트', () => { 5 | expect( 6 | semigroupVector.concat( 7 | { from: { x: 1, y: 2 }, to: { x: 2, y: 3 } }, 8 | { from: { x: 2, y: 3 }, to: { x: 3, y: 4 } }, 9 | ), 10 | ).toMatchObject({ from: { x: 3, y: 5 }, to: { x: 5, y: 7 } }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/__test__/sum.test.ts: -------------------------------------------------------------------------------- 1 | import { sum } from '../sum'; 2 | 3 | describe('concatAll, SemigroupSum를 사용한 sum 함수 테스트', () => { 4 | it('sum함수 테스트', () => { 5 | expect(sum(0)([1, 2, 3, 4])).toBe(10); 6 | expect(sum(10)([1, 2, 3, 4])).toBe(20); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/appliedSemigroup.ts: -------------------------------------------------------------------------------- 1 | import { Apply } from 'fp-ts/lib/Option'; 2 | import { SemigroupSum } from 'fp-ts/lib/number'; 3 | import { getApplySemigroup } from 'fp-ts/lib/Apply'; 4 | 5 | export const appliedSemigroup = getApplySemigroup(Apply)(SemigroupSum); 6 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/getArraySemigroup.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from './semigroup'; 2 | 3 | export function getArraySemigroup(): Semigroup> { 4 | return { concat: (x, y) => x.concat(y) }; 5 | } 6 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/getFirstSemigroup.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from './semigroup'; 2 | 3 | /** 항상 첫 번째 인자를 반환한다. */ 4 | export function getFirstSemigroup(): Semigroup { 5 | return { concat: (x, _) => x }; 6 | } 7 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/getLastSemigroup.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from './semigroup'; 2 | 3 | /** 항상 두 번째 인자를 반환한다. */ 4 | export function getLastSemigroup(): Semigroup { 5 | return { concat: (_, y) => y }; 6 | } 7 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/isPositiveXY.ts: -------------------------------------------------------------------------------- 1 | import type { Point } from './semigroupPoint'; 2 | import { semigroupPredicate } from './semigroupPredicate'; 3 | 4 | const isPositiveX = (p: Point): boolean => p.x >= 0; 5 | const isPositiveY = (p: Point): boolean => p.y >= 0; 6 | 7 | export const isPositiveXY = semigroupPredicate.concat(isPositiveX, isPositiveY); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/of.ts: -------------------------------------------------------------------------------- 1 | export function of(a: A): Array { 2 | return [a]; 3 | } 4 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/product.ts: -------------------------------------------------------------------------------- 1 | import { SemigroupProduct } from 'fp-ts/lib/number'; 2 | import { concatAll } from 'fp-ts/lib/Semigroup'; 3 | 4 | export const product = concatAll(SemigroupProduct); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroup.ts: -------------------------------------------------------------------------------- 1 | export interface Semigroup { 2 | concat: (x: A, y: A) => A; 3 | } 4 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupCustomer.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from 'fp-ts/lib/Semigroup'; 2 | import { struct, max, min } from 'fp-ts/lib/Semigroup'; 3 | import { getMonoid } from 'fp-ts/lib/Array'; 4 | import { Ord } from 'fp-ts/lib/number'; 5 | import { contramap } from 'fp-ts/lib/Ord'; 6 | import { SemigroupAny } from 'fp-ts/lib/boolean'; 7 | 8 | interface Customer { 9 | name: string; 10 | favouriteThings: Array; 11 | registeredAt: number; 12 | lastUpdatedAt: number; 13 | hasMadePurchase: boolean; 14 | } 15 | 16 | export const semigroupCustomer: Semigroup = struct({ 17 | // 더 긴 이름을 유지한다. 18 | name: max(contramap((s: string) => s.length)(Ord)), 19 | // 항목을 축적한다. 20 | // getMonoid는 Semigroup을 위한 `Array(), 22 | // 가장 이전의 날짜를 유지한다. 23 | registeredAt: min(Ord), 24 | // 가장 최근의 날짜를 유지한다. 25 | lastUpdatedAt: max(Ord), 26 | // 분리된 boolean Semigroup 27 | hasMadePurchase: SemigroupAny, 28 | }); 29 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupMax.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from 'fp-ts/lib/Semigroup'; 2 | import { Ord } from 'fp-ts/lib/number'; 3 | import { max } from 'fp-ts/lib/Semigroup'; 4 | 5 | /** 2개의 값 중 큰 값을 반환한다. */ 6 | export const semigroupMax: Semigroup = max(Ord); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupMin.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from 'fp-ts/lib/Semigroup'; 2 | import { Ord } from 'fp-ts/lib/number'; 3 | import { min } from 'fp-ts/lib/Semigroup'; 4 | 5 | /** 2개의 값 중 작은 값을 반환한다. */ 6 | export const semigroupMin: Semigroup = min(Ord); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupPoint.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from 'fp-ts/lib/Semigroup'; 2 | import { struct } from 'fp-ts/lib/Semigroup'; 3 | import { semigroupSum } from './semigroupSum'; 4 | 5 | export type Point = { 6 | x: number; 7 | y: number; 8 | }; 9 | 10 | // export const semigroupPoint: Semigroup = { 11 | // concat: (p1, p2) => ({ 12 | // x: semigroupSum.concat(p1.x, p2.x), 13 | // y: semigroupSum.concat(p1.y, p2.y), 14 | // }), 15 | // }; 16 | 17 | export const semigroupPoint: Semigroup = struct({ 18 | x: semigroupSum, 19 | y: semigroupSum, 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupPredicate.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from 'fp-ts/lib/Semigroup'; 2 | import type { Point } from './semigroupPoint'; 3 | import { getSemigroup } from 'fp-ts/lib/function'; 4 | import { SemigroupAll } from 'fp-ts/lib/boolean'; 5 | 6 | /** `SemigroupAll`은 결합 된 boolean Semigroup입니다. */ 7 | export const semigroupPredicate: Semigroup< 8 | (p: Point) => boolean 9 | > = getSemigroup(SemigroupAll)(); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupProduct.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from './semigroup'; 2 | 3 | /** number 타입의 곱셈 `Semigroup` */ 4 | export const semigroupProduct: Semigroup = { 5 | concat: (x, y) => x * y, 6 | }; 7 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupString.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from './semigroup'; 2 | 3 | export const semigroupString: Semigroup = { 4 | concat: (x, y) => x + y, 5 | }; 6 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupSum.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from './semigroup'; 2 | 3 | /** number 타입의 덧셈 `Semigroup` */ 4 | export const semigroupSum: Semigroup = { 5 | concat: (x, y) => x + y, 6 | }; 7 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/semigroupVector.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from 'fp-ts/lib/Semigroup'; 2 | import type { Point } from './semigroupPoint'; 3 | import { struct } from 'fp-ts/lib/Semigroup'; 4 | import { semigroupPoint } from './semigroupPoint'; 5 | 6 | type Vector = { 7 | from: Point; 8 | to: Point; 9 | }; 10 | 11 | export const semigroupVector: Semigroup = struct({ 12 | from: semigroupPoint, 13 | to: semigroupPoint, 14 | }); 15 | -------------------------------------------------------------------------------- /src/getting_started_series/3_semigroup/sum.ts: -------------------------------------------------------------------------------- 1 | import { SemigroupSum } from 'fp-ts/lib/number'; 2 | import { concatAll } from 'fp-ts/lib/Semigroup'; 3 | 4 | export const sum = concatAll(SemigroupSum); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/appliedMonoidSum.test.ts: -------------------------------------------------------------------------------- 1 | import { isNone, isSome, none, some } from 'fp-ts/lib/Option'; 2 | import { appliedMonoidSum } from '../appliedMonoidSum'; 3 | 4 | describe('Option타입을 지원하는 appliedMonoidSum 인스턴스 테스트', () => { 5 | let result; 6 | it('appliedMonoidSum 인스턴스 concat 함수 테스트 (none + none)', () => { 7 | result = appliedMonoidSum.concat(none, none); 8 | expect(result).toBe(none); 9 | expect(isNone(result)).toBeTruthy(); 10 | }); 11 | it('appliedMonoidSum 인스턴스 concat 함수 테스트 (some + none)', () => { 12 | result = appliedMonoidSum.concat(some(1), none); 13 | expect(result).toBe(none); 14 | expect(isNone(result)).toBeTruthy(); 15 | }); 16 | it('appliedMonoidSum 인스턴스 concat 함수 테스트 (some + some)', () => { 17 | result = appliedMonoidSum.concat(some(1), some(2)); 18 | expect(result).toMatchObject(some(3)); 19 | expect(isSome(result)).toBeTruthy(); 20 | }); 21 | it('appliedMonoidSum 인스턴스 concat 함수 테스트 (some + empty)', () => { 22 | result = appliedMonoidSum.concat(some(1), appliedMonoidSum.empty); 23 | expect(result).toMatchObject(some(1)); 24 | expect(isSome(result)).toBeTruthy(); 25 | }); 26 | it('appliedMonoidSum 인스턴스 concat 함수 테스트 (empty + empty)', () => { 27 | result = appliedMonoidSum.concat( 28 | appliedMonoidSum.empty, 29 | appliedMonoidSum.empty, 30 | ); 31 | expect(result).toMatchObject(appliedMonoidSum.empty); 32 | expect(isSome(result)).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/firstMonoid.test.ts: -------------------------------------------------------------------------------- 1 | import { isNone, isSome, none, some } from 'fp-ts/lib/Option'; 2 | import { firstMonoid } from '../firstMonoid'; 3 | 4 | describe('첫 번째 Some타입을 반환하는 firstMonoid 인스턴스 테스트', () => { 5 | let result; 6 | it('firstMonoid 인스턴스 concat 함수 테스트 (none + none)', () => { 7 | result = firstMonoid.concat(none, none); 8 | expect(result).toMatchObject(none); 9 | expect(isNone(result)).toBeTruthy(); 10 | }); 11 | it('firstMonoid 인스턴스 concat 함수 테스트 (some + none)', () => { 12 | result = firstMonoid.concat(some(1), none); 13 | expect(result).toMatchObject(some(1)); 14 | expect(isSome(result)).toBeTruthy(); 15 | }); 16 | it('firstMonoid 인스턴스 concat 함수 테스트 (none + some)', () => { 17 | result = firstMonoid.concat(none, some(1)); 18 | expect(result).toMatchObject(some(1)); 19 | expect(isSome(result)).toBeTruthy(); 20 | }); 21 | it('firstMonoid 인스턴스 concat 함수 테스트 (some + some)', () => { 22 | result = firstMonoid.concat(some(1), some(2)); 23 | expect(result).toMatchObject(some(1)); 24 | expect(isSome(result)).toBeTruthy(); 25 | }); 26 | it('firstMonoid 인스턴스 concat 함수 테스트 (some + empty)', () => { 27 | result = firstMonoid.concat(some(1), firstMonoid.empty); 28 | expect(result).toMatchObject(some(1)); 29 | expect(isSome(result)).toBeTruthy(); 30 | }); 31 | it('firstMonoid 인스턴스 concat 함수 테스트 (empty + some)', () => { 32 | result = firstMonoid.concat(firstMonoid.empty, some(1)); 33 | expect(result).toMatchObject(some(1)); 34 | expect(isSome(result)).toBeTruthy(); 35 | }); 36 | it('firstMonoid 인스턴스 concat 함수 테스트 (empty + empty)', () => { 37 | result = firstMonoid.concat(firstMonoid.empty, firstMonoid.empty); 38 | expect(result).toMatchObject(none); 39 | expect(isNone(result)).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/foldMonoidAll.test.ts: -------------------------------------------------------------------------------- 1 | import { foldMonoidAll } from '../foldMonoidAll'; 2 | 3 | describe('monoidAll 인스턴스에 concatAll 함수를 사용한 foldMonoidAll 함수 테스트', () => { 4 | it('foldMonoidAll 함수 테스트', () => { 5 | expect(foldMonoidAll([true, true, true])).toBeTruthy(); 6 | expect(foldMonoidAll([true, false, true])).toBeFalsy(); 7 | expect(foldMonoidAll([false, false, false])).toBeFalsy(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/foldMonoidAny.test.ts: -------------------------------------------------------------------------------- 1 | import { foldMonoidAny } from '../foldMonoidAny'; 2 | 3 | describe('monoidAny 인스턴스에 concatAll 함수를 사용한 foldMonoidAny 함수 테스트', () => { 4 | it('foldMonoidAny 함수 테스트', () => { 5 | expect(foldMonoidAny([true, true, true])).toBeTruthy(); 6 | expect(foldMonoidAny([true, true, false])).toBeTruthy(); 7 | expect(foldMonoidAny([true, false, false])).toBeTruthy(); 8 | expect(foldMonoidAny([false, false, false])).toBeFalsy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/foldMonoidProduct.test.ts: -------------------------------------------------------------------------------- 1 | import { foldMonoidProduct } from '../foldMonoidProduct'; 2 | 3 | describe('monoidProduct 인스턴스에 concatAll 함수를 사용한 foldMonoidProduct 함수 테스트', () => { 4 | it('foldMonoidProduct 함수 테스트', () => { 5 | expect(foldMonoidProduct([1, 2, 3, 4])).toBe(24); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/foldMonoidString.test.ts: -------------------------------------------------------------------------------- 1 | import { foldMonoidString } from '../foldMonoidString'; 2 | 3 | describe('monoidString 인스턴스에 concatAll 함수를 사용한 foldMonoidString 함수 테스트', () => { 4 | it('foldMonoidString 함수 테스트', () => { 5 | expect(foldMonoidString(['a', 'b', 'c'])).toBe('abc'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/foldMonoidSum.test.ts: -------------------------------------------------------------------------------- 1 | import { foldMonoidSum } from '../foldMonoidSum'; 2 | 3 | describe('monoidSum 인스턴스에 concatAll 함수를 사용한 foldMonoidSum 함수 테스트', () => { 4 | it('foldMonoidSum 함수 테스트', () => { 5 | expect(foldMonoidSum([1, 2, 3, 4])).toBe(10); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/lastMonoid.test.ts: -------------------------------------------------------------------------------- 1 | import { isNone, isSome, none, some } from 'fp-ts/lib/Option'; 2 | import { lastMonoid } from '../lastMonoid'; 3 | 4 | describe('두 번째 Some타입을 반환하는 lastMonoid 인스턴스 테스트', () => { 5 | let result; 6 | it('lastMonoid 인스턴스 concat 함수 테스트 (none + none)', () => { 7 | result = lastMonoid.concat(none, none); 8 | expect(result).toMatchObject(none); 9 | expect(isNone(result)).toBeTruthy(); 10 | }); 11 | it('lastMonoid 인스턴스 concat 함수 테스트 (some + none)', () => { 12 | result = lastMonoid.concat(some(1), none); 13 | expect(result).toMatchObject(some(1)); 14 | expect(isSome(result)).toBeTruthy(); 15 | }); 16 | it('lastMonoid 인스턴스 concat 함수 테스트 (none + some)', () => { 17 | result = lastMonoid.concat(none, some(1)); 18 | expect(result).toMatchObject(some(1)); 19 | expect(isSome(result)).toBeTruthy(); 20 | }); 21 | it('lastMonoid 인스턴스 concat 함수 테스트 (some + some)', () => { 22 | result = lastMonoid.concat(some(1), some(2)); 23 | expect(result).toMatchObject(some(2)); 24 | expect(isSome(result)).toBeTruthy(); 25 | }); 26 | it('lastMonoid 인스턴스 concat 함수 테스트 (some + empty)', () => { 27 | result = lastMonoid.concat(some(1), lastMonoid.empty); 28 | expect(result).toMatchObject(some(1)); 29 | expect(isSome(result)).toBeTruthy(); 30 | }); 31 | it('lastMonoid 인스턴스 concat 함수 테스트 (empty + some)', () => { 32 | result = lastMonoid.concat(lastMonoid.empty, some(1)); 33 | expect(result).toMatchObject(some(1)); 34 | expect(isSome(result)).toBeTruthy(); 35 | }); 36 | it('lastMonoid 인스턴스 concat 함수 테스트 (empty + empty)', () => { 37 | result = lastMonoid.concat(lastMonoid.empty, lastMonoid.empty); 38 | expect(result).toMatchObject(none); 39 | expect(isNone(result)).toBeTruthy(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/monoidAll.test.ts: -------------------------------------------------------------------------------- 1 | import { monoidAll } from '../monoidAll'; 2 | 3 | describe('Monoid 인터페이스를 구현한 monoidAll 인스턴스 테스트', () => { 4 | it('monoidAll 인스턴스 concat 함수 테스트', () => { 5 | expect(monoidAll.concat(false, false)).toBeFalsy(); 6 | expect(monoidAll.concat(true, false)).toBeFalsy(); 7 | expect(monoidAll.concat(true, true)).toBeTruthy(); 8 | }); 9 | 10 | it('monoidAll 인스턴스 empty 필드 테스트', () => { 11 | expect(monoidAll.empty).toBe(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/monoidAny.test.ts: -------------------------------------------------------------------------------- 1 | import { monoidAny } from '../monoidAny'; 2 | 3 | describe('Monoid 인터페이스를 구현한 monoidAny 인스턴스 테스트', () => { 4 | it('monoidAny 인스턴스 concat 함수 테스트', () => { 5 | expect(monoidAny.concat(false, false)).toBeFalsy(); 6 | expect(monoidAny.concat(true, false)).toBeTruthy(); 7 | expect(monoidAny.concat(true, true)).toBeTruthy(); 8 | }); 9 | 10 | it('monoidAny 인스턴스 empty 필드 테스트', () => { 11 | expect(monoidAny.empty).toBe(false); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/monoidPoint.test.ts: -------------------------------------------------------------------------------- 1 | import { monoidPoint } from '../monoidPoint'; 2 | 3 | describe('Monoid 인터페이스를 구현한 monoidPoint 인스턴스 테스트', () => { 4 | it('monoidPoint 인스턴스 concat 함수 테스트', () => { 5 | expect(monoidPoint.concat({ x: 1, y: 2 }, { x: 2, y: 3 })).toMatchObject({ 6 | x: 3, 7 | y: 5, 8 | }); 9 | }); 10 | 11 | it('monoidPoint 인스턴스 empty 필드 테스트', () => { 12 | expect(monoidPoint.empty).toMatchObject({ x: 0, y: 0 }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/monoidProduct.test.ts: -------------------------------------------------------------------------------- 1 | import { monoidProduct } from '../monoidProduct'; 2 | 3 | describe('Monoid 인터페이스를 구현한 monoidProduct 인스턴스 테스트', () => { 4 | it('monoidProduct 인스턴스 concat 함수 테스트', () => { 5 | expect(monoidProduct.concat(10, 20)).toBe(200); 6 | }); 7 | 8 | it('monoidProduct 인스턴스 empty 필드 테스트', () => { 9 | expect(monoidProduct.empty).toBe(1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/monoidSettings.test.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from '../monoidSettings'; 2 | import { isSome, some, none } from 'fp-ts/lib/Option'; 3 | import { monoidSettings } from '../monoidSettings'; 4 | 5 | describe('getLastMonoid를 사용한 monoidSettings 인스턴스 테스트', () => { 6 | const workspaceSettings: Settings = { 7 | fontFamily: some('Courier'), 8 | fontSize: none, 9 | maxColumn: some(80), 10 | }; 11 | const userSettings: Settings = { 12 | fontFamily: some('Fira Code'), 13 | fontSize: some(12), 14 | maxColumn: none, 15 | }; 16 | 17 | it('monoidSettings 인스턴스 concat 함수 테스트', () => { 18 | const result = monoidSettings.concat(workspaceSettings, userSettings); 19 | 20 | expect(isSome(result.fontFamily)).toBeTruthy(); 21 | expect(result.fontFamily).toMatchObject(some('Fira Code')); 22 | 23 | expect(isSome(result.fontSize)).toBeTruthy(); 24 | expect(result.fontSize).toMatchObject(some(12)); 25 | 26 | expect(isSome(result.maxColumn)).toBeTruthy(); 27 | expect(result.maxColumn).toMatchObject(some(80)); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/monoidString.test.ts: -------------------------------------------------------------------------------- 1 | import { monoidString } from '../monoidString'; 2 | 3 | describe('Monoid 인터페이스를 구현한 monoidString 인스턴스 테스트', () => { 4 | it('monoidString 인스턴스 concat 함수 테스트', () => { 5 | expect(monoidString.concat('Hello ', 'World!')).toBe('Hello World!'); 6 | }); 7 | 8 | it('monoidString 인스턴스 empty 필드 테스트', () => { 9 | expect(monoidString.empty).toBe(''); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/monoidSum.test.ts: -------------------------------------------------------------------------------- 1 | import { monoidSum } from '../monoidSum'; 2 | 3 | describe('Monoid 인터페이스를 구현한 monoidSum 인스턴스 테스트', () => { 4 | it('monoidSum 인스턴스 concat 함수 테스트', () => { 5 | expect(monoidSum.concat(1, 2)).toBe(3); 6 | }); 7 | 8 | it('monoidSum 인스턴스 empty 필드 테스트', () => { 9 | expect(monoidSum.empty).toBe(0); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/__test__/monoidVectore.test.ts: -------------------------------------------------------------------------------- 1 | import { monoidVector } from '../monoidVector'; 2 | 3 | describe('Monoid 인터페이스를 구현한 monoidVector 인스턴스 테스트', () => { 4 | it('monoidVector 인스턴스 concat 함수 테스트', () => { 5 | expect( 6 | monoidVector.concat( 7 | { from: { x: 1, y: 2 }, to: { x: 2, y: 3 } }, 8 | { from: { x: 2, y: 3 }, to: { x: 3, y: 4 } }, 9 | ), 10 | ).toMatchObject({ 11 | from: { 12 | x: 3, 13 | y: 5, 14 | }, 15 | to: { 16 | x: 5, 17 | y: 7, 18 | }, 19 | }); 20 | }); 21 | 22 | it('monoidVector 인스턴스 empty 필드 테스트', () => { 23 | expect(monoidVector.empty).toMatchObject({ 24 | from: { x: 0, y: 0 }, 25 | to: { x: 0, y: 0 }, 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/appliedMonoidSum.ts: -------------------------------------------------------------------------------- 1 | import { Applicative } from 'fp-ts/lib/Option'; 2 | import { getApplicativeMonoid } from 'fp-ts/lib/Applicative'; 3 | import { monoidSum } from './monoidSum'; 4 | 5 | export const appliedMonoidSum = getApplicativeMonoid(Applicative)(monoidSum); 6 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/firstMonoid.ts: -------------------------------------------------------------------------------- 1 | import { getFirstMonoid } from 'fp-ts/lib/Option'; 2 | 3 | export const firstMonoid = getFirstMonoid(); 4 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/foldMonoidAll.ts: -------------------------------------------------------------------------------- 1 | import { concatAll } from 'fp-ts/lib/Monoid'; 2 | import { monoidAll } from './monoidAll'; 3 | 4 | export const foldMonoidAll = concatAll(monoidAll); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/foldMonoidAny.ts: -------------------------------------------------------------------------------- 1 | import { concatAll } from 'fp-ts/lib/Monoid'; 2 | import { monoidAny } from './monoidAny'; 3 | 4 | export const foldMonoidAny = concatAll(monoidAny); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/foldMonoidProduct.ts: -------------------------------------------------------------------------------- 1 | import { concatAll } from 'fp-ts/lib/Monoid'; 2 | import { monoidProduct } from './monoidProduct'; 3 | 4 | export const foldMonoidProduct = concatAll(monoidProduct); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/foldMonoidString.ts: -------------------------------------------------------------------------------- 1 | import { concatAll } from 'fp-ts/lib/Monoid'; 2 | import { monoidString } from './monoidString'; 3 | 4 | export const foldMonoidString = concatAll(monoidString); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/foldMonoidSum.ts: -------------------------------------------------------------------------------- 1 | import { concatAll } from 'fp-ts/lib/Monoid'; 2 | import { monoidSum } from './monoidSum'; 3 | 4 | export const foldMonoidSum = concatAll(monoidSum); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/lastMonoid.ts: -------------------------------------------------------------------------------- 1 | import { getLastMonoid } from 'fp-ts/lib/Option'; 2 | 3 | export const lastMonoid = getLastMonoid(); 4 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoid.ts: -------------------------------------------------------------------------------- 1 | import type { Semigroup } from 'fp-ts/lib/Semigroup'; 2 | 3 | export interface Monoid extends Semigroup { 4 | readonly empty: A; 5 | } 6 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoidAll.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from './monoid'; 2 | 3 | /** boolean타입의 논리곱 monoid */ 4 | export const monoidAll: Monoid = { 5 | concat: (x, y) => x && y, 6 | empty: true, 7 | }; 8 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoidAny.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from './monoid'; 2 | 3 | /** boolean 타입의 논리합 `monoid` */ 4 | export const monoidAny: Monoid = { 5 | concat: (x, y) => x || y, 6 | empty: false, 7 | }; 8 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoidPoint.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from './monoid'; 2 | import { struct } from 'fp-ts/lib/Monoid'; 3 | import { monoidSum } from './monoidSum'; 4 | 5 | export type Point = { 6 | x: number; 7 | y: number; 8 | }; 9 | 10 | export const monoidPoint: Monoid = struct({ 11 | x: monoidSum, 12 | y: monoidSum, 13 | }); 14 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoidProduct.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from './monoid'; 2 | 3 | /** number 타입의 곱셈 `Monoid` */ 4 | export const monoidProduct: Monoid = { 5 | concat: (x, y) => x * y, 6 | empty: 1, 7 | }; 8 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoidSettings.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from 'fp-ts/lib/Monoid'; 2 | import type { Option } from 'fp-ts/lib/Option'; 3 | import { struct } from 'fp-ts/lib/Monoid'; 4 | import { getLastMonoid } from 'fp-ts/lib/Option'; 5 | 6 | /** VSCode 설정 */ 7 | export interface Settings { 8 | /** font family를 제어한다. */ 9 | fontFamily: Option; 10 | /** font size 픽셀을 제어한다. */ 11 | fontSize: Option; 12 | /** 특정 수의 열만 렌더링하도록 미니 맵의 너비를 제한합니다. */ 13 | maxColumn: Option; 14 | } 15 | 16 | export const monoidSettings: Monoid = struct({ 17 | fontFamily: getLastMonoid(), 18 | fontSize: getLastMonoid(), 19 | maxColumn: getLastMonoid(), 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoidString.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from './monoid'; 2 | 3 | export const monoidString: Monoid = { 4 | concat: (x, y) => x + y, 5 | empty: '', 6 | }; 7 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoidSum.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from './monoid'; 2 | 3 | /** number 타입의 덧셈 `Monoid` */ 4 | export const monoidSum: Monoid = { 5 | concat: (x, y) => x + y, 6 | empty: 0, 7 | }; 8 | -------------------------------------------------------------------------------- /src/getting_started_series/4_monoid/monoidVector.ts: -------------------------------------------------------------------------------- 1 | import type { Monoid } from './monoid'; 2 | import type { Point } from './monoidPoint'; 3 | import { struct } from 'fp-ts/lib/Monoid'; 4 | import { monoidPoint } from './monoidPoint'; 5 | 6 | type Vector = { 7 | from: Point; 8 | to: Point; 9 | }; 10 | 11 | export const monoidVector: Monoid = struct({ 12 | from: monoidPoint, 13 | to: monoidPoint, 14 | }); 15 | -------------------------------------------------------------------------------- /src/getting_started_series/5_category/__test__/compose.test.ts: -------------------------------------------------------------------------------- 1 | import { compose } from '../compose'; 2 | 3 | describe('Typescript에서 카테고리를 표현할 수 있는 간단한 compose 함수 테스트', () => { 4 | it('f = string => number, g = number => boolean, h = g ∘ f = string => boolean 조합 테스트', () => { 5 | const f = (s: string) => s.length; 6 | const g = (n: number) => n > 2; 7 | const h = compose(g, f); 8 | expect(h('bye')).toBeTruthy(); 9 | expect(h('hi')).toBeFalsy(); 10 | expect(h('')).toBeFalsy(); 11 | }); 12 | it('f = number => string, g = string => string, h = g ∘ f = number => string 조합 테스트', () => { 13 | const f = (n: number) => n.toString(); 14 | const g = (s: string) => s.repeat(Number(s)); 15 | const h = compose(g, f); 16 | expect(h(0)).toBe(''); 17 | expect(h(1)).toBe('1'); 18 | expect(h(2)).toBe('22'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/5_category/__test__/fgh.test.ts: -------------------------------------------------------------------------------- 1 | import { f, g, h } from '../fgh'; 2 | 3 | describe('프로그래밍 언어에서의 Category 예시 함수 테스트', () => { 4 | it('문자열의 길이를 반환하는 f 함수 테스트', () => { 5 | expect(f('bye')).toBe(3); 6 | expect(f('hi')).toBe(2); 7 | expect(f('')).toBe(0); 8 | }); 9 | it('숫자가 2보다 큰 경우 true를 반환하는 g 함수 테스트', () => { 10 | expect(g(3)).toBeTruthy(); 11 | expect(g(2)).toBeFalsy(); 12 | expect(g(-1)).toBeFalsy(); 13 | }); 14 | it('g 함수와 f 함수를 조합해 h = g ∘ f 등식을 만족하는 h 함수 테스트', () => { 15 | expect(h('bye')).toBeTruthy(); 16 | expect(h('hi')).toBeFalsy(); 17 | expect(h('')).toBeFalsy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/getting_started_series/5_category/compose.ts: -------------------------------------------------------------------------------- 1 | // B와 C의 타입이 같을 경우 `f: (a: A) => B`와 `g: (c: C) => D`를 조합할 수 있다. 2 | export function compose(g: (b: B) => C, f: (a: A) => B): (a: A) => C { 3 | return (a) => g(f(a)); 4 | } 5 | -------------------------------------------------------------------------------- /src/getting_started_series/5_category/fgh.ts: -------------------------------------------------------------------------------- 1 | export function f(s: string): number { 2 | return s.length; 3 | } 4 | 5 | export function g(n: number): boolean { 6 | return n > 2; 7 | } 8 | 9 | // h = g ∘ f 10 | export function h(s: string): boolean { 11 | return g(f(s)); 12 | } 13 | -------------------------------------------------------------------------------- /src/getting_started_series/6_functor/__test__/arrayLift.test.ts: -------------------------------------------------------------------------------- 1 | import { arrayLift } from '../arrayLift'; 2 | 3 | describe('(fb: F) => F에서 F가 Array인 Functor 테스트', () => { 4 | it('B = string, C = number인 경우 테스트', () => { 5 | const g = (s: string) => s.length; 6 | const arr = ['a', 'bb', 'ccc', 'dddd']; 7 | expect(arrayLift(g)(arr)).toMatchObject([1, 2, 3, 4]); 8 | }); 9 | it('B = boolean, C = string인 경우 테스트', () => { 10 | const g = (b: boolean) => (b ? 'success' : 'fail'); 11 | const arr = [true, true, false, false, true]; 12 | expect(arrayLift(g)(arr)).toMatchObject([ 13 | 'success', 14 | 'success', 15 | 'fail', 16 | 'fail', 17 | 'success', 18 | ]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/getting_started_series/6_functor/__test__/functorResponse.test.ts: -------------------------------------------------------------------------------- 1 | import { functorResponse } from '../functorResponse'; 2 | 3 | describe('Response를 위한 Functor 인스턴스 functorResponse 테스트', () => { 4 | it('functorResponse 인스턴스 URI 필드 테스트', () => { 5 | expect(functorResponse.URI).toBe('Response'); 6 | }); 7 | it('functorResponse 인스턴스 map 함수 (f: string) => boolean 테스트', () => { 8 | const f = (s: string): boolean => s === 'success'; 9 | const result = functorResponse.map( 10 | { 11 | url: 'https:127.0.0.1', 12 | status: 200, 13 | headers: { 'Content-Type': 'application/json' }, 14 | body: 'success', 15 | }, 16 | f, 17 | ); 18 | expect(result).toMatchObject({ 19 | url: 'https:127.0.0.1', 20 | status: 200, 21 | headers: { 'Content-Type': 'application/json' }, 22 | body: true, 23 | }); 24 | }); 25 | it('functorResponse 인스턴스 map 함수 (f: object & { text: string }) => string 테스트', () => { 26 | const f = (body: object & { text: string }): string => body.text; 27 | const result = functorResponse.map( 28 | { 29 | url: 'https:127.0.0.1', 30 | status: 200, 31 | headers: { 'Content-Type': 'application/json' }, 32 | body: { text: 'Hello World!' }, 33 | }, 34 | f, 35 | ); 36 | expect(result).toMatchObject({ 37 | url: 'https:127.0.0.1', 38 | status: 200, 39 | headers: { 'Content-Type': 'application/json' }, 40 | body: 'Hello World!', 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/getting_started_series/6_functor/__test__/optionLift.test.ts: -------------------------------------------------------------------------------- 1 | import { none, some } from 'fp-ts/lib/Option'; 2 | import { optionLift } from '../optionLift'; 3 | 4 | describe('(fb: F) => F에서 F가 Option인 Functor 테스트', () => { 5 | it('B = string, C = number인 경우 테스트', () => { 6 | const g = (s: string) => s.length; 7 | expect(optionLift(g)(none)).toMatchObject(none); 8 | expect(optionLift(g)(some('1'))).toMatchObject(some(1)); 9 | expect(optionLift(g)(some('22'))).toMatchObject(some(2)); 10 | expect(optionLift(g)(some('333'))).toMatchObject(some(3)); 11 | }); 12 | it('B = boolean, C = string 경우 테스트', () => { 13 | const g = (b: boolean) => (b ? 'success' : 'fail'); 14 | expect(optionLift(g)(none)).toMatchObject(none); 15 | expect(optionLift(g)(some(true))).toMatchObject(some('success')); 16 | expect(optionLift(g)(some(false))).toMatchObject(some('fail')); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/getting_started_series/6_functor/__test__/taskLift.test.ts: -------------------------------------------------------------------------------- 1 | import { taskLift } from '../taskLift'; 2 | 3 | describe('(fb: F) => F에서 F가 Task인 Functor 테스트', () => { 4 | let result; 5 | it('B = string, C = number인 경우 테스트', async () => { 6 | const g = (s: string) => s.length; 7 | const taskC = taskLift(g)( 8 | () => new Promise((resolve) => resolve('success')), 9 | ); 10 | result = await taskC(); 11 | expect(result).toBe(7); 12 | }); 13 | it('B = boolean, C = string 경우 테스트', async () => { 14 | const g = (b: boolean) => (b ? 'success' : 'fail'); 15 | const taskC = taskLift(g)(() => new Promise((resolve) => resolve(true))); 16 | result = await taskC(); 17 | expect(result).toBe('success'); 18 | }); 19 | it('Promise가 reject를 반환할 경우 테스트', async () => { 20 | const g = (s: string) => s.length; 21 | const taskC = taskLift(g)(() => new Promise((_, reject) => reject('fail'))); 22 | expect(taskC()).rejects.toMatch('fail'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/getting_started_series/6_functor/arrayLift.ts: -------------------------------------------------------------------------------- 1 | export function arrayLift(g: (b: B) => C): (fb: Array) => Array { 2 | return (fb) => fb.map(g); 3 | } 4 | -------------------------------------------------------------------------------- /src/getting_started_series/6_functor/functorResponse.ts: -------------------------------------------------------------------------------- 1 | import type { Functor1 } from 'fp-ts/lib/Functor'; 2 | 3 | const URI = 'Response'; 4 | 5 | type URI = typeof URI; 6 | 7 | declare module 'fp-ts/lib/HKT' { 8 | interface URItoKind { 9 | Response: Response; 10 | } 11 | } 12 | 13 | interface Response { 14 | url: string; 15 | status: number; 16 | headers: Record; 17 | body: A; 18 | } 19 | 20 | function map(fa: Response, f: (a: A) => B): Response { 21 | return { ...fa, body: f(fa.body) }; 22 | } 23 | 24 | // `Response`를 위한 Functor 인스턴스 25 | export const functorResponse: Functor1 = { 26 | URI, 27 | map, 28 | }; 29 | -------------------------------------------------------------------------------- /src/getting_started_series/6_functor/optionLift.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { isNone, some, none } from 'fp-ts/lib/Option'; 3 | 4 | export function optionLift(g: (b: B) => C): (fb: Option) => Option { 5 | return (fb) => (isNone(fb) ? none : some(g(fb.value))); 6 | } 7 | -------------------------------------------------------------------------------- /src/getting_started_series/6_functor/taskLift.ts: -------------------------------------------------------------------------------- 1 | import type { Task } from 'fp-ts/lib/Task'; 2 | 3 | export function taskLift(g: (b: B) => C): (fb: Task) => Task { 4 | return (fb) => () => fb().then(g); 5 | } 6 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/__test__/applicativeArray.test.ts: -------------------------------------------------------------------------------- 1 | import { applicativeArray } from '../applicativeArray'; 2 | 3 | describe('Applicative에서 F가 Array인 Applicative 테스트', () => { 4 | let result; 5 | it('A, B 타입 매개변수가 모두 number인 경우 map 함수 테스트', () => { 6 | const fa = [1, 2, 3, 4, 5]; 7 | const f = (n: number): number => n * n; 8 | result = applicativeArray.map(fa, f); 9 | expect(result).toMatchObject([1, 4, 9, 16, 25]); 10 | }); 11 | it('A 타입 매개변수가 number이고 B 타입 매개변수가 string인 경우 map 함수 테스트', () => { 12 | const fa = [1, 2, 3, 4, 5]; 13 | const f = (n: number): string => n.toString(); 14 | result = applicativeArray.map(fa, f); 15 | expect(result).toMatchObject(['1', '2', '3', '4', '5']); 16 | }); 17 | it('A 타입 매개변수가 number인 경우 of 함수 테스트', () => { 18 | result = applicativeArray.of(1); 19 | expect(result).toMatchObject([1]); 20 | }); 21 | it('A 타입 매개변수가 string인 경우 of 함수 테스트', () => { 22 | result = applicativeArray.of('1'); 23 | expect(result).toMatchObject(['1']); 24 | }); 25 | it('A, B 타입 매개변수가 number인 경우 ap 함수 테스트', () => { 26 | const fab = [(a: number): number => a + 1, (a: number): number => a * a]; 27 | const fa = [1, 2, 3, 4, 5]; 28 | result = applicativeArray.ap(fab, fa); 29 | expect(result).toMatchObject([2, 3, 4, 5, 6, 1, 4, 9, 16, 25]); 30 | }); 31 | it('A 타입 매개변수가 number이고 B 타입 매개변수가 string인 경우 ap 함수 테스트', () => { 32 | const fab = [ 33 | (a: number): string => a.toString(), 34 | (a: number): string => a.toString().repeat(a), 35 | ]; 36 | const fa = [1, 2, 3, 4, 5]; 37 | result = applicativeArray.ap(fab, fa); 38 | expect(result).toMatchObject([ 39 | '1', 40 | '2', 41 | '3', 42 | '4', 43 | '5', 44 | '1', 45 | '22', 46 | '333', 47 | '4444', 48 | '55555', 49 | ]); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/__test__/applicativeOption.test.ts: -------------------------------------------------------------------------------- 1 | import { some, none, isSome, isNone } from 'fp-ts/lib/Option'; 2 | import { applicativeOption } from '../applicativeOption'; 3 | 4 | describe('Applicative에서 F가 Option인 Applicative 테스트', () => { 5 | let result; 6 | it('A, B 타입 매개변수가 모두 number인 경우 map 함수 테스트 (some)', () => { 7 | const fa = some(2); 8 | const f = (n: number): number => n * n; 9 | result = applicativeOption.map(fa, f); 10 | expect(isSome(result)).toBeTruthy(); 11 | expect(result).toMatchObject(some(4)); 12 | }); 13 | it('A, B 타입 매개변수가 모두 number인 경우 map 함수 테스트 (none)', () => { 14 | const fa = none; 15 | const f = (n: number): number => n * n; 16 | result = applicativeOption.map(fa, f); 17 | expect(isNone(result)).toBeTruthy(); 18 | }); 19 | it('A 타입 매개변수가 number이고 B 타입 매개변수가 string인 경우 map 함수 테스트 (some)', () => { 20 | const fa = some(2); 21 | const f = (n: number): string => n.toString(); 22 | result = applicativeOption.map(fa, f); 23 | expect(isSome(result)).toBeTruthy(); 24 | expect(result).toMatchObject(some('2')); 25 | }); 26 | it('A 타입 매개변수가 number인 경우 of 함수 테스트', () => { 27 | result = applicativeOption.of(1); 28 | expect(result).toMatchObject(some(1)); 29 | }); 30 | it('A 타입 매개변수가 string인 경우 of 함수 테스트', () => { 31 | result = applicativeOption.of('1'); 32 | expect(result).toMatchObject(some('1')); 33 | }); 34 | it('A, B 타입 매개변수가 number인 경우 ap 함수 테스트 (fab = some, fa = some)', () => { 35 | const fab = some((a: number): number => a * a); 36 | const fa = some(2); 37 | result = applicativeOption.ap(fab, fa); 38 | expect(isSome(result)).toBeTruthy(); 39 | expect(result).toMatchObject(some(4)); 40 | }); 41 | it('A, B 타입 매개변수가 number인 경우 ap 함수 테스트 (fab = none, fa = some)', () => { 42 | const fab = none; 43 | const fa = some(2); 44 | result = applicativeOption.ap(fab, fa); 45 | expect(isNone(result)).toBeTruthy(); 46 | }); 47 | it('A, B 타입 매개변수가 number인 경우 ap 함수 테스트 (fab = some, fa = none)', () => { 48 | const fab = some((a: number): number => a * a); 49 | const fa = none; 50 | result = applicativeOption.ap(fab, fa); 51 | expect(isNone(result)).toBeTruthy(); 52 | }); 53 | it('A 타입 매개변수가 number이고 B 타입 매개변수가 string인 경우 ap 함수 테스트 (fab = some, fa = some)', () => { 54 | const fab = some((a: number): string => a.toString()); 55 | const fa = some(2); 56 | result = applicativeOption.ap(fab, fa); 57 | expect(isSome(result)).toBeTruthy(); 58 | expect(result).toMatchObject(some('2')); 59 | }); 60 | it('A 타입 매개변수가 number이고 B 타입 매개변수가 string인 경우 ap 함수 테스트 (fab = none, fa = some)', () => { 61 | const fab = none; 62 | const fa = some(2); 63 | result = applicativeOption.ap(fab, fa); 64 | expect(isNone(result)).toBeTruthy(); 65 | }); 66 | it('A 타입 매개변수가 number이고 B 타입 매개변수가 string인 경우 ap 함수 테스트 (fab = some, fa = none)', () => { 67 | const fab = some((a: number): string => a.toString()); 68 | const fa = none; 69 | result = applicativeOption.ap(fab, fa); 70 | expect(isNone(result)).toBeTruthy(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/__test__/applicativeTask.test.ts: -------------------------------------------------------------------------------- 1 | import { applicativeTask } from '../applicativeTask'; 2 | 3 | describe('Applicative에서 F가 Task인 Applicative 테스트', () => { 4 | let result; 5 | it('A, B 타입 매개변수가 모두 number인 경우 map 함수 테스트', async () => { 6 | const fa = () => new Promise((resolve) => resolve(2)); 7 | const f = (n: number): number => n * n; 8 | const task = applicativeTask.map(fa, f); 9 | result = await task(); 10 | expect(result).toBe(4); 11 | }); 12 | it('A 타입 매개변수가 string이고 B 타입 매개변수가 number인 경우 map 함수 테스트', async () => { 13 | const fa = () => new Promise((resolve) => resolve('success')); 14 | const f = (n: string): number => n.length; 15 | const task = applicativeTask.map(fa, f); 16 | result = await task(); 17 | expect(result).toBe(7); 18 | }); 19 | it('A 타입 매개변수가 number인 경우 of 함수 테스트', async () => { 20 | const task = applicativeTask.of(1); 21 | result = await task(); 22 | expect(result).toBe(1); 23 | }); 24 | it('A 타입 매개변수가 string인 경우 of 함수 테스트', async () => { 25 | const task = applicativeTask.of('success'); 26 | result = await task(); 27 | expect(result).toBe('success'); 28 | }); 29 | it('A, B 타입 매개변수가 모두 number인 경우 ap 함수 테스트', async () => { 30 | const fab = applicativeTask.of((a: number) => a * a); 31 | const fa = () => new Promise((resolve) => resolve(2)); 32 | const task = applicativeTask.ap(fab, fa); 33 | result = await task(); 34 | expect(result).toBe(4); 35 | }); 36 | it('A 타입 매개변수가 string이고 B 타입 매개변수가 number인 경우 map 함수 테스트', async () => { 37 | const fab = applicativeTask.of((a: string) => a.length); 38 | const fa = () => new Promise((resolve) => resolve('success')); 39 | const task = applicativeTask.ap(fab, fa); 40 | result = await task(); 41 | expect(result).toBe(7); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/__test__/liftA2.test.ts: -------------------------------------------------------------------------------- 1 | import { liftA2 } from '../liftA2'; 2 | import * as O from 'fp-ts/lib/Option'; 3 | import * as A from 'fp-ts/lib/Array'; 4 | import * as T from 'fp-ts/lib/Task'; 5 | 6 | describe('인자가 2개인 들어 올리는 기능을 하는 liftA2 함수 테스트', () => { 7 | it('F가 Option 타입인 경우 liftA2 함수 테스트', () => { 8 | const g = (b: string) => (c: number) => b + String(c); 9 | const fb = O.some('success'); 10 | const fc = O.some(1); 11 | const result = liftA2(O.Apply)(g)(fb)(fc); 12 | expect(result).toMatchObject(O.some('success1')); 13 | }); 14 | it('F가 Array 타입인 경우 liftA2 함수 테스트', () => { 15 | const g = (b: boolean) => (c: boolean) => b && c; 16 | const fb = [true, true, false]; 17 | const fc = [true, false, false]; 18 | const result = liftA2(A.Apply)(g)(fb)(fc); 19 | expect(result).toMatchObject([ 20 | true, 21 | false, 22 | false, 23 | true, 24 | false, 25 | false, 26 | false, 27 | false, 28 | false, 29 | ]); 30 | }); 31 | it('F가 Task 타입인 경우 liftA2 함수 테스트', async () => { 32 | const g = (b: number) => (c: string) => String(b) + c; 33 | const fb = () => new Promise((resolve) => resolve(1)); 34 | const fc = () => new Promise((resolve) => resolve('a')); 35 | const result = await liftA2(T.ApplySeq)(g)(fb)(fc)(); 36 | expect(result).toBe('1a'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/__test__/liftA3.test.ts: -------------------------------------------------------------------------------- 1 | import { liftA3 } from '../liftA3'; 2 | import * as O from 'fp-ts/lib/Option'; 3 | import * as A from 'fp-ts/lib/Array'; 4 | import * as T from 'fp-ts/lib/Task'; 5 | 6 | describe('인자가 3개인 들어 올리는 기능을 하는 liftA3 함수 테스트', () => { 7 | it('F가 Option 타입인 경우 liftA3 함수 테스트', () => { 8 | const g = (b: string) => (c: number) => (d: boolean) => 9 | b + String(c) + String(d); 10 | const fb = O.some('a'); 11 | const fc = O.some(1); 12 | const fd = O.some(true); 13 | const result = liftA3(O.Apply)(g)(fb)(fc)(fd); 14 | expect(result).toMatchObject(O.some('a1true')); 15 | }); 16 | it('F가 Array 타입인 경우 liftA3 함수 테스트', () => { 17 | const g = (b: string) => (c: number) => (d: boolean) => 18 | b + String(c) + String(d); 19 | const fb = ['a']; 20 | const fc = [1, 2, 3]; 21 | const fd = [true, false]; 22 | const result = liftA3(A.Apply)(g)(fb)(fc)(fd); 23 | expect(result).toMatchObject([ 24 | 'a1true', 25 | 'a1false', 26 | 'a2true', 27 | 'a2false', 28 | 'a3true', 29 | 'a3false', 30 | ]); 31 | }); 32 | it('F가 Task 타입인 경우 liftA3 함수 테스트', async () => { 33 | const g = (b: number) => (c: string) => (d: boolean) => 34 | String(b) + c + String(d); 35 | const fb = () => new Promise((resolve) => resolve(1)); 36 | const fc = () => new Promise((resolve) => resolve('a')); 37 | const fd = () => new Promise((resolve) => resolve(true)); 38 | const result = await liftA3(T.ApplySeq)(g)(fb)(fc)(fd)(); 39 | expect(result).toBe('1atrue'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/applicative.ts: -------------------------------------------------------------------------------- 1 | import type { HKT } from 'fp-ts/lib/HKT'; 2 | import type { Apply } from './apply'; 3 | 4 | export interface Applicative extends Apply { 5 | of: (a: A) => HKT; 6 | } 7 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/applicativeArray.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from 'fp-ts/lib/Array'; 2 | 3 | export const applicativeArray = { 4 | map: (fa: Array, f: (a: A) => B): Array => fa.map(f), 5 | of: (a: A): Array => [a], 6 | ap: (fab: Array<(a: A) => B>, fa: Array): Array => 7 | flatten(fab.map((f) => fa.map(f))), 8 | }; 9 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/applicativeOption.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { some, none, isNone } from 'fp-ts/lib/Option'; 3 | 4 | export const applicativeOption = { 5 | map: (fa: Option, f: (a: A) => B): Option => 6 | isNone(fa) ? none : some(f(fa.value)), 7 | of: (a: A): Option => some(a), 8 | ap: (fab: Option<(a: A) => B>, fa: Option): Option => 9 | isNone(fab) ? none : applicativeOption.map(fa, fab.value), 10 | }; 11 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/applicativeTask.ts: -------------------------------------------------------------------------------- 1 | import type { Task } from 'fp-ts/lib/Task'; 2 | 3 | export const applicativeTask = { 4 | map: (fa: Task, f: (a: A) => B): Task => () => fa().then(f), 5 | of: (a: A): Task => () => Promise.resolve(a), 6 | ap: (fab: Task<(a: A) => B>, fa: Task): Task => () => 7 | Promise.all([fab(), fa()]).then(([f, a]) => f(a)), 8 | }; 9 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/apply.ts: -------------------------------------------------------------------------------- 1 | import type { Functor } from 'fp-ts/lib/Functor'; 2 | import type { HKT } from 'fp-ts/lib/HKT'; 3 | 4 | export interface Apply extends Functor { 5 | ap: (fcd: HKT D>, fc: HKT) => HKT; 6 | } 7 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/liftA2.ts: -------------------------------------------------------------------------------- 1 | import type { Apply1 } from 'fp-ts/lib/Apply'; 2 | import type { Kind, URIS } from 'fp-ts/lib/HKT'; 3 | 4 | type Curried2 = (b: B) => (c: C) => D; 5 | 6 | export function liftA2( 7 | F: Apply1, 8 | ): ( 9 | g: Curried2, 10 | ) => Curried2, Kind, Kind> { 11 | return (g) => (fb) => (fc) => F.ap(F.map(fb, g), fc); 12 | } 13 | -------------------------------------------------------------------------------- /src/getting_started_series/7_applicative/liftA3.ts: -------------------------------------------------------------------------------- 1 | import type { Apply1 } from 'fp-ts/lib/Apply'; 2 | import type { Kind, URIS } from 'fp-ts/lib/HKT'; 3 | 4 | type Curried3 = (b: B) => (c: C) => (d: D) => E; 5 | 6 | export function liftA3( 7 | F: Apply1, 8 | ): ( 9 | g: Curried3, 10 | ) => Curried3, Kind, Kind, Kind> { 11 | return (g) => (fb) => (fc) => (fd) => F.ap(F.ap(F.map(fb, g), fc), fd); 12 | } 13 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/__test__/flatten.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { isSome, isNone, none, some } from 'fp-ts/lib/Option'; 3 | import { flatten } from '../flatten'; 4 | 5 | describe('Option>를 받아 Option를 반환하는 flatten 함수 테스트', () => { 6 | let result: Option; 7 | it('인자로 전달되는 mma가 some인 경우 flatten 함수 테스트', () => { 8 | result = flatten(some(some(1))); 9 | expect(isSome(result)).toBeTruthy(); 10 | expect(result).toMatchObject(some(1)); 11 | }); 12 | it('인자로 전달되는 mma가 none인 경우 flatten 함수 테스트', () => { 13 | result = flatten(none); 14 | expect(isNone(result)).toBeTruthy(); 15 | expect(result).toMatchObject(none); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/__test__/flattenFollowersOfFollowers.test.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '../user'; 2 | import { flattenFollowersOfFollowers } from '../flattenFollowersOfFollowers'; 3 | 4 | describe('flatten을 이용한 followers의 followers를 반환하는 flattenFollowersOfFollowers 함수 테스트', () => { 5 | const userD: User = { followers: [] }; 6 | const userA: User = { followers: [userD] }; 7 | const userB: User = { followers: [userA] }; 8 | const userC: User = { followers: [userA, userB] }; 9 | let result: Array; 10 | it('인자로 전달된 user가 userA인 경우', () => { 11 | result = flattenFollowersOfFollowers(userA); 12 | expect(result).toMatchObject([]); 13 | }); 14 | it('인자로 전달된 user가 userB인 경우', () => { 15 | result = flattenFollowersOfFollowers(userB); 16 | expect(result).toMatchObject([userD]); 17 | }); 18 | it('인자로 전달된 user가 userC인 경우', () => { 19 | result = flattenFollowersOfFollowers(userC); 20 | expect(result).toMatchObject([userD, userA]); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/__test__/flattenInverseHead.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { isSome, isNone, none, some } from 'fp-ts/lib/Option'; 3 | import { flattenInverseHead } from '../flattenInverseHead'; 4 | 5 | describe('flatten을 이용한 Option를 반환하는 flattenInverseHead 함수 테스트', () => { 6 | let result: Option; 7 | it('인자로 전달되는 배열이 비어있지 않은 경우 flattenInverseHead 함수 테스트', () => { 8 | const arr = [2, 4, 6]; 9 | result = flattenInverseHead(arr); 10 | expect(isSome(result)).toBeTruthy(); 11 | expect(result).toMatchObject(some(0.5)); 12 | }); 13 | it('인자로 전달되는 배열이 비어있는 경우 flattenInverseHead 함수 테스트', () => { 14 | const arr = [0]; 15 | result = flattenInverseHead(arr); 16 | expect(isNone(result)).toBeTruthy(); 17 | expect(result).toMatchObject(none); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/__test__/followersOfFollowers.test.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '../user'; 2 | import { followersOfFollowers } from '../followersOfFollowers'; 3 | 4 | describe('chain을 이용한 followers의 followers를 반환하는 followersOfFollowers 함수 테스트', () => { 5 | const userD: User = { followers: [] }; 6 | const userA: User = { followers: [userD] }; 7 | const userB: User = { followers: [userA] }; 8 | const userC: User = { followers: [userA, userB] }; 9 | let result: Array; 10 | it('인자로 전달된 user가 userA인 경우', () => { 11 | result = followersOfFollowers(userA); 12 | expect(result).toMatchObject([]); 13 | }); 14 | it('인자로 전달된 user가 userB인 경우', () => { 15 | result = followersOfFollowers(userB); 16 | expect(result).toMatchObject([userD]); 17 | }); 18 | it('인자로 전달된 user가 userC인 경우', () => { 19 | result = followersOfFollowers(userC); 20 | expect(result).toMatchObject([userD, userA]); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/__test__/getFollowers.test.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '../user'; 2 | import { getFollowers } from '../getFollowers'; 3 | 4 | describe('User 인스턴스의 followers 값을 반환하는 getFollowers 함수 테스트', () => { 5 | const userA: User = { followers: [] }; 6 | const userB: User = { followers: [userA] }; 7 | const userC: User = { followers: [userA, userB] }; 8 | let result: Array; 9 | it('인자로 전달된 user가 userA인 경우', () => { 10 | result = getFollowers(userA); 11 | expect(result).toMatchObject([]); 12 | }); 13 | it('인자로 전달된 user가 userB인 경우', () => { 14 | result = getFollowers(userB); 15 | expect(result).toMatchObject([userA]); 16 | }); 17 | it('인자로 전달된 user가 userC인 경우', () => { 18 | result = getFollowers(userC); 19 | expect(result).toMatchObject([userA, userB]); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/__test__/headInverse.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { isSome, isNone, none, some } from 'fp-ts/lib/Option'; 3 | import { headInverse } from '../headInverse'; 4 | 5 | describe('chain을 이용한 Option를 반환하는 headInverse 함수 테스트', () => { 6 | let result: Option; 7 | it('인자로 전달되는 배열이 비어있지 않은 경우 headInverse 함수 테스트', () => { 8 | const arr = [2, 4, 6]; 9 | result = headInverse(arr); 10 | expect(isSome(result)).toBeTruthy(); 11 | expect(result).toMatchObject(some(0.5)); 12 | }); 13 | it('인자로 전달되는 배열이 비어있는 경우 headInverse 함수 테스트', () => { 14 | const arr = [0]; 15 | result = headInverse(arr); 16 | expect(isNone(result)).toBeTruthy(); 17 | expect(result).toMatchObject(none); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/__test__/mapFollowersOfFollowers.test.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '../user'; 2 | import { mapFollowersOfFollowers } from '../mapFollowersOfFollowers'; 3 | 4 | describe('Array의 map으로 구현된 followers의 followers를 반환하는 mapFollowersOfFollowers 함수 테스트', () => { 5 | const userD: User = { followers: [] }; 6 | const userA: User = { followers: [userD] }; 7 | const userB: User = { followers: [userA] }; 8 | const userC: User = { followers: [userA, userB] }; 9 | let result: Array>; 10 | it('인자로 전달된 user가 userA인 경우', () => { 11 | result = mapFollowersOfFollowers(userA); 12 | expect(result).toMatchObject([[]]); 13 | }); 14 | it('인자로 전달된 user가 userB인 경우', () => { 15 | result = mapFollowersOfFollowers(userB); 16 | expect(result).toMatchObject([[userD]]); 17 | }); 18 | it('인자로 전달된 user가 userC인 경우', () => { 19 | result = mapFollowersOfFollowers(userC); 20 | expect(result).toMatchObject([[userD], [userA]]); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/__test__/mapInverseHead.test.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { isSome, none, some } from 'fp-ts/lib/Option'; 3 | import { mapInverseHead } from '../mapInverseHead'; 4 | 5 | describe('map을 이용한 Option>를 반환하는 mapInverseHead 함수 테스트', () => { 6 | let result: Option>; 7 | it('인자로 전달되는 배열이 비어있지 않은 경우 mapInverseHead 함수 테스트', () => { 8 | const arr = [2, 4, 6]; 9 | result = mapInverseHead(arr); 10 | expect(isSome(result)).toBeTruthy(); 11 | expect(result).toMatchObject(some(some(0.5))); 12 | }); 13 | it('인자로 전달되는 배열이 비어있는 경우 mapInverseHead 함수 테스트', () => { 14 | const arr = [0]; 15 | result = mapInverseHead(arr); 16 | expect(isSome(result)).toBeTruthy(); 17 | expect(result).toMatchObject(some(none)); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/flatten.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { isNone, none } from 'fp-ts/lib/Option'; 3 | 4 | export const flatten = (mma: Option>): Option => 5 | isNone(mma) ? none : mma.value; 6 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/flattenFollowersOfFollowers.ts: -------------------------------------------------------------------------------- 1 | import type { User } from './user'; 2 | import { flatten } from 'fp-ts/lib/Array'; 3 | import { getFollowers } from './getFollowers'; 4 | 5 | export const flattenFollowersOfFollowers = (user: User): Array => 6 | flatten(getFollowers(user).map(getFollowers)); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/flattenInverseHead.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { map } from 'fp-ts/lib/Option'; 3 | import { head } from 'fp-ts/lib/Array'; 4 | import { inverse } from './inverse'; 5 | import { flatten } from './flatten'; 6 | 7 | export const flattenInverseHead = (arr: Array): Option => 8 | flatten(map(inverse)(head(arr))); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/followersOfFollowers.ts: -------------------------------------------------------------------------------- 1 | import type { User } from './user'; 2 | import { chain } from 'fp-ts/lib/Array'; 3 | import { getFollowers } from './getFollowers'; 4 | 5 | export const followersOfFollowers = (user: User): Array => 6 | chain(getFollowers)(getFollowers(user)); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/getFollowers.ts: -------------------------------------------------------------------------------- 1 | import type { User } from './user'; 2 | 3 | export const getFollowers = (user: User): Array => user.followers; 4 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/headInverse.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { head } from 'fp-ts/lib/Array'; 3 | import { chain } from 'fp-ts/lib/Option'; 4 | import { inverse } from './inverse'; 5 | 6 | export const headInverse = (arr: Array): Option => 7 | chain(inverse)(head(arr)); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/inverse.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { none, some } from 'fp-ts/lib/Option'; 3 | 4 | export const inverse = (n: number): Option => 5 | n === 0 ? none : some(1 / n); 6 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/mapFollowersOfFollowers.ts: -------------------------------------------------------------------------------- 1 | import type { User } from './user'; 2 | import { getFollowers } from './getFollowers'; 3 | 4 | export const mapFollowersOfFollowers = (user: User): Array> => 5 | getFollowers(user).map(getFollowers); 6 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/mapInverseHead.ts: -------------------------------------------------------------------------------- 1 | import type { Option } from 'fp-ts/lib/Option'; 2 | import { map } from 'fp-ts/lib/Option'; 3 | import { head } from 'fp-ts/lib/Array'; 4 | import { inverse } from './inverse'; 5 | 6 | export const mapInverseHead = (arr: Array): Option> => 7 | map(inverse)(head(arr)); 8 | -------------------------------------------------------------------------------- /src/getting_started_series/8_monad/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | followers: Array; 3 | } 4 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/chainValidatePassword.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { isLeft, isRight, right } from 'fp-ts/lib/Either'; 3 | import { chainValidatePassword } from '../chainValidatePassword'; 4 | 5 | describe('chain을 사용해 비밀번호의 유효성을 검증하는 chainValidatePassword 함수 테스트', () => { 6 | let result: Either; 7 | it('인자로 전달되는 문자열에 대문자가 없을 경우 테스트', () => { 8 | result = chainValidatePassword('abcdef'); 9 | expect(isLeft(result)).toBeTruthy(); 10 | }); 11 | it('인자로 전달되는 문자열에 대문자가 1개 있으나 길이가 짧은 경우 테스트', () => { 12 | result = chainValidatePassword('Abcde'); 13 | expect(isLeft(result)).toBeTruthy(); 14 | }); 15 | it('인자로 전달되는 문자열에 대문자가 1개 있으나 숫자가 없는 경우 테스트', () => { 16 | result = chainValidatePassword('Abcdef'); 17 | expect(isLeft(result)).toBeTruthy(); 18 | }); 19 | it('인자로 전달되는 문자열이 모든 유효성을 만족하는 경우 테스트', () => { 20 | result = chainValidatePassword('Abcdef1'); 21 | expect(isRight(result)).toBeTruthy(); 22 | expect(result).toMatchObject(right('Abcdef1')); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/minLength.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { isLeft, isRight, right } from 'fp-ts/lib/Either'; 3 | import { minLength } from '../minLength'; 4 | 5 | describe('문자열의 길이가 6보다 큰지 확인하는 minLength 함수 테스트', () => { 6 | let result: Either; 7 | it('인자로 전달되는 문자열의 길이가 6보다 작을 경우 테스트', () => { 8 | result = minLength('12345'); 9 | expect(isLeft(result)).toBeTruthy(); 10 | }); 11 | it('인자로 전달되는 문자열의 길이가 6일 경우 테스트', () => { 12 | result = minLength('123456'); 13 | expect(isRight(result)).toBeTruthy(); 14 | expect(right('123456')).toMatchObject(result); 15 | }); 16 | it('인자로 전달되는 문자열의 길이가 6보다 클 경우 테스트', () => { 17 | result = minLength('1234567'); 18 | expect(isRight(result)).toBeTruthy(); 19 | expect(right('1234567')).toMatchObject(result); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/minLengthV.test.ts: -------------------------------------------------------------------------------- 1 | import { Either, left } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { isLeft, isRight, right } from 'fp-ts/lib/Either'; 4 | import { minLengthV } from '../minLengthV'; 5 | 6 | describe('문자열의 길이가 6보다 큰지 확인하는 minLengthV 함수 테스트', () => { 7 | let result: Either, string>; 8 | it('인자로 전달되는 문자열의 길이가 6보다 작을 경우 테스트', () => { 9 | result = minLengthV('12345'); 10 | expect(isLeft(result)).toBeTruthy(); 11 | expect(result).toMatchObject(left(['at least 6 characters'])); 12 | }); 13 | it('인자로 전달되는 문자열의 길이가 6일 경우 테스트', () => { 14 | result = minLengthV('123456'); 15 | expect(isRight(result)).toBeTruthy(); 16 | expect(right('123456')).toMatchObject(result); 17 | }); 18 | it('인자로 전달되는 문자열의 길이가 6보다 클 경우 테스트', () => { 19 | result = minLengthV('1234567'); 20 | expect(isRight(result)).toBeTruthy(); 21 | expect(right('1234567')).toMatchObject(result); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/oneCapital.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { isLeft, isRight, right } from 'fp-ts/lib/Either'; 3 | import { oneCapital } from '../oneCapital'; 4 | 5 | describe('문자열에 대문자가 적어도 하나 있는지 확인하는 oneCapital 함수 테스트', () => { 6 | let result: Either; 7 | it('인자로 전달되는 문자열에 대문자가 없을 경우 테스트', () => { 8 | result = oneCapital('abcde'); 9 | expect(isLeft(result)).toBeTruthy(); 10 | }); 11 | it('인자로 전달되는 문자열에 대문자가 1개 있을 경우 테스트', () => { 12 | result = oneCapital('Abcde'); 13 | expect(isRight(result)).toBeTruthy(); 14 | expect(right('Abcde')).toMatchObject(result); 15 | }); 16 | it('인자로 전달되는 문자열에 대문자 1개보다 많을 경우 테스트', () => { 17 | result = oneCapital('ABCDE'); 18 | expect(isRight(result)).toBeTruthy(); 19 | expect(right('ABCDE')).toMatchObject(result); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/oneCapitalV.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { isLeft, isRight, right, left } from 'fp-ts/lib/Either'; 4 | import { oneCapitalV } from '../oneCapitalV'; 5 | 6 | describe('문자열에 대문자가 적어도 하나 있는지 확인하는 oneCapitalV 함수 테스트', () => { 7 | let result: Either, string>; 8 | it('인자로 전달되는 문자열에 대문자가 없을 경우 테스트', () => { 9 | result = oneCapitalV('abcde'); 10 | expect(isLeft(result)).toBeTruthy(); 11 | expect(result).toMatchObject(left(['at least one capital letter'])); 12 | }); 13 | it('인자로 전달되는 문자열에 대문자가 1개 있을 경우 테스트', () => { 14 | result = oneCapitalV('Abcde'); 15 | expect(isRight(result)).toBeTruthy(); 16 | expect(right('Abcde')).toMatchObject(result); 17 | }); 18 | it('인자로 전달되는 문자열에 대문자 1개보다 많을 경우 테스트', () => { 19 | result = oneCapitalV('ABCDE'); 20 | expect(isRight(result)).toBeTruthy(); 21 | expect(right('ABCDE')).toMatchObject(result); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/oneNumber.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { isLeft, isRight, right } from 'fp-ts/lib/Either'; 3 | import { oneNumber } from '../oneNumber'; 4 | 5 | describe('문자열에 숫자가 적어도 하나 있는지 확인하는 oneNumber 함수 테스트', () => { 6 | let result: Either; 7 | it('인자로 전달되는 문자열에 숫자가 없을 경우 테스트', () => { 8 | result = oneNumber('ABCDE'); 9 | expect(isLeft(result)).toBeTruthy(); 10 | }); 11 | it('인자로 전달되는 문자열에 숫자가 1개 있을 경우 테스트', () => { 12 | result = oneNumber('ABCDE1'); 13 | expect(isRight(result)).toBeTruthy(); 14 | expect(right('ABCDE1')).toMatchObject(result); 15 | }); 16 | it('인자로 전달되는 문자열에 숫자가 1개보다 많을 경우 테스트', () => { 17 | result = oneNumber('ABCD12'); 18 | expect(isRight(result)).toBeTruthy(); 19 | expect(right('ABCD12')).toMatchObject(result); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/oneNumberV.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { isLeft, isRight, right, left } from 'fp-ts/lib/Either'; 4 | import { oneNumberV } from '../oneNumberV'; 5 | 6 | describe('문자열에 숫자가 적어도 하나 있는지 확인하는 oneNumberV 함수 테스트', () => { 7 | let result: Either, string>; 8 | it('인자로 전달되는 문자열에 숫자가 없을 경우 테스트', () => { 9 | result = oneNumberV('ABCDE'); 10 | expect(isLeft(result)).toBeTruthy(); 11 | expect(result).toMatchObject(left(['at least one number'])); 12 | }); 13 | it('인자로 전달되는 문자열에 숫자가 1개 있을 경우 테스트', () => { 14 | result = oneNumberV('ABCDE1'); 15 | expect(isRight(result)).toBeTruthy(); 16 | expect(right('ABCDE1')).toMatchObject(result); 17 | }); 18 | it('인자로 전달되는 문자열에 숫자가 1개보다 많을 경우 테스트', () => { 19 | result = oneNumberV('ABCD12'); 20 | expect(isRight(result)).toBeTruthy(); 21 | expect(right('ABCD12')).toMatchObject(result); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/toPerson.test.ts: -------------------------------------------------------------------------------- 1 | import { toPerson } from '../toPerson'; 2 | 3 | describe('[string, number] 튜플을 받아 Person 객체를 반환하는 toPerson 함수 테스트', () => { 4 | it('toPerson 함수가 정상적으로 값을 반환하는지 테스트', () => { 5 | const name = 'test'; 6 | const age = 20; 7 | const tuple: [string, number] = [name, age]; 8 | const result = toPerson(tuple); 9 | expect(result.age).toBe(age); 10 | expect(result.name).toBe(name); 11 | expect(result).toMatchObject({ name, age }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/validateAge.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { isLeft, isRight, right, left } from 'fp-ts/lib/Either'; 4 | import { validateAge } from '../validateAge'; 5 | 6 | describe('입력으로 받은 문자열이 유효한 숫자인지 확인하는 validateAge 함수 테스트', () => { 7 | let result: Either, number>; 8 | it('인자로 전달되는 문자열이 숫자가 아닌 경우 테스트', () => { 9 | result = validateAge('abcde'); 10 | expect(isLeft(result)).toBeTruthy(); 11 | expect(result).toMatchObject(left(['Invalid age'])); 12 | }); 13 | it('인자로 전달되는 문자열이 양수인 경우 테스트', () => { 14 | result = validateAge('20'); 15 | expect(isRight(result)).toBeTruthy(); 16 | expect(result).toMatchObject(right(20)); 17 | }); 18 | it('인자로 전달되는 문자열이 음수인 경우 테스트', () => { 19 | result = validateAge('-20'); 20 | expect(isRight(result)).toBeTruthy(); 21 | expect(result).toMatchObject(right(-20)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/validateName.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { isLeft, isRight, right, left } from 'fp-ts/lib/Either'; 4 | import { validateName } from '../validateName'; 5 | 6 | describe('입력으로 받은 문자열이 유효한 이름인지 확인하는 validateName 함수 테스트', () => { 7 | let result: Either, string>; 8 | it('인자로 전달되는 문자열이 빈 문자열인 경우 테스트', () => { 9 | result = validateName(''); 10 | expect(isLeft(result)).toBeTruthy(); 11 | expect(result).toMatchObject(left(['Invalid name'])); 12 | }); 13 | it('인자로 전달되는 문자열이 비어있지 않은 경우 테스트', () => { 14 | result = validateName('test'); 15 | expect(isRight(result)).toBeTruthy(); 16 | expect(right('test')).toMatchObject(result); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/validatePassword.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { isLeft, isRight, right, left } from 'fp-ts/lib/Either'; 4 | import { validatePassword } from '../validatePassword'; 5 | 6 | describe('sequenceT을 사용해 비밀번호의 유효성을 검증하는 validatePassword 함수 테스트', () => { 7 | let result: Either, string>; 8 | it('인자로 전달되는 문자열이 모든 유효성을 만족하지 못할 경우 테스트', () => { 9 | result = validatePassword('a'); 10 | expect(isLeft(result)).toBeTruthy(); 11 | expect(result).toMatchObject( 12 | left([ 13 | 'at least 6 characters', 14 | 'at least one capital letter', 15 | 'at least one number', 16 | ]), 17 | ); 18 | }); 19 | it('인자로 전달되는 문자열이 대문자 유효성만 만족할 경우 테스트', () => { 20 | result = validatePassword('Abcde'); 21 | expect(isLeft(result)).toBeTruthy(); 22 | expect(result).toMatchObject( 23 | left(['at least 6 characters', 'at least one number']), 24 | ); 25 | }); 26 | it('인자로 전달되는 문자열에 길이 유효성만 만족할 경우 테스트', () => { 27 | result = validatePassword('abcdef'); 28 | expect(isLeft(result)).toBeTruthy(); 29 | expect(result).toMatchObject( 30 | left(['at least one capital letter', 'at least one number']), 31 | ); 32 | }); 33 | it('인자로 전달되는 문자열에 숫자 포함 유효성만 만족할 경우 테스트', () => { 34 | result = validatePassword('1'); 35 | expect(isLeft(result)).toBeTruthy(); 36 | expect(result).toMatchObject( 37 | left(['at least 6 characters', 'at least one capital letter']), 38 | ); 39 | }); 40 | it('인자로 전달되는 문자열에 숫자 포함, 대문자 유효성만 만족할 경우 테스트', () => { 41 | result = validatePassword('A1'); 42 | expect(isLeft(result)).toBeTruthy(); 43 | expect(result).toMatchObject(left(['at least 6 characters'])); 44 | }); 45 | it('인자로 전달되는 문자열에 숫자 포함, 길이 유효성만 만족할 경우 테스트', () => { 46 | result = validatePassword('abcde1'); 47 | expect(isLeft(result)).toBeTruthy(); 48 | expect(result).toMatchObject(left(['at least one capital letter'])); 49 | }); 50 | it('인자로 전달되는 문자열에 대문자 포함, 길이 유효성만 만족할 경우 테스트', () => { 51 | result = validatePassword('Abcdef'); 52 | expect(isLeft(result)).toBeTruthy(); 53 | expect(result).toMatchObject(left(['at least one number'])); 54 | }); 55 | it('인자로 전달되는 문자열이 모든 유효성을 만족하는 경우 테스트', () => { 56 | result = validatePassword('Abcdef1'); 57 | expect(isRight(result)).toBeTruthy(); 58 | expect(result).toMatchObject(right('Abcdef1')); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/__test__/validatePerson.test.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import type { Person } from '../person'; 4 | import { isLeft, isRight, right, left } from 'fp-ts/lib/Either'; 5 | import { validatePerson } from '../validatePerson'; 6 | 7 | describe('입력으로 받은 문자열이 유효한 이름인지 확인하는 validatePerson 함수 테스트', () => { 8 | let result: Either, Person>; 9 | const validAge = '20'; 10 | const validName = 'test'; 11 | const inValidAge = 'age'; 12 | const inValidName = ''; 13 | it('인자로 전달되는 이름과 나이가 유효하지 않은 경우 테스트', () => { 14 | result = validatePerson(inValidName, inValidAge); 15 | expect(isLeft(result)).toBeTruthy(); 16 | expect(result).toMatchObject(left(['Invalid name', 'Invalid age'])); 17 | }); 18 | it('인자로 전달되는 이름이 유효하지 않은 경우 테스트', () => { 19 | result = validatePerson(inValidName, validAge); 20 | expect(isLeft(result)).toBeTruthy(); 21 | expect(result).toMatchObject(left(['Invalid name'])); 22 | }); 23 | it('인자로 전달되는 나이가 유효하지 않은 경우 테스트', () => { 24 | result = validatePerson(validName, inValidAge); 25 | expect(isLeft(result)).toBeTruthy(); 26 | expect(result).toMatchObject(left(['Invalid age'])); 27 | }); 28 | it('인자로 전달되는 이름과 나이가 유효한 경우 테스트', () => { 29 | const result = validatePerson(validName, validAge); 30 | expect(isRight(result)).toBeTruthy(); 31 | expect(result).toMatchObject(right({ name: validName, age: +validAge })); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/applicativeValidation.ts: -------------------------------------------------------------------------------- 1 | import { getSemigroup } from 'fp-ts/lib/NonEmptyArray'; 2 | import { getApplicativeValidation } from 'fp-ts/lib/Either'; 3 | 4 | export const applicativeValidation = getApplicativeValidation( 5 | getSemigroup(), 6 | ); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/chainValidatePassword.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { chain } from 'fp-ts/lib/Either'; 3 | import { pipe } from 'fp-ts/lib/function'; 4 | import { minLength } from './minLength'; 5 | import { oneCapital } from './oneCapital'; 6 | import { oneNumber } from './oneNumber'; 7 | 8 | export const chainValidatePassword = (s: string): Either => 9 | pipe(minLength(s), chain(oneCapital), chain(oneNumber)); 10 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/lift.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { pipe } from 'fp-ts/lib/function'; 4 | import { mapLeft } from 'fp-ts/lib/Either'; 5 | 6 | export function lift( 7 | check: (a: A) => Either, 8 | ): (a: A) => Either, A> { 9 | return (a) => 10 | pipe( 11 | check(a), 12 | mapLeft((a) => [a]), 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/minLength.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { left, right } from 'fp-ts/lib/Either'; 3 | 4 | export const minLength = (s: string): Either => 5 | s.length >= 6 ? right(s) : left('at least 6 characters'); 6 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/minLengthV.ts: -------------------------------------------------------------------------------- 1 | import { lift } from './lift'; 2 | import { minLength } from './minLength'; 3 | 4 | export const minLengthV = lift(minLength); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/oneCapital.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { left, right } from 'fp-ts/lib/Either'; 3 | 4 | export const oneCapital = (s: string): Either => 5 | /[A-Z]/g.test(s) ? right(s) : left('at least one capital letter'); 6 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/oneCapitalV.ts: -------------------------------------------------------------------------------- 1 | import { lift } from './lift'; 2 | import { oneCapital } from './oneCapital'; 3 | 4 | export const oneCapitalV = lift(oneCapital); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/oneNumber.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import { left, right } from 'fp-ts/lib/Either'; 3 | 4 | export const oneNumber = (s: string): Either => 5 | /[0-9]/g.test(s) ? right(s) : left('at least one number'); 6 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/oneNumberV.ts: -------------------------------------------------------------------------------- 1 | import { lift } from './lift'; 2 | import { oneNumber } from './oneNumber'; 3 | 4 | export const oneNumberV = lift(oneNumber); 5 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/person.ts: -------------------------------------------------------------------------------- 1 | export interface Person { 2 | name: string; 3 | age: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/toPerson.ts: -------------------------------------------------------------------------------- 1 | import type { Person } from './person'; 2 | 3 | export const toPerson = ([name, age]: [string, number]): Person => ({ 4 | name, 5 | age, 6 | }); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/validateAge.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { left, right } from 'fp-ts/lib/Either'; 4 | 5 | export const validateAge = (s: string): Either, number> => 6 | isNaN(+s) ? left(['Invalid age']) : right(+s); 7 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/validateName.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { left, right } from 'fp-ts/lib/Either'; 4 | 5 | export const validateName = ( 6 | s: string, 7 | ): Either, string> => 8 | s.length === 0 ? left(['Invalid name']) : right(s); 9 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/validatePassword.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import { sequenceT } from 'fp-ts/lib/Apply'; 4 | import { pipe } from 'fp-ts/lib/function'; 5 | import { map } from 'fp-ts/lib/Either'; 6 | import { minLengthV } from './minLengthV'; 7 | import { oneCapitalV } from './oneCapitalV'; 8 | import { oneNumberV } from './oneNumberV'; 9 | import { applicativeValidation } from './applicativeValidation'; 10 | 11 | export function validatePassword( 12 | s: string, 13 | ): Either, string> { 14 | return pipe( 15 | sequenceT(applicativeValidation)( 16 | minLengthV(s), 17 | oneCapitalV(s), 18 | oneNumberV(s), 19 | ), 20 | map(() => s), 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/getting_started_series/9_either_vs_validation/validatePerson.ts: -------------------------------------------------------------------------------- 1 | import type { Either } from 'fp-ts/lib/Either'; 2 | import type { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'; 3 | import type { Person } from './person'; 4 | import { map } from 'fp-ts/lib/Either'; 5 | import { pipe } from 'fp-ts/lib/function'; 6 | import { sequenceT } from 'fp-ts/lib/Apply'; 7 | import { toPerson } from './toPerson'; 8 | import { validateAge } from './validateAge'; 9 | import { validateName } from './validateName'; 10 | import { applicativeValidation } from './applicativeValidation'; 11 | 12 | export function validatePerson( 13 | name: string, 14 | age: string, 15 | ): Either, Person> { 16 | return pipe( 17 | sequenceT(applicativeValidation)(validateName(name), validateAge(age)), 18 | map(toPerson), 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // noop 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "allowJs": true, 8 | "importHelpers": true, 9 | "alwaysStrict": true, 10 | "sourceMap": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitReturns": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitAny": false, 17 | "noImplicitThis": false, 18 | "strictNullChecks": false 19 | }, 20 | "include": ["src/**/*", "__tests__/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "build", 6 | "removeComments": true 7 | }, 8 | "include": ["src/**/*"] 9 | } 10 | --------------------------------------------------------------------------------