├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── book.toml ├── file.txt ├── mind-map.png ├── monad-transformers.md ├── package-lock.json ├── package.json ├── src ├── 00_pipe_and_flow.ts ├── 01_retry.ts ├── 02_ord.ts ├── 03_shapes.ts ├── 04_functor.ts ├── 05_applicative.ts ├── 06_game.ts ├── Console.ts ├── README.md ├── SUMMARY.md ├── algebraic-data-types │ ├── README.md │ ├── adt.md │ ├── product-types.md │ └── sum-types.md ├── applicative-functor │ ├── Currying.md │ ├── README.md │ ├── ap.md │ ├── compose.md │ ├── of.md │ └── solve-general-problem.md ├── category-theory │ ├── README.md │ ├── composition-core-problem.md │ ├── definition.md │ ├── modeling-programming-languages.md │ └── typescript.md ├── eq-modeling │ └── README.md ├── exercises │ ├── ADT01.ts │ ├── ADT02.ts │ ├── ADT03.ts │ ├── Applicative01.ts │ ├── Apply01.ts │ ├── Eq01.ts │ ├── Eq02.ts │ ├── Eq03.ts │ ├── FEH01.ts │ ├── FEH02.ts │ ├── FEH03.ts │ ├── FEH04.ts │ ├── FEH05.ts │ ├── Functor01.ts │ ├── Functor02.ts │ ├── Functor03.ts │ ├── Magma01.ts │ ├── Monad01.ts │ ├── Monad02.ts │ ├── Monad03.ts │ ├── Monoid01.ts │ ├── Ord01.ts │ ├── Semigroup01.ts │ └── Semigroup02.ts ├── functional-error-handling │ ├── README.md │ ├── either.md │ ├── eq.md │ ├── option.md │ └── semigroup-monoid.md ├── functor.html ├── functor │ ├── README.md │ ├── boundary-of-functor.md │ ├── compose.md │ ├── contravariant-functor.md │ ├── definition.md │ ├── error-handling.md │ ├── functions-as-programs.md │ ├── functor-in-fp-ts.md │ └── solve-general-problem.md ├── hangman.ts ├── images │ ├── adt.png │ ├── associativity.png │ ├── bird.png │ ├── category.png │ ├── chain.png │ ├── composition.png │ ├── contramap.png │ ├── eilenberg.jpg │ ├── flatMap.png │ ├── functor.png │ ├── identity.png │ ├── kleisli.jpg │ ├── kleisli_arrows.png │ ├── kleisli_category.png │ ├── kleisli_composition.png │ ├── liftA2-first-step.png │ ├── liftA2.png │ ├── maclane.jpg │ ├── map.png │ ├── moggi.jpg │ ├── monoid.png │ ├── morphism.png │ ├── mutable-immutable.jpg │ ├── objects-morphisms.png │ ├── of.png │ ├── semigroup.png │ ├── semigroupVector.png │ ├── spoiler.png │ └── wadler.jpg ├── laws.ts ├── monad │ ├── README.md │ ├── defining-chain.md │ ├── definition.md │ ├── kleisli-category.md │ ├── manipulating-program.md │ └── nested-context-problem.md ├── monoid-modeling │ ├── README.md │ ├── concat-all.md │ └── product-monoid.md ├── mutable-is-unsafe-in-typescript.ts ├── onion-architecture │ ├── 1.ts │ ├── 2.ts │ ├── 3.ts │ ├── 4.ts │ ├── 5.ts │ ├── 6.ts │ └── employee_data.txt ├── ord-modeling │ ├── README.md │ └── dual-ordering.md ├── program5.ts ├── pure-and-partial-functions │ └── README.md ├── reader.ts ├── semigroup-modeling │ ├── README.md │ ├── concat-all.md │ ├── dual-semigroup.md │ ├── find-semigroup.md │ ├── magma.md │ ├── order-derivable-semigroup.md │ ├── semigroup-product.md │ └── semigroup.md ├── shapes.html ├── solutions │ ├── ADT01.ts │ ├── ADT02.ts │ ├── ADT03.ts │ ├── Applicative01.ts │ ├── Apply01.ts │ ├── Eq01.ts │ ├── Eq02.ts │ ├── Eq03.ts │ ├── FEH01.ts │ ├── FEH02.ts │ ├── FEH03.ts │ ├── FEH04.ts │ ├── FEH05.ts │ ├── Functor01.ts │ ├── Functor02.ts │ ├── Functor03.ts │ ├── Magma01.ts │ ├── Monad01.ts │ ├── Monad02.ts │ ├── Monad03.ts │ ├── Monoid01.ts │ ├── Ord01.ts │ ├── Semigroup01.ts │ └── Semigroup02.ts ├── two-pillar-of-fp │ ├── README.md │ ├── composition.md │ └── referential-transparency.md └── what-is-fp │ └── README.md ├── task-vs-promise.md ├── tsconfig.json └── tslint.json /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy static content to Pages 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | deploy: 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | 28 | - name: Setup mdBook 29 | uses: peaceiris/actions-mdbook@v1 30 | with: 31 | mdbook-version: 'latest' 32 | 33 | - name: Build mdBook 34 | run: mdbook build 35 | 36 | - name: Setup Pages 37 | uses: actions/configure-pages@v2 38 | 39 | - name: Upload Artifact 40 | uses: actions/upload-pages-artifact@v1 41 | with: 42 | path: book 43 | 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v1 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | dist 4 | dev 5 | coverage 6 | .cache 7 | .idea 8 | /book 9 | /index.html 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to "Introduction to Functional Programming" 2 | 3 | Contributions should have generally the goal of making the original author's content more digestible or fixing typos/language. 4 | 5 | Pull Requests are welcome if they comply with the aforementioned goal. 6 | 7 | These policies may change in the future. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Based on functional-programming by Giulio Canti 2 | 3 | MIT License 4 | 5 | Copyright (c) 2019 - present: Giulio Canti 6 | 7 | English translation and modifications: 8 | Copyright (c) 2020 - present: Enrico Polanski 9 | 10 | Korean translation and modifications: 11 | Copyright (c) 2022 - present: Jake Son 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 소개 2 | 3 | 이 저장소는 Typescript 와 fp-ts 라이브러리를 활용한 함수형 프로그래밍을 소개합니다. 4 | 5 | 모든 내용은 [enricopolanski](https://github.com/enricopolanski/functional-programming) 의 저장소에서 나온 것입니다. 6 | 7 | 해당 저장소도 이탈리아어로 작성된 [Giulio Canti](https://gcanti.github.io/about.html) 의 ["Introduction to Functional Programming (Italian)"](https://github.com/gcanti/functional-programming) 을 영어로 번역한 것입니다. 8 | 9 | 원본 작성자는 해당 글을 함수형 프로그래밍에 관한 강의나 워크샵에 참고자료로 사용하였습니다. 10 | 11 | 개인적인 공부와 fp-ts 라이브러리 생태계를 소개하기 위한 목적으로 번역하였습니다. 12 | 13 | 오역 및 번역이 매끄럽지 않은 부분이 존재하며 특히 번역이 어려웠던 부분은 원글을 함께 표시하였습니다. 14 | 15 | **Setup** 16 | 17 | ```sh 18 | git clone https://github.com/jbl428/functional-programming.git 19 | cd functional-programming 20 | npm i 21 | ``` 22 | 23 | > 본 문서는 웹사이트로 볼 수 있습니다. 24 | > [Github Page Link](https://jbl428.github.io/functional-programming) 25 | 26 | * [함수형 프로그래밍이란](src/what-is-fp/README.md) 27 | 28 | * [함수형 프로그래밍의 두 가지 요소](src/two-pillar-of-fp/README.md) 29 | * [참조 투명성](src/two-pillar-of-fp/referential-transparency.md) 30 | * [합성](src/two-pillar-of-fp/composition.md) 31 | 32 | * [Semigroup 으로 합성 모델링](src/semigroup-modeling/README.md) 33 | * [Magma 의 정의](src/semigroup-modeling/magma.md) 34 | * [Semigroup 의 정의](src/semigroup-modeling/semigroup.md) 35 | * [concatAll 함수](src/semigroup-modeling/concat-all.md) 36 | * [Dual semigroup](src/semigroup-modeling/dual-semigroup.md) 37 | * [Semigroup product](src/semigroup-modeling/semigroup-product.md) 38 | * [임의의 타입에 대한 semigroup 인스턴스 찾기](src/semigroup-modeling/find-semigroup.md) 39 | * [Order-derivable Semigroups](src/semigroup-modeling/order-derivable-semigroup.md) 40 | 41 | * [`Eq` 를 활용한 동등성 모델링](src/eq-modeling/README.md) 42 | 43 | * [`Ord` 를 활용한 순서 관계 모델링](src/ord-modeling/README.md) 44 | * [Dual Ordering](src/ord-modeling/dual-ordering.md) 45 | 46 | * [`Monoid` 를 활용한 합성 모델링](src/monoid-modeling/README.md) 47 | * [concatAll 함수](src/monoid-modeling/concat-all.md) 48 | * [product monoid](src/monoid-modeling/product-monoid.md) 49 | 50 | * [순수함수와 부분함수](src/pure-and-partial-functions/README.md) 51 | 52 | * [대수적 자료형](src/algebraic-data-types/README.md) 53 | * [정의](src/algebraic-data-types/adt.md) 54 | * [곱타입](src/algebraic-data-types/product-types.md) 55 | * [합타입](src/algebraic-data-types/sum-types.md) 56 | 57 | * [함수적 오류 처리](src/functional-error-handling/README.md) 58 | * [Option 타입](src/functional-error-handling/option.md) 59 | * [Eq 인스턴스](src/functional-error-handling/eq.md) 60 | * [Semigroup, Monoid 인스턴스](src/functional-error-handling/semigroup-monoid.md) 61 | * [Either 타입](src/functional-error-handling/either.md) 62 | 63 | * [Category theory](src/category-theory/README.md) 64 | * [정의](src/category-theory/definition.md) 65 | * [프로그래밍 언어 모델링](src/category-theory/modeling-programming-languages.md) 66 | * [TypeScript](src/category-theory/typescript.md) 67 | * [합성의 핵심 문제](src/category-theory/composition-core-problem.md) 68 | 69 | * [Functor](src/functor/README.md) 70 | * [프로그램으로서의 함수](src/functor/functions-as-programs.md) 71 | * [Functor 의 경계](src/functor/boundary-of-functor.md) 72 | * [정의](src/functor/definition.md) 73 | * [오류 처리](src/functor/error-handling.md) 74 | * [합성](src/functor/compose.md) 75 | * [contravariant functor](src/functor/contravariant-functor.md) 76 | * [fp-ts 에서의 functor](src/functor/functor-in-fp-ts.md) 77 | * [일반적인 문제 해결](src/functor/solve-general-problem.md) 78 | 79 | * [Applicative Functor](src/applicative-functor/README.md) 80 | * [Currying](src/applicative-functor/Currying.md) 81 | * [ap 연산](src/applicative-functor/ap.md) 82 | * [of 연산](src/applicative-functor/of.md) 83 | * [합성](src/applicative-functor/compose.md) 84 | * [문제 해결](src/applicative-functor/solve-general-problem.md) 85 | 86 | * [Monad](src/monad/README.md) 87 | * [중첩된 context 문제](src/monad/nested-context-problem.md) 88 | * [정의](src/monad/definition.md) 89 | * [Kleisli Category](src/monad/kleisli-category.md) 90 | * [단계별 chain 정의](src/monad/defining-chain.md) 91 | * [프로그램 다루기](src/monad/manipulating-program.md) 92 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "Typescript 와 fp-ts 라이브러리를 활용한 함수형 프로그래밍" 3 | -------------------------------------------------------------------------------- /file.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /mind-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/mind-map.png -------------------------------------------------------------------------------- /monad-transformers.md: -------------------------------------------------------------------------------- 1 | # Monad transformers 2 | 3 | **Senario 1** 4 | 5 | Supponiamo di avere definto le seguenti API: 6 | 7 | ```typescript 8 | import { head } from 'fp-ts/lib/Array' 9 | import { Option } from 'fp-ts/lib/Option' 10 | import { pipe } from 'fp-ts/lib/pipeable' 11 | import { map, Task, task } from 'fp-ts/lib/Task' 12 | 13 | function fetchUserComments( 14 | id: string 15 | ): Task> { 16 | return task.of(['comment1', 'comment2']) 17 | } 18 | 19 | function fetchFirstComment( 20 | id: string 21 | ): Task> { 22 | return pipe( 23 | fetchUserComments(id), 24 | map(head) 25 | ) 26 | } 27 | ``` 28 | 29 | Il tipo del codominio della funzione `fetchFirstComment` è `Task>` ovvero una struttura dati innestata. 30 | 31 | È possibile associare una istanza di monade? 32 | 33 | **Senario 2** 34 | 35 | Per modellare una chiamata AJAX, il type constructor `Task` non è sufficiente dato che rappresenta una computazione 36 | asincrona che non può mai fallire, come possiamo modellare anche i possibili errori restituiti dalla chiamata (`403`, `500`, etc...)? 37 | 38 | Più in generale supponiamo di avere una computazione con le seguenti proprietà: 39 | 40 | - asincrona 41 | - può fallire 42 | 43 | Come possiamo modellarla? 44 | 45 | Sappiamo che questi due effetti possono essere rispettivamente codificati dai seguenti tipi: 46 | 47 | - `Task` (asincronicità) 48 | - `Either` (possibile fallimento) 49 | 50 | e che ambedue hanno una istanza di monade. 51 | 52 | Come posso combinare questi due effetti? 53 | 54 | In due modi: 55 | 56 | - `Task>` rappresenta una computazione asincrona che può fallire 57 | - `Either>` rappresenta una computazione che può fallire oppure che restituisce una computazione asincrona 58 | 59 | Diciamo che sono interessato al primo dei due modi: 60 | 61 | ```typescript 62 | interface TaskEither extends Task> {} 63 | ``` 64 | 65 | È possibile definire una istanza di monade per `TaskEither`? 66 | 67 | ## Le monadi non compongono 68 | 69 | In generale le monadi non compongono, ovvero date due istanze di monade, una per `M` e una per `N`, 70 | allora non è detto che `M>` ammetta ancora una istanza di monade. 71 | 72 | **Quiz**. Perchè? Provare a definire la funzione `flatten`. 73 | 74 | Che non compongano in generale però non vuol dire che non esistano dei casi particolari ove questo succede. 75 | 76 | Vediamo qualche esempio, se `M` ammette una istanza di monade allora ammettono una istanza di monade i seguenti tipi: 77 | 78 | - `OptionT = M>` 79 | - `EitherT = M>` 80 | 81 | Più in generale i **monad transformer** sono un elenco di "ricette" specifiche che mostrano come a `M>` può essere associata una istanza di monade quando `M` e `N` ammettono una istanza di monade. 82 | 83 | Per operare comodamente abbiamo bisogno di operazioni che permettono di immergere le computazioni che girano nelle monadi costituenti la monade target: le _lifting_ functions. 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functional-programming", 3 | "version": "2.0.0", 4 | "description": "functional-programming", 5 | "files": [], 6 | "scripts": { 7 | "lint": "tslint -p .", 8 | "build": "tsc -p ./tsconfig.json" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/gcanti/functional-programming.git" 13 | }, 14 | "author": "Giulio Canti ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/gcanti/functional-programming/issues" 18 | }, 19 | "homepage": "https://github.com/gcanti/functional-programming", 20 | "dependencies": { 21 | "@types/react": "^17.0.3", 22 | "fast-check": "^2.12.1", 23 | "fp-ts": "^2.10.4", 24 | "react": "^17.0.2" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^12.6.8", 28 | "prettier": "^2.2.1", 29 | "tslint": "^5.11.0", 30 | "tslint-config-standard": "^8.0.1", 31 | "tslint-etc": "^1.13.9", 32 | "tslint-immutable": "^6.0.1", 33 | "typescript": "^4.2.4" 34 | }, 35 | "tags": [], 36 | "keywords": [] 37 | } 38 | -------------------------------------------------------------------------------- /src/00_pipe_and_flow.ts: -------------------------------------------------------------------------------- 1 | import { pipe, flow } from 'fp-ts/function' 2 | 3 | const double = (n: number): number => n * 2 4 | 5 | const increment = (n: number): number => n + 1 6 | 7 | const decrement = (n: number): number => n - 1 8 | 9 | /* 10 | pipe 연산: 11 | 12 | def program1 (n) do 13 | n 14 | |> increment 15 | |> double 16 | |> decrement 17 | end 18 | 19 | 메소드 체이닝: 20 | 21 | n 22 | .andThen(increment) 23 | .andThen(double) 24 | .andThen(decrement) 25 | */ 26 | const program1 = (n: number): number => pipe(n, increment, double, decrement) 27 | 28 | console.log(program1(10)) // 21 29 | 30 | // const program2: (n: number) => number 31 | const program2 = flow(increment, double, decrement) 32 | 33 | console.log(program2(10)) // 21 34 | -------------------------------------------------------------------------------- /src/01_retry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 성공할 때까지 반복적으로 작업을 수행하는 작업에 대한 추상화입니다. 4 | 5 | 이 모듈은 다음과 같은 3가지 요소로 이루어집니다: 6 | 7 | - the model 8 | - primitives 9 | - combinators 10 | 11 | */ 12 | 13 | // ------------------------------------------------------------------------------------- 14 | // model 15 | // ------------------------------------------------------------------------------------- 16 | 17 | export interface RetryStatus { 18 | /** 반복자, `0` 은 첫 번째 시도를 의미합니다 */ 19 | readonly iterNumber: number 20 | 21 | /** 가장 최근 시도의 지연시간. 첫 번째 시도에서는 항상 `undefined` 입니다 */ 22 | readonly previousDelay: number | undefined 23 | } 24 | 25 | export const startStatus: RetryStatus = { 26 | iterNumber: 0, 27 | previousDelay: undefined 28 | } 29 | 30 | /** 31 | * `RetryPolicy` 는 `RetryStatus` 를 인자로 받고 지연시간을 밀리초로 반환하는 함수입니다. 32 | * 반복자는 0에서 시작하고 각 시도마다 1씩 증가합니다. 33 | * 만약 *undefined* 를 반환했다면 재시도 제한에 도달했다는 것을 의미합니다. 34 | */ 35 | export interface RetryPolicy { 36 | (status: RetryStatus): number | undefined 37 | } 38 | 39 | // ------------------------------------------------------------------------------------- 40 | // primitives 41 | // ------------------------------------------------------------------------------------- 42 | 43 | /** 44 | * 무제한 시도하는 상수시간 지연 45 | */ 46 | export const constantDelay = (delay: number): RetryPolicy => () => delay 47 | 48 | /** 49 | * 최대 `i` 번까지 즉시 재시도 50 | */ 51 | export const limitRetries = (i: number): RetryPolicy => (status) => 52 | status.iterNumber >= i ? undefined : 0 53 | 54 | /** 55 | * 각 시도마다 지연시간이 기하급수적으로 증가한다. 56 | * 지연시간은 2의 거듭제곱으로 증가한다 57 | */ 58 | export const exponentialBackoff = (delay: number): RetryPolicy => (status) => 59 | delay * Math.pow(2, status.iterNumber) 60 | 61 | // ------------------------------------------------------------------------------------- 62 | // combinators 63 | // ------------------------------------------------------------------------------------- 64 | 65 | /** 66 | * 주어진 정책의 최대 지연시간 상한값을 설정합니다. 67 | */ 68 | export const capDelay = (maxDelay: number) => ( 69 | policy: RetryPolicy 70 | ): RetryPolicy => (status) => { 71 | const delay = policy(status) 72 | return delay === undefined ? undefined : Math.min(maxDelay, delay) 73 | } 74 | 75 | /** 76 | * 두 정책을 병합합니다. **Quiz**: 두 정책을 병합한다는 것은 무엇을 의미할까요? 77 | */ 78 | export const concat = (second: RetryPolicy) => ( 79 | first: RetryPolicy 80 | ): RetryPolicy => (status) => { 81 | const delay1 = first(status) 82 | const delay2 = second(status) 83 | if (delay1 !== undefined && delay2 !== undefined) { 84 | return Math.max(delay1, delay2) 85 | } 86 | return undefined 87 | } 88 | 89 | // ------------------------------------------------------------------------------------- 90 | // tests 91 | // ------------------------------------------------------------------------------------- 92 | 93 | /** 94 | * 정책을 상태에 적용하고 그 결과를 확인합니다. 95 | */ 96 | export const applyPolicy = (policy: RetryPolicy) => ( 97 | status: RetryStatus 98 | ): RetryStatus => ({ 99 | iterNumber: status.iterNumber + 1, 100 | previousDelay: policy(status) 101 | }) 102 | 103 | /** 104 | * 정책을 적용하고 중간 결과를 모두 반환합니다. 105 | */ 106 | export const dryRun = (policy: RetryPolicy): ReadonlyArray => { 107 | const apply = applyPolicy(policy) 108 | let status: RetryStatus = apply(startStatus) 109 | const out: Array = [status] 110 | while (status.previousDelay !== undefined) { 111 | out.push((status = apply(out[out.length - 1]))) 112 | } 113 | return out 114 | } 115 | 116 | import { pipe } from 'fp-ts/function' 117 | 118 | /* 119 | constantDelay(300) 120 | |> concat(exponentialBackoff(200)) 121 | |> concat(limitRetries(5)) 122 | |> capDelay(2000) 123 | */ 124 | const myPolicy = pipe( 125 | constantDelay(300), 126 | concat(exponentialBackoff(200)), 127 | concat(limitRetries(5)), 128 | capDelay(2000) 129 | ) 130 | 131 | console.log(dryRun(myPolicy)) 132 | /* 133 | [ 134 | { iterNumber: 1, previousDelay: 300 }, <= constantDelay 135 | { iterNumber: 2, previousDelay: 400 }, <= exponentialBackoff 136 | { iterNumber: 3, previousDelay: 800 }, <= exponentialBackoff 137 | { iterNumber: 4, previousDelay: 1600 }, <= exponentialBackoff 138 | { iterNumber: 5, previousDelay: 2000 }, <= capDelay 139 | { iterNumber: 6, previousDelay: undefined } <= limitRetries 140 | ] 141 | */ 142 | -------------------------------------------------------------------------------- /src/02_ord.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | **Quiz**. Given a type `A` is it possible to define a semigroup instance 4 | for `Ord`. What could it represent? 5 | */ 6 | 7 | import { pipe } from 'fp-ts/function' 8 | import * as O from 'fp-ts/Ord' 9 | import { sort } from 'fp-ts/ReadonlyArray' 10 | import { concatAll, Semigroup } from 'fp-ts/Semigroup' 11 | import * as S from 'fp-ts/string' 12 | import * as N from 'fp-ts/number' 13 | import * as B from 'fp-ts/boolean' 14 | 15 | /* 16 | 17 | first of all let's define a semigroup instance for `Ord` 18 | 19 | */ 20 | 21 | const getSemigroup = (): Semigroup> => ({ 22 | concat: (first, second) => 23 | O.fromCompare((a1, a2) => { 24 | const ordering = first.compare(a1, a2) 25 | return ordering !== 0 ? ordering : second.compare(a1, a2) 26 | }) 27 | }) 28 | 29 | /* 30 | 31 | now let's see it applied to a practical example 32 | 33 | */ 34 | 35 | interface User { 36 | readonly id: number 37 | readonly name: string 38 | readonly age: number 39 | readonly rememberMe: boolean 40 | } 41 | 42 | const byName = pipe( 43 | S.Ord, 44 | O.contramap((_: User) => _.name) 45 | ) 46 | 47 | const byAge = pipe( 48 | N.Ord, 49 | O.contramap((_: User) => _.age) 50 | ) 51 | 52 | const byRememberMe = pipe( 53 | B.Ord, 54 | O.contramap((_: User) => _.rememberMe) 55 | ) 56 | 57 | const SemigroupOrdUser = getSemigroup() 58 | 59 | // represents a table to sort 60 | const users: ReadonlyArray = [ 61 | { id: 1, name: 'Guido', age: 47, rememberMe: false }, 62 | { id: 2, name: 'Guido', age: 46, rememberMe: true }, 63 | { id: 3, name: 'Giulio', age: 44, rememberMe: false }, 64 | { id: 4, name: 'Giulio', age: 44, rememberMe: true } 65 | ] 66 | 67 | // a classic ordering: 68 | // first by name, then by age, then by `rememberMe` 69 | 70 | const byNameAgeRememberMe = concatAll(SemigroupOrdUser)(byName)([ 71 | byAge, 72 | byRememberMe 73 | ]) 74 | pipe(users, sort(byNameAgeRememberMe), console.log) 75 | /* 76 | [ { id: 3, name: 'Giulio', age: 44, rememberMe: false }, 77 | { id: 4, name: 'Giulio', age: 44, rememberMe: true }, 78 | { id: 2, name: 'Guido', age: 46, rememberMe: true }, 79 | { id: 1, name: 'Guido', age: 47, rememberMe: false } ] 80 | */ 81 | 82 | // now I want all the users with 83 | // `rememberMe = true` first 84 | 85 | const byRememberMeNameAge = concatAll(SemigroupOrdUser)( 86 | O.reverse(byRememberMe) 87 | )([byName, byAge]) 88 | pipe(users, sort(byRememberMeNameAge), console.log) 89 | /* 90 | [ { id: 4, name: 'Giulio', age: 44, rememberMe: true }, 91 | { id: 2, name: 'Guido', age: 46, rememberMe: true }, 92 | { id: 3, name: 'Giulio', age: 44, rememberMe: false }, 93 | { id: 1, name: 'Guido', age: 47, rememberMe: false } ] 94 | */ 95 | -------------------------------------------------------------------------------- /src/03_shapes.ts: -------------------------------------------------------------------------------- 1 | // 실행하려면 다음 명령어를 수행하세요 `npm run shapes` 2 | /* 3 | 문제: 캔버스에 도형을 그리는 시스템을 고안하세요. 4 | */ 5 | import { pipe } from 'fp-ts/function' 6 | import { Monoid, concatAll } from 'fp-ts/Monoid' 7 | 8 | // ------------------------------------------------------------------------------------- 9 | // model 10 | // ------------------------------------------------------------------------------------- 11 | 12 | export interface Point { 13 | readonly x: number 14 | readonly y: number 15 | } 16 | 17 | /** 18 | * 도형은 주어진 point 가 도형 안에 포함되면 `ture` 를 반환하고 그렇지 않으면 `false` 를 반환하는 함수입니다 19 | */ 20 | export type Shape = (point: Point) => boolean 21 | 22 | /* 23 | 24 | FFFFFFFFFFFFFFFFFFFFFF 25 | FFFFFFFFFFFFFFFFFFFFFF 26 | FFFFFFFTTTTTTTTFFFFFFF 27 | FFFFFFFTTTTTTTTFFFFFFF 28 | FFFFFFFTTTTTTTTFFFFFFF 29 | FFFFFFFTTTTTTTTFFFFFFF 30 | FFFFFFFFFFFFFFFFFFFFFF 31 | FFFFFFFFFFFFFFFFFFFFFF 32 | 33 | ▧▧▧▧▧▧▧▧ 34 | ▧▧▧▧▧▧▧▧ 35 | ▧▧▧▧▧▧▧▧ 36 | ▧▧▧▧▧▧▧▧ 37 | 38 | */ 39 | 40 | // ------------------------------------------------------------------------------------- 41 | // primitives 42 | // ------------------------------------------------------------------------------------- 43 | 44 | /** 45 | * 원을 표현하는 도형을 만듭니다 46 | */ 47 | export const disk = (center: Point, radius: number): Shape => (point) => 48 | distance(point, center) <= radius 49 | 50 | // 유클리드 거리 51 | const distance = (p1: Point, p2: Point) => 52 | Math.sqrt( 53 | Math.pow(Math.abs(p1.x - p2.x), 2) + Math.pow(Math.abs(p1.y - p2.y), 2) 54 | ) 55 | 56 | // pipe(disk({ x: 200, y: 200 }, 100), draw) 57 | 58 | // ------------------------------------------------------------------------------------- 59 | // combinators 60 | // ------------------------------------------------------------------------------------- 61 | 62 | /** 63 | * 주어진 도형을 반전(부정)시키는 첫 번째 combinator 를 정의할 수 있습니다 64 | */ 65 | export const outside = (s: Shape): Shape => (point) => !s(point) 66 | 67 | // pipe(disk({ x: 200, y: 200 }, 100), outside, draw) 68 | 69 | // ------------------------------------------------------------------------------------- 70 | // instances 71 | // ------------------------------------------------------------------------------------- 72 | 73 | /** 74 | * 두 `도형`의 합집합을 계산하는 `concat` 을 가진 monoid 75 | */ 76 | export const MonoidUnion: Monoid = { 77 | concat: (first, second) => (point) => first(point) || second(point), 78 | empty: () => false 79 | } 80 | 81 | // pipe( 82 | // MonoidUnion.concat( 83 | // disk({ x: 150, y: 200 }, 100), 84 | // disk({ x: 250, y: 200 }, 100) 85 | // ), 86 | // draw 87 | // ) 88 | 89 | /** 90 | * 두 `도형`의 교집합을 계산하는 `concat` 을 가진 monoid 91 | */ 92 | const MonoidIntersection: Monoid = { 93 | concat: (first, second) => (point) => first(point) && second(point), 94 | empty: () => true 95 | } 96 | 97 | // pipe( 98 | // MonoidIntersection.concat( 99 | // disk({ x: 150, y: 200 }, 100), 100 | // disk({ x: 250, y: 200 }, 100) 101 | // ), 102 | // draw 103 | // ) 104 | 105 | /** 106 | * `outside` 와 `MonoidIntersection` 를 사용해 반지를 표현하는 `도형`을 만들 수 있습니다 107 | */ 108 | export const ring = ( 109 | point: Point, 110 | bigRadius: number, 111 | smallRadius: number 112 | ): Shape => 113 | MonoidIntersection.concat( 114 | disk(point, bigRadius), 115 | outside(disk(point, smallRadius)) 116 | ) 117 | 118 | // pipe(ring({ x: 200, y: 200 }, 100, 50), draw) 119 | 120 | export const mickeymouse: ReadonlyArray = [ 121 | disk({ x: 200, y: 200 }, 100), 122 | disk({ x: 130, y: 100 }, 60), 123 | disk({ x: 280, y: 100 }, 60) 124 | ] 125 | 126 | // pipe(concatAll(MonoidUnion)(mickeymouse), draw) 127 | 128 | // ------------------------------------------------------------------------------------- 129 | // utils 130 | // ------------------------------------------------------------------------------------- 131 | 132 | export function draw(shape: Shape): void { 133 | const canvas: HTMLCanvasElement = document.getElementById('canvas') as any 134 | const ctx: CanvasRenderingContext2D = canvas.getContext('2d') as any 135 | const width = canvas.width 136 | const height = canvas.height 137 | const imagedata = ctx.createImageData(width, height) 138 | for (let x = 0; x < width; x++) { 139 | for (let y = 0; y < height; y++) { 140 | const point: Point = { x, y } 141 | if (shape(point)) { 142 | const pixelIndex = (point.y * width + point.x) * 4 143 | imagedata.data[pixelIndex] = 0 144 | imagedata.data[pixelIndex + 1] = 0 145 | imagedata.data[pixelIndex + 2] = 0 146 | imagedata.data[pixelIndex + 3] = 255 147 | } 148 | } 149 | } 150 | ctx.putImageData(imagedata, 0, 0) 151 | } 152 | -------------------------------------------------------------------------------- /src/04_functor.ts: -------------------------------------------------------------------------------- 1 | // https://adrian-salajan.github.io/blog/2021/01/25/images-functor 를 참조 2 | // 실행하려면 `npm run functor` 을 수행하세요 3 | 4 | import { Endomorphism } from 'fp-ts/function' 5 | import * as R from 'fp-ts/Reader' 6 | 7 | // ------------------------------------------------------------------------------------- 8 | // model 9 | // ------------------------------------------------------------------------------------- 10 | 11 | type Color = { 12 | readonly red: number 13 | readonly green: number 14 | readonly blue: number 15 | } 16 | 17 | type Point = { 18 | readonly x: number 19 | readonly y: number 20 | } 21 | 22 | type Image = R.Reader 23 | 24 | // ------------------------------------------------------------------------------------- 25 | // constructors 26 | // ------------------------------------------------------------------------------------- 27 | 28 | const color = (red: number, green: number, blue: number): Color => ({ 29 | red, 30 | green, 31 | blue 32 | }) 33 | 34 | const BLACK: Color = color(0, 0, 0) 35 | 36 | const WHITE: Color = color(255, 255, 255) 37 | 38 | // ------------------------------------------------------------------------------------- 39 | // combinators 40 | // ------------------------------------------------------------------------------------- 41 | 42 | const brightness = (color: Color): number => 43 | (color.red + color.green + color.blue) / 3 44 | 45 | export const grayscale = (c: Color): Color => { 46 | const n = brightness(c) 47 | return color(n, n, n) 48 | } 49 | 50 | export const invert = (c: Color): Color => 51 | color(255 - c.red, 255 - c.green, 255 - c.red) 52 | 53 | // 만약 밝기가 특정 값 V 를 넘으면 하얀색으로, 그렇지 않으면 검정색으로 설정한다 54 | export const threshold = (c: Color): Color => 55 | brightness(c) < 100 ? BLACK : WHITE 56 | 57 | // ------------------------------------------------------------------------------------- 58 | // main 59 | // ------------------------------------------------------------------------------------- 60 | 61 | // `main` 은 transformation function, 즉 `Endomorphism>` 를 파라미터로 호출해야 합니다. 62 | main(R.map((c: Color) => c)) 63 | // main(R.map(grayscale)) 64 | // main(R.map(invert)) 65 | // main(R.map(threshold)) 66 | 67 | // ------------------------------------------------------------------------------------- 68 | // utils 69 | // ------------------------------------------------------------------------------------- 70 | 71 | function main(f: Endomorphism>) { 72 | const canvas: HTMLCanvasElement = document.getElementById('canvas') as any 73 | const ctx: CanvasRenderingContext2D = canvas.getContext('2d') as any 74 | const bird: HTMLImageElement = document.getElementById('bird') as any 75 | bird.onload = function () { 76 | console.log('hello') 77 | function getImage(imageData: ImageData): Image { 78 | const data = imageData.data 79 | return (loc) => { 80 | const pos = loc.x * 4 + loc.y * 632 * 4 81 | return color(data[pos], data[pos + 1], data[pos + 2]) 82 | } 83 | } 84 | 85 | function setImage(imageData: ImageData, image: Image): void { 86 | const data = imageData.data 87 | for (let x = 0; x < 632; x++) { 88 | for (let y = 0; y < 421; y++) { 89 | const pos = x * 4 + y * 632 * 4 90 | const { red, green, blue } = image({ x, y }) 91 | data[pos] = red 92 | data[pos + 1] = green 93 | data[pos + 2] = blue 94 | } 95 | } 96 | ctx.putImageData(imageData, 0, 0) 97 | } 98 | 99 | ctx.drawImage(bird, 0, 0) 100 | const imageData = ctx.getImageData(0, 0, 632, 421) 101 | setImage(imageData, f(getImage(imageData))) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/05_applicative.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 롤플레잉 게임의 주사위 게임의 모델링입니다. 4 | 5 | */ 6 | import { pipe } from 'fp-ts/function' 7 | import * as IO from 'fp-ts/IO' 8 | import { Monoid } from 'fp-ts/Monoid' 9 | import * as R from 'fp-ts/Random' 10 | 11 | // ------------------------------------ 12 | // model 13 | // ------------------------------------ 14 | 15 | export interface Die extends IO.IO {} 16 | 17 | // ------------------------------------ 18 | // constructors 19 | // ------------------------------------ 20 | 21 | export const die = (faces: number): Die => R.randomInt(1, faces) 22 | 23 | // ------------------------------------ 24 | // combinators 25 | // ------------------------------------ 26 | 27 | export const modifier = (n: number) => (die: Die): Die => 28 | pipe( 29 | die, 30 | IO.map((m) => m + n) 31 | ) 32 | 33 | const liftA2 = (f: (a: A) => (b: B) => C) => (fa: IO.IO) => ( 34 | fb: IO.IO 35 | ): IO.IO => pipe(fa, IO.map(f), IO.ap(fb)) 36 | 37 | export const add: ( 38 | second: Die 39 | ) => (first: Die) => Die = liftA2((a: number) => (b: number) => a + b) 40 | 41 | export const multiply = (n: number) => (die: Die): Die => 42 | pipe( 43 | die, 44 | IO.map((m) => m * n) 45 | ) 46 | 47 | // ------------------------------------ 48 | // 인스턴스 49 | // ------------------------------------ 50 | 51 | export const monoidDie: Monoid = { 52 | concat: (first, second) => pipe(first, add(second)), 53 | empty: () => 0 54 | } 55 | 56 | // ------------------------------------ 57 | // 테스트 58 | // ------------------------------------ 59 | 60 | const d6 = die(6) 61 | const d8 = die(8) 62 | 63 | // 2d6 + 1d8 + 2 64 | const _2d6_1d8_2 = pipe(d6, multiply(2), add(d8), modifier(2)) 65 | 66 | console.log(_2d6_1d8_2()) 67 | -------------------------------------------------------------------------------- /src/06_game.ts: -------------------------------------------------------------------------------- 1 | // ==================== 2 | // 숫자 맞추기 3 | // ==================== 4 | 5 | import { pipe } from 'fp-ts/function' 6 | import * as N from 'fp-ts/number' 7 | import * as O from 'fp-ts/Option' 8 | import { between } from 'fp-ts/Ord' 9 | import { randomInt } from 'fp-ts/Random' 10 | import * as T from 'fp-ts/Task' 11 | import { getLine, putStrLn } from './Console' 12 | 13 | // 맞추어야 할 숫자 14 | export const secret: T.Task = T.fromIO(randomInt(1, 100)) 15 | 16 | // combinator: 행동 전에 메시지를 출력 17 | const withMessage = (message: string, next: T.Task): T.Task => 18 | pipe( 19 | putStrLn(message), 20 | T.chain(() => next) 21 | ) 22 | 23 | // 입력값은 문자열이므로 검증해야 한다 24 | const isValidGuess = between(N.Ord)(1, 100) 25 | const parseGuess = (s: string): O.Option => { 26 | const n = parseInt(s, 10) 27 | return isNaN(n) || !isValidGuess(n) ? O.none : O.some(n) 28 | } 29 | 30 | const question: T.Task = withMessage('숫자를 맞춰보세요', getLine) 31 | 32 | const answer: T.Task = pipe( 33 | question, 34 | T.chain((s) => 35 | pipe( 36 | s, 37 | parseGuess, 38 | O.match( 39 | () => withMessage('1 부터 100 사이의 숫자를 넣어주세요', answer), 40 | (a) => T.of(a) 41 | ) 42 | ) 43 | ) 44 | ) 45 | 46 | const check = ( 47 | secret: number, // 맞추어야 할 숫자 48 | guess: number, // 시도 횟수 49 | ok: T.Task, // 맞춘 경우 해야할 일 50 | ko: T.Task // 맞추지 못한 경우 해야할 일 51 | ): T.Task => { 52 | if (guess > secret) { 53 | return withMessage('높아요', ko) 54 | } else if (guess < secret) { 55 | return withMessage('낮아요', ko) 56 | } else { 57 | return ok 58 | } 59 | } 60 | 61 | const end: T.Task = putStrLn('맞추셨습니다!') 62 | 63 | // 함수의 인자로서 상태 (secret) 를 유지합니다 64 | const loop = (secret: number): T.Task => 65 | pipe( 66 | answer, 67 | T.chain((guess) => check(secret, guess, end, loop(secret))) 68 | ) 69 | 70 | const program: T.Task = pipe(secret, T.chain(loop)) 71 | 72 | program() 73 | -------------------------------------------------------------------------------- /src/Console.ts: -------------------------------------------------------------------------------- 1 | import { log } from 'fp-ts/Console' 2 | import { fromIO, Task } from 'fp-ts/Task' 3 | import { createInterface } from 'readline' 4 | 5 | /** reads from the standard input */ 6 | export const getLine: Task = () => 7 | new Promise((resolve) => { 8 | const rl = createInterface({ 9 | input: process.stdin, 10 | output: process.stdout 11 | }) 12 | rl.question('> ', (answer) => { 13 | rl.close() 14 | resolve(answer) 15 | }) 16 | }) 17 | 18 | /** writes to the standard output */ 19 | export const putStrLn = (message: string): Task => fromIO(log(message)) 20 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # 소개 2 | 3 | 이 저장소는 Typescript 와 fp-ts 라이브러리를 활용한 함수형 프로그래밍을 소개합니다. 4 | 5 | 모든 내용은 [enricopolanski](https://github.com/enricopolanski/functional-programming) 의 저장소에서 나온 것입니다. 6 | 7 | 해당 저장소도 이탈리아어로 작성된 [Giulio Canti](https://gcanti.github.io/about.html) 의 ["Introduction to Functional Programming (Italian)"](https://github.com/gcanti/functional-programming) 을 영어로 번역한 것입니다. 8 | 9 | 원본 작성자는 해당 글을 함수형 프로그래밍에 관한 강의나 워크샵에 참고자료로 사용하였습니다. 10 | 11 | 개인적인 공부와 fp-ts 라이브러리 생태계를 소개하기 위한 목적으로 번역하였습니다. 12 | 13 | 오역 및 번역이 매끄럽지 않은 부분이 존재하며 특히 번역이 어려웠던 부분은 원글을 함께 표시하였습니다. 14 | 15 | **Setup** 16 | 17 | ```sh 18 | git clone https://github.com/jbl428/functional-programming.git 19 | cd functional-programming 20 | npm i 21 | ``` 22 | -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 목차 2 | 3 | * [소개](README.md) 4 | 5 | * [함수형 프로그래밍이란](what-is-fp/README.md) 6 | 7 | * [함수형 프로그래밍의 두 가지 요소](two-pillar-of-fp/README.md) 8 | * [참조 투명성](two-pillar-of-fp/referential-transparency.md) 9 | * [합성](two-pillar-of-fp/composition.md) 10 | 11 | * [Semigroup 으로 합성 모델링](semigroup-modeling/README.md) 12 | * [Magma 의 정의](semigroup-modeling/magma.md) 13 | * [Semigroup 의 정의](semigroup-modeling/semigroup.md) 14 | * [concatAll 함수](semigroup-modeling/concat-all.md) 15 | * [Dual semigroup](semigroup-modeling/dual-semigroup.md) 16 | * [Semigroup product](semigroup-modeling/semigroup-product.md) 17 | * [임의의 타입에 대한 semigroup 인스턴스 찾기](semigroup-modeling/find-semigroup.md) 18 | * [Order-derivable Semigroups](semigroup-modeling/order-derivable-semigroup.md) 19 | 20 | * [`Eq` 를 활용한 동등성 모델링](eq-modeling/README.md) 21 | 22 | * [`Ord` 를 활용한 순서 관계 모델링](ord-modeling/README.md) 23 | * [Dual Ordering](ord-modeling/dual-ordering.md) 24 | 25 | * [`Monoid` 를 활용한 합성 모델링](monoid-modeling/README.md) 26 | * [concatAll 함수](monoid-modeling/concat-all.md) 27 | * [product monoid](monoid-modeling/product-monoid.md) 28 | 29 | * [순수함수와 부분함수](pure-and-partial-functions/README.md) 30 | 31 | * [대수적 자료형](algebraic-data-types/README.md) 32 | * [정의](algebraic-data-types/adt.md) 33 | * [곱타입](algebraic-data-types/product-types.md) 34 | * [합타입](algebraic-data-types/sum-types.md) 35 | 36 | * [함수적 오류 처리](functional-error-handling/README.md) 37 | * [Option 타입](functional-error-handling/option.md) 38 | * [Eq 인스턴스](functional-error-handling/eq.md) 39 | * [Semigroup, Monoid 인스턴스](functional-error-handling/semigroup-monoid.md) 40 | * [Either 타입](functional-error-handling/either.md) 41 | 42 | * [Category theory](category-theory/README.md) 43 | * [정의](category-theory/definition.md) 44 | * [프로그래밍 언어 모델링](category-theory/modeling-programming-languages.md) 45 | * [TypeScript](category-theory/typescript.md) 46 | * [합성의 핵심 문제](category-theory/composition-core-problem.md) 47 | 48 | * [Functor](functor/README.md) 49 | * [프로그램으로서의 함수](functor/functions-as-programs.md) 50 | * [Functor 의 경계](functor/boundary-of-functor.md) 51 | * [정의](functor/definition.md) 52 | * [오류 처리](functor/error-handling.md) 53 | * [합성](functor/compose.md) 54 | * [contravariant functor](functor/contravariant-functor.md) 55 | * [fp-ts 에서의 functor](functor/functor-in-fp-ts.md) 56 | * [일반적인 문제 해결](functor/solve-general-problem.md) 57 | 58 | * [Applicative Functor](applicative-functor/README.md) 59 | * [Currying](applicative-functor/Currying.md) 60 | * [ap 연산](applicative-functor/ap.md) 61 | * [of 연산](applicative-functor/of.md) 62 | * [합성](applicative-functor/compose.md) 63 | * [문제 해결](applicative-functor/solve-general-problem.md) 64 | 65 | * [Monad](monad/README.md) 66 | * [중첩된 context 문제](monad/nested-context-problem.md) 67 | * [정의](monad/definition.md) 68 | * [Kleisli Category](monad/kleisli-category.md) 69 | * [단계별 chain 정의](monad/defining-chain.md) 70 | * [프로그램 다루기](monad/manipulating-program.md) 71 | -------------------------------------------------------------------------------- /src/algebraic-data-types/README.md: -------------------------------------------------------------------------------- 1 | # 대수적 자료형 2 | 3 | 응용 프로그램이나 기능을 작성할 때 가장 좋은 첫 번째 단계는 도메인 모델을 정의하는 것입니다. TypeScript 는 이 작업을 수행하는 데 도움이 되는 많은 도구를 제공합니다. **대수적 자료형**(줄여서 ADT)이 이러한 도구 중 하나입니다. 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/algebraic-data-types/adt.md: -------------------------------------------------------------------------------- 1 | ## 정의 2 | 3 | > 컴퓨터 프로그래밍, 특히 함수형 프로그래밍과 타입 이론에서, 대수적 자료형은 복합 타입의 일종이다. 즉, 다른 타입을 조합하여 만든 타입이다. 4 | 5 | 대수적 자료형은 다음 두 개의 유형이 있습니다: 6 | 7 | - **곱타입** 8 | - **합타입** 9 | 10 | ![ADT](../images/adt.png) 11 | 12 | 좀 더 친숙한 곱타입부터 시작해봅시다. 13 | -------------------------------------------------------------------------------- /src/algebraic-data-types/product-types.md: -------------------------------------------------------------------------------- 1 | ## 곱타입 2 | 3 | 곱타입은 집합 `I` 로 색인된 타입 Ti 의 목록입니다: 4 | 5 | ```typescript 6 | type Tuple1 = [string] // I = [0] 7 | type Tuple2 = [string, number] // I = [0, 1] 8 | type Tuple3 = [string, number, boolean] // I = [0, 1, 2] 9 | 10 | // Accessing by index 11 | type Fst = Tuple2[0] // string 12 | type Snd = Tuple2[1] // number 13 | ``` 14 | 15 | 구조체의 경우, `I` 는 label 의 집합입니다: 16 | 17 | ```typescript 18 | // I = {"name", "age"} 19 | interface Person { 20 | name: string 21 | age: number 22 | } 23 | 24 | // label 을 통한 접근 25 | type Name = Person['name'] // string 26 | type Age = Person['age'] // number 27 | ``` 28 | 29 | 곱타입은 **다형적(polymorphic)** 일 수 있습니다. 30 | 31 | **예제** 32 | 33 | ```typescript 34 | // ↓ 타입 파라미터 35 | type HttpResponse = { 36 | readonly code: number 37 | readonly body: A 38 | } 39 | ``` 40 | 41 | ### 왜 "곱"타입 이라 하는가? 42 | 43 | 만약 `A` 의 요소의 개수를 `C(A)` 로 표기한다면 (수학에서는 **cardinality** 로 부릅니다), 다음 방적식은 참입니다: 44 | 45 | ```typescript 46 | C([A, B]) = C(A) * C(B) 47 | ``` 48 | 49 | > 곱의 cardinality 는 각 cardinality 들의 곱과 같습니다 50 | 51 | **예제** 52 | 53 | `null` 타입의 cardinality 는 `1` 입니다. 왜냐하면 `null` 하나의 요소만 가지기 때문입니다. 54 | 55 | **문제**: `boolean` 타입의 cardinality 는 어떻게 될까요? 56 | 57 | **예제** 58 | 59 | ```typescript 60 | type Hour = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 61 | type Period = 'AM' | 'PM' 62 | type Clock = [Hour, Period] 63 | ``` 64 | 65 | `Hour` 타입은 12 개의 요소를 가지고 있습니다. has 12 members. 66 | `Period` 타입은 2 개의 요소를 가지고 있습니다. has 2 members. 67 | 따라서 `Clock` 타입은 `12 * 2 = 24` 개의 요소를 가지고 있습니다. 68 | 69 | **문제**: 다음 `Clock` 타입의 cardinality 는 무엇일까요? 70 | 71 | ```typescript 72 | // 이전과 같음 73 | type Hour = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 74 | // 이전과 같음 75 | type Period = 'AM' | 'PM' 76 | 77 | // 형태만 다름, tuple 타입이 아님 78 | type Clock = { 79 | readonly hour: Hour 80 | readonly period: Period 81 | } 82 | ``` 83 | 84 | ### 언제 곱타입을 쓸 수 있나요? 85 | 86 | 각 구성 요소가 **독립적** 이면 사용할 수 있습니다. 87 | 88 | ```typescript 89 | type Clock = [Hour, Period] 90 | ``` 91 | 92 | 여기서 `Hour` 와 `Period` 는 독립적입니다: `Hour` 값이 `Period` 의 값을 바꾸지 않습니다. 모든 가능한 쌍 `[Hour, Period]` 는 **이치** 에 맞고 올바릅니다. 93 | -------------------------------------------------------------------------------- /src/applicative-functor/Currying.md: -------------------------------------------------------------------------------- 1 | ## Currying 2 | 3 | 우선 타입 `B` 와 `C` (tuple 로 표현할 수 있습니다) 두 개의 인자를 받고 타입 `D` 를 반환하는 함수 모델링이 필요합니다. 4 | 5 | ```typescript 6 | g: (b: B, c: C) => D 7 | ``` 8 | 9 | **currying** 으로 불리는 기법을 사용해 `g` 를 다시 작성할 수 있습니다. 10 | 11 | > Currying 은 여러 개의 인자를 받는 함수의 평가를 각각 하나의 인자를 가진 일련의 함수들의 평가하는 것으로 변환하는 기술입니다. 예를 들어, 두 개의 인자 `B` 와 `C` 를 받아 `D` 를 반환하는 함수를 currying 하면 `B` 하나를 받는 함수로 변환되며 해당 함수는 `C` 를 받아 `D` 를 반환하는 함수를 반환합니다. 12 | 13 | (출처: [wikipedia.org](https://en.wikipedia.org/wiki/Currying)) 14 | 15 | 따라서, currying 을 통해 `g` 를 다음과 같이 작성할 수 있습니다: 16 | 17 | ```typescript 18 | g: (b: B) => (c: C) => D 19 | ``` 20 | 21 | **예제** 22 | 23 | ```typescript 24 | interface User { 25 | readonly id: number 26 | readonly name: string 27 | readonly followers: ReadonlyArray 28 | } 29 | 30 | const addFollower = (follower: User, user: User): User => ({ 31 | ...user, 32 | followers: [...user.followers, follower] 33 | }) 34 | ``` 35 | 36 | Currying 을 통해 `addFollower` 를 개선해봅시다 37 | 38 | ```typescript 39 | interface User { 40 | readonly id: number 41 | readonly name: string 42 | readonly followers: ReadonlyArray 43 | } 44 | 45 | const addFollower = (follower: User) => (user: User): User => ({ 46 | ...user, 47 | followers: [...user.followers, follower] 48 | }) 49 | 50 | // ------------------- 51 | // 사용 예제 52 | // ------------------- 53 | 54 | const user: User = { id: 1, name: 'Ruth R. Gonzalez', followers: [] } 55 | const follower: User = { id: 3, name: 'Marsha J. Joslyn', followers: [] } 56 | 57 | console.log(addFollower(follower)(user)) 58 | /* 59 | { 60 | id: 1, 61 | name: 'Ruth R. Gonzalez', 62 | followers: [ { id: 3, name: 'Marsha J. Joslyn', followers: [] } ] 63 | } 64 | */ 65 | ``` 66 | -------------------------------------------------------------------------------- /src/applicative-functor/README.md: -------------------------------------------------------------------------------- 1 | # Applicative functors 2 | 3 | Functor 섹션에서 effectful 프로그램 인 `f: (a: A) => F` 와 순수함수 `g: (b: B) => C` 를 합성하기 위해 `g` 를 `map(g): (fb: F) => F` 처럼 변형시킨 과정을 살펴보았습니다. (`F` 는 functor 인스턴스) 4 | 5 | | 프로그램 f | 프로그램 g | 합성 | 6 | |-----------|--------------|--------------| 7 | | pure | pure | `g ∘ f` | 8 | | effectful | pure (unary) | `map(g) ∘ f` | 9 | 10 | 하지만 `g` 는 한 개의 파라미터를 받는 unary 함수이어야 합니다. 만약 `g` 가 두 개를 받는다면 어떻게 될까요? 여전히 functor 인스턴스만 가지고 `g` 를 변형할 수 있을까요? 11 | -------------------------------------------------------------------------------- /src/applicative-functor/compose.md: -------------------------------------------------------------------------------- 1 | ## Applicative functors 합성 2 | 3 | Applicative functors 합성이란, 두 applicative functor `F` 와 `G` 에 대해 `F>` 또한 applicative functor 라는 것을 말합니다. 4 | 5 | **예제** (`F = Task`, `G = Option`) 6 | 7 | 합성의 `of` 는 각 `of` 의 합성과 같습니다: 8 | 9 | ```typescript 10 | import { flow } from 'fp-ts/function' 11 | import * as O from 'fp-ts/Option' 12 | import * as T from 'fp-ts/Task' 13 | 14 | type TaskOption = T.Task> 15 | 16 | const of: (a: A) => TaskOption = flow(O.of, T.of) 17 | ``` 18 | 19 | 합성의 `ap` 는 다음 패턴을 활용해 얻을 수 있습니다: 20 | 21 | ```typescript 22 | const ap = ( 23 | fa: TaskOption 24 | ): ((fab: TaskOption<(a: A) => B>) => TaskOption) => 25 | flow( 26 | T.map((gab) => (ga: O.Option) => O.ap(ga)(gab)), 27 | T.ap(fa) 28 | ) 29 | ``` 30 | -------------------------------------------------------------------------------- /src/applicative-functor/of.md: -------------------------------------------------------------------------------- 1 | ## `of` 연산 2 | 3 | 이제 다음 두 함수 `f: (a: A) => F`, `g: (b: B, c: C) => D` 에서 다음 합성 `h` 을 얻을 수 있음을 보았습니다: 4 | 5 | ```typescript 6 | h: (a: A) => (fb: F) => F 7 | ``` 8 | 9 | `h` 를 실행하기 위해 타입 `A` 의 값과 타입 `F` 의 값이 필요합니다. 10 | 11 | 하지만 만약, 두 번째 파라미터 `fb` 를 위한 타입 `F` 의 값 대신 `B` 만 가지고 있다면 어떡할까요? 12 | 13 | `B` 를 `F` 로 바꿔주는 연산이 있다면 유용할 것입니다. 14 | 15 | 이제 그러한 역할을 하는 `of` 로 불리는 연산을 소개합니다 (동의어: **pure**, **return**): 16 | 17 | ```typescript 18 | declare const of: (b: B) => F 19 | ``` 20 | 21 | 보통 `ap` 와 `of` 연산을 가지는 type constructor 에 대해서만 **applicative functors** 라는 용어를 사용합니다. 22 | 23 | 지금까지 살펴본 type constructor 에 대한 `of` 의 정의를 살펴봅시다: 24 | 25 | **예제** (`F = ReadonlyArray`) 26 | 27 | ```typescript 28 | const of = (a: A): ReadonlyArray => [a] 29 | ``` 30 | 31 | **예제** (`F = Option`) 32 | 33 | ```typescript 34 | import * as O from 'fp-ts/Option' 35 | 36 | const of = (a: A): O.Option => O.some(a) 37 | ``` 38 | 39 | **예제** (`F = IO`) 40 | 41 | ```typescript 42 | import { IO } from 'fp-ts/IO' 43 | 44 | const of = (a: A): IO => () => a 45 | ``` 46 | 47 | **예제** (`F = Task`) 48 | 49 | ```typescript 50 | import { Task } from 'fp-ts/Task' 51 | 52 | const of = (a: A): Task => () => Promise.resolve(a) 53 | ``` 54 | 55 | **예제** (`F = Reader`) 56 | 57 | ```typescript 58 | import { Reader } from 'fp-ts/Reader' 59 | 60 | const of = (a: A): Reader => () => a 61 | ``` 62 | 63 | **데모** 64 | 65 | [`05_applicative.ts`](../05_applicative.ts) 66 | -------------------------------------------------------------------------------- /src/applicative-functor/solve-general-problem.md: -------------------------------------------------------------------------------- 1 | ## 이제 applicative functors 로 일반적인 문제를 해결할 수 있나요? 2 | 3 | 아직 아닙니다. 이제 마지막으로 가장 중요한 경우를 고려해야합니다. 두 프로그램 **모두** effectful 할 때 입니다. 4 | 5 | 아직 무언가 더 필요합니다. 다음 장에서 함수형 프로그래밍에서 가장 중요한 추상화 중 하나인 **monad** 를 살펴볼 예정입니다. 6 | -------------------------------------------------------------------------------- /src/category-theory/README.md: -------------------------------------------------------------------------------- 1 | # Category theory 2 | 3 | 지금까지 함수형 프로그래밍의 기초인 **합성** 에 대해 살펴보았습니다. 4 | 5 | > 문제를 어떻게 해결하나요? 우리는 큰 문제를 작은 문제로 분할합니다. 만약 작은 문제들이 여전히 크다면, 더 작게 분할합니다. 마지막으로, 작은 문제들을 해결하는 코드를 작성합니다. 그리고 여기서 프로그래밍의 정수가 등장합니다: 우리는 큰 문제를 해결하기 위해 작은 문제를 다시 합성합니다. 만약 조각을 다시 되돌릴 수 없다면 분할은 아무 의미도 없을 것입니다. - Bartosz Milewski 6 | 7 | 이 말은 정확히 무엇을 의미하는 걸까요? 어떻게 두 조각이 _합성_ 되는지 알 수 있을까요? 두 조각이 _잘_ 합성된다는 것은 어떤 의미일까요? 8 | 9 | > 만약 결합된 엔티티를 변경하지 않고 각각의 행동을 쉽고 일반적으로 결합할 수 있다면 엔티티들은 합성가능하다고 말한다. 나는 재사용성과 프로그래밍 모델에서 간결하게 표현되는 조합적 확장을 달성하기 위한 이루기 위해 가장 중요한 요소가 합성가능성 이라고 생각한다. - Paul Chiusano 10 | 11 | 함수형 스타일로 작성된 프로그램이 파이프라인과 얼마나 유사한지 간략하게 언급하였습니다: 12 | 13 | ```typescript 14 | const program = pipe( 15 | input, 16 | f1, // 순수 함수 17 | f2, // 순수 함수 18 | f3, // 순수 함수 19 | ... 20 | ) 21 | ``` 22 | 23 | 하지만 이런 스타일로 코딩하는 것은 얼마나 간단한 걸까요? 한번 시도해봅시다: 24 | 25 | ```typescript 26 | import { pipe } from 'fp-ts/function' 27 | import * as RA from 'fp-ts/ReadonlyArray' 28 | 29 | const double = (n: number): number => n * 2 30 | 31 | /** 32 | * 프로그램은 주어진 ReadonlyArray 의 첫 번째 요소를 두 배한 값을 반환합니다 33 | */ 34 | const program = (input: ReadonlyArray): number => 35 | pipe( 36 | input, 37 | RA.head, // 컴파일 에러! 타입 'Option' 는 'number' 에 할당할 수 없습니다 38 | double 39 | ) 40 | ``` 41 | 42 | 왜 컴파일 에러가 발생할까요? 왜냐하면 `head` 와 `double` 을 합성할 수 없기 때문입니다. 43 | 44 | ```typescript 45 | head: (as: ReadonlyArray) => Option 46 | double: (n: number) => number 47 | ``` 48 | 49 | `head` 의 공역은 `double` 의 정의역에 포함되지 않습니다. 50 | 51 | 순수함수로 프로그램을 만드려는 우리의 시도는 끝난걸까요? 52 | 53 | 우리는 그러한 근본적인 질문에 답할 수 있는 **엄격한 이론** 을 찾아야 합니다. 54 | 55 | 우리는 합성가능성에 대한 **공식적인 정의** 가 필요합니다. 56 | 57 | 다행히도, 지난 70년 동안, 가장 오래되고 가장 큰 인류의 오픈 소스 프로젝트 (수학) 의 구성원과 많은 수의 연구원들이 합성가능성에 대한 다음 이론을 개발하는데 몰두했습니다. 58 | **category theory**, Saunders Mac Lane along Samuel Eilenberg (1945) 에 의해 시작된 수학의 한 분야. 59 | 60 | > category 는 합성의 본질이라 할 수 있습니다. 61 | 62 | ![Saunders Mac Lane](../images/maclane.jpg) 63 | 64 | ![Samuel Eilenberg](../images/eilenberg.jpg) 65 | 66 | 다음 장에서 어떻게 category 가 다음 기반을 구성할 수 있는지 살펴볼 것입니다: 67 | 68 | - 일반적인 **프로그래밍 언어** 를 위한 모델 69 | - **합성** 의 개념을 위한 모델 70 | -------------------------------------------------------------------------------- /src/category-theory/composition-core-problem.md: -------------------------------------------------------------------------------- 1 | ## 합성의 핵심 문제 2 | 3 | _TS_ category 에서 다음 두 개의 일반적인 함수를 합성할 수 있습니다. `f: (a: A) => B` 와 `g: (c: C) => D`, 여기서 `C = B` 4 | 5 | ```typescript 6 | function flow(f: (a: A) => B, g: (b: B) => C): (a: A) => C { 7 | return (a) => g(f(a)) 8 | } 9 | 10 | function pipe(a: A, f: (a: A) => B, g: (b: B) => C): C { 11 | return flow(f, g)(a) 12 | } 13 | ``` 14 | 15 | 하지만 `B != C` 인 경우에는 어떻게 될까요? 어떻게 그러한 두 함수를 합성할 수 있을까요? 포기해야 할까요? 16 | 17 | 다음 장에서, 어떠한 조건에서 두 함수의 합성이 가능한지 살펴보겠습니다. 18 | 19 | **스포일러** 20 | 21 | - `f: (a: A) => B` 와 `g: (b: B) => C` 의 합성은 일반적인 함수의 합성을 사용합니다 we use our usual function composition 22 | - `f: (a: A) => F` 와 `g: (b: B) => C` 의 합성은 `F` 의 **functor** 인스턴스가 필요합니다 23 | - `f: (a: A) => F` 와 `g: (b: B, c: C) => D` 의 합성은 `F` 의 **applicative functor** 인스턴스가 필요합니다 24 | - `f: (a: A) => F` 와 `g: (b: B) => F` 의 합성은 `F` 의 **monad** 인스턴스가 필요합니다 25 | 26 | The four composition recipes 27 | 28 | 이번 장의 초반에 언급한 문제가 두 번째 상황에 해당하며 `F` 는 `Option` 타입을 의미합니다: 29 | 30 | ```typescript 31 | // A = ReadonlyArray, B = number, F = Option 32 | head: (as: ReadonlyArray) => Option 33 | double: (n: number) => number 34 | ``` 35 | 36 | 문제를 해결하기 위해, 다음 장에서 functor 에 대해 알아봅시다. 37 | -------------------------------------------------------------------------------- /src/category-theory/definition.md: -------------------------------------------------------------------------------- 1 | ## 정의 2 | 3 | category 의 정의는 그렇게 복잡하진 않지만, 조금 길기 때문에 두 부분으로 나누겠습니다: 4 | 5 | - 첫 번째는 단지 기술적인 것입니다 (구성요소를 정의할 필요가 있습니다) 6 | - 두 번째는 우리가 관심있는 합성에 더 연관되어 있습니다 7 | 8 | ### 첫 번째 (구성요소) 9 | 10 | category 는 `(Objects, Morphisms)` 쌍으로 되어있고 각각 다음을 의미합니다: 11 | 12 | - `Objects` 는 **object** 들의 목록입니다 13 | - `Morphisms` 는 **object** 들 간의 **morphisms** 의 목록 ("arrow" 라고도 불립니다) 입니다 14 | 15 | **참고**. "object" 라는 용어는 프로그래밍에서의 "객체" 와는 관련이 없습니다. 단지 "object" 를 확인할 수 없는 블랙박스나, 다양한 morphisms 을 정의하는데 유용한 단순한 placeholder 라고 생각해주세요. 16 | 17 | 모든 morphisms `f` 는 원천 object `A` 와 대상 object `B` 를 가지고 있습니다. 18 | 19 | 모든 morphism 에서, `A` 와 `B` 는 모두 `Ojbects` 의 구성원입니다. 보통 `f: A ⟼ B` 라고 쓰며 "f 는 A 에서 B 로 가는 morphism" 이라 말합니다. 20 | 21 | A morphism 22 | 23 | **참고**. 앞으로는, 단순하게 원은 제외하고 object 에만 라벨을 붙이겠습니다> 24 | 25 | ### 두 번째 (합성) 26 | 27 | 다음 속성을 만족하는 "합성" 이라 불리는 `∘` 연산이 존재합니다: 28 | 29 | - (**morphisms 의 합성**) 모든 임의의 두 morphisms `f: A ⟼ B` 와 `g: B ⟼ C` 에 대해 `f` 와 `g` 의 _합성_ 인 다음 `g ∘ f: A ⟼ C` morphism 이 존재해야 합니다. 30 | 31 | composition 32 | 33 | - (**결합 법칙**) 만약 `f: A ⟼ B`, `g: B ⟼ C` 이고 `h: C ⟼ D` 이면 `h ∘ (g ∘ f) = (h ∘ g) ∘ f` 34 | 35 | associativity 36 | 37 | - (**항등성**) 모든 `X` 의 object 에 대해, 다음 법칙을 만족하는 _identity morphism_ 이라 불리는 morphism `identity: X ⟼ X` 가 존재합니다. 모든 임의의 morphism `f: A ⟼ X` 와 `g: X ⟼ B` 에 대해, `identity ∘ f = f` 와 `g ∘ identity = g` 식을 만족합니다. 38 | 39 | identity 40 | 41 | **예제** 42 | 43 | a simple category 44 | 45 | category 는 매우 단순합니다, 3 개의 objects 와 6 개의 morphism 이 존재합니다 (1A, 1B, 1C 와 `A`, `B`, `C` 에 대한 identity morphism 들 입니다). 46 | -------------------------------------------------------------------------------- /src/category-theory/modeling-programming-languages.md: -------------------------------------------------------------------------------- 1 | ## Category 로 프로그래밍 언어 모델링 2 | 3 | Category 는 **타입이 있는 프로그래밍 언어** 의 단순화된 모델로 볼 수 있습니다. 4 | 5 | - object 는 **타입** 으로 6 | - morphism 은 **함수** 로 7 | - `∘` 을 일반적인 **함수의 합성** 으로 8 | 9 | 다음 다이어그램에서: 10 | 11 | a simple programming language 12 | 13 | 3가지 타입과 6가지 함수를 가진 가상의 (그리고 단순한) 프로그래밍 언어로 생각할 수 있습니다. 14 | 15 | 예를 들면: 16 | 17 | - `A = string` 18 | - `B = number` 19 | - `C = boolean` 20 | - `f = string => number` 21 | - `g = number => boolean` 22 | - `g ∘ f = string => boolean` 23 | 24 | 그리고 다음과 같이 구현할 수 있습니다: 25 | 26 | ```typescript 27 | const idA = (s: string): string => s 28 | 29 | const idB = (n: number): string => n 30 | 31 | const idC = (b: boolean): boolean => b 32 | 33 | const f = (s: string): number => s.length 34 | 35 | const g = (n: number): boolean => n > 2 36 | 37 | // gf = g ∘ f 38 | const gf = (s: string): boolean => g(f(s)) 39 | ``` 40 | -------------------------------------------------------------------------------- /src/category-theory/typescript.md: -------------------------------------------------------------------------------- 1 | ## Category 로 구현한 TypeScript 2 | 3 | TypeScript 언어를 단순화한 _TS_ 라고 불리는 category 를 정의해봅시다: 4 | 5 | - **object** 는 TypeScript 의 타입입니다: `string`, `number`, `ReadonlyArray`, 등... 6 | - **morphism** 은 TypeScript 함수입니다: `(a: A) => B`, `(b: B) => C`, ... 여기서 `A`, `B`, `C`, ... 는 TypeScript 의 타입 7 | - **identity morphism** 은 단일 다형 함수를 의미합니다 `const identity = (a: A): A => a` 8 | - **morphism 의 합성** 은 (결합법칙을 만족하는) 일반적인 함수의 합성입니다 9 | 10 | TypeScript 모델 _TS_ category 는 다소 제한적으로 보일 수 있습니다: 반목문도 없고, `if` 문도 없으며, _대부분_ 기능이 없습니다... 하지만 잘 정의된 합성의 개념을 활용하면 이 모델은 우리가 목표에 도달하는 데 도움이 될 만큼 충분히 풍부하다고 할 수 있습니다. 11 | > (원문) As a model of TypeScript, the _TS_ category may seem a bit limited: no loops, no `if`s, there's _almost_ nothing... that being said that simplified model is rich enough to help us reach our goal: to reason about a well-defined notion of composition. 12 | -------------------------------------------------------------------------------- /src/exercises/ADT01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modellare un albero binario completo, il/i costruttore/i, la funzione di pattern matching 3 | * e una funzione che converte l'albero in un `ReadonlyArray` 4 | */ 5 | export type BinaryTree = unknown 6 | -------------------------------------------------------------------------------- /src/exercises/ADT02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modellare il punteggio di un game di tennis 3 | */ 4 | type Player = 'A' | 'B' 5 | 6 | type Game = unknown 7 | 8 | /** 9 | * Punteggio di partenza 10 | */ 11 | const start: Game = null 12 | 13 | /** 14 | * Dato un punteggio e un giocatore che si è aggiudicato il punto restituisce il nuovo punteggio 15 | */ 16 | declare const win: (player: Player) => (game: Game) => Game 17 | 18 | /** 19 | * Restituisce il punteggio in formato leggibile 20 | */ 21 | declare const show: (game: Game) => string 22 | 23 | // ------------------------------------ 24 | // tests 25 | // ------------------------------------ 26 | 27 | import * as assert from 'assert' 28 | import { pipe } from 'fp-ts/function' 29 | 30 | assert.deepStrictEqual( 31 | pipe(start, win('A'), win('A'), win('A'), win('A'), show), 32 | 'game player A' 33 | ) 34 | 35 | const fifteenAll = pipe(start, win('A'), win('B')) 36 | assert.deepStrictEqual(pipe(fifteenAll, show), '15 - all') 37 | 38 | const fourtyAll = pipe(fifteenAll, win('A'), win('B'), win('A'), win('B')) 39 | assert.deepStrictEqual(pipe(fourtyAll, show), '40 - all') 40 | 41 | const advantageA = pipe(fourtyAll, win('A')) 42 | assert.deepStrictEqual(pipe(advantageA, show), 'advantage player A') 43 | 44 | assert.deepStrictEqual(pipe(advantageA, win('B'), show), 'deuce') 45 | -------------------------------------------------------------------------------- /src/exercises/ADT03.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Rifattorizzare il seguente codice in modo da eliminare l'errore di compilazione. 4 | 5 | */ 6 | import { flow } from 'fp-ts/function' 7 | import { match, Option } from 'fp-ts/Option' 8 | 9 | interface User { 10 | readonly username: string 11 | } 12 | 13 | declare const queryByUsername: (username: string) => Option 14 | 15 | // ------------------------------------- 16 | // model 17 | // ------------------------------------- 18 | 19 | interface HttpResponse { 20 | readonly code: number 21 | readonly body: T 22 | } 23 | 24 | // ------------------------------------- 25 | // API 26 | // ------------------------------------- 27 | 28 | export const getByUsername: ( 29 | username: string 30 | ) => HttpResponse = flow( 31 | queryByUsername, 32 | match( 33 | () => ({ code: 404, body: 'User not found.' }), 34 | // @ts-ignore 35 | (user) => ({ code: 200, body: user }) // Error: Type 'User' is not assignable to type 'string' 36 | ) 37 | ) 38 | -------------------------------------------------------------------------------- /src/exercises/Applicative01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * E' possibile derivare una istanza di `Monoid` da una istanza di `Applicative`? 3 | */ 4 | import { Monoid } from 'fp-ts/Monoid' 5 | import * as S from 'fp-ts/string' 6 | import * as O from 'fp-ts/Option' 7 | 8 | declare const getMonoid: (M: Monoid) => Monoid> 9 | 10 | // ------------------------------------ 11 | // tests 12 | // ------------------------------------ 13 | 14 | import * as assert from 'assert' 15 | 16 | const M = getMonoid(S.Monoid) 17 | 18 | assert.deepStrictEqual(M.concat(O.none, O.none), O.none) 19 | assert.deepStrictEqual(M.concat(O.some('a'), O.none), O.none) 20 | assert.deepStrictEqual(M.concat(O.none, O.some('a')), O.none) 21 | assert.deepStrictEqual(M.concat(O.some('a'), O.some('b')), O.some('ab')) 22 | assert.deepStrictEqual(M.concat(O.some('a'), M.empty), O.some('a')) 23 | assert.deepStrictEqual(M.concat(M.empty, O.some('a')), O.some('a')) 24 | -------------------------------------------------------------------------------- /src/exercises/Apply01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * E' possibile derivare una istanza di `Semigroup` da una istanza di `Apply`? 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as S from 'fp-ts/string' 6 | import * as O from 'fp-ts/Option' 7 | 8 | declare const getSemigroup: (S: Semigroup) => Semigroup> 9 | 10 | // ------------------------------------ 11 | // tests 12 | // ------------------------------------ 13 | 14 | import * as assert from 'assert' 15 | 16 | const SO = getSemigroup(S.Semigroup) 17 | 18 | assert.deepStrictEqual(SO.concat(O.none, O.none), O.none) 19 | assert.deepStrictEqual(SO.concat(O.some('a'), O.none), O.none) 20 | assert.deepStrictEqual(SO.concat(O.none, O.some('a')), O.none) 21 | assert.deepStrictEqual(SO.concat(O.some('a'), O.some('b')), O.some('ab')) 22 | -------------------------------------------------------------------------------- /src/exercises/Eq01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Eq` per `ReadonlyArray` 3 | */ 4 | import { Eq } from 'fp-ts/Eq' 5 | import * as N from 'fp-ts/number' 6 | 7 | declare const getEq: (E: Eq) => Eq> 8 | 9 | // ------------------------------------ 10 | // tests 11 | // ------------------------------------ 12 | 13 | import * as assert from 'assert' 14 | 15 | const E = getEq(N.Eq) 16 | 17 | const as: ReadonlyArray = [1, 2, 3] 18 | 19 | assert.deepStrictEqual(E.equals(as, [1]), false) 20 | assert.deepStrictEqual(E.equals(as, [1, 2]), false) 21 | assert.deepStrictEqual(E.equals(as, [1, 2, 3, 4]), false) 22 | assert.deepStrictEqual(E.equals(as, [1, 2, 3]), true) 23 | -------------------------------------------------------------------------------- /src/exercises/Eq02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Eq` per `Tree` 3 | */ 4 | import { Eq } from 'fp-ts/Eq' 5 | import * as S from 'fp-ts/string' 6 | 7 | type Forest = ReadonlyArray> 8 | 9 | interface Tree { 10 | readonly value: A 11 | readonly forest: Forest 12 | } 13 | 14 | declare const getEq: (E: Eq) => Eq> 15 | 16 | // ------------------------------------ 17 | // tests 18 | // ------------------------------------ 19 | 20 | import * as assert from 'assert' 21 | 22 | const make = (value: A, forest: Forest = []): Tree => ({ 23 | value, 24 | forest 25 | }) 26 | 27 | const E = getEq(S.Eq) 28 | 29 | const t = make('a', [make('b'), make('c')]) 30 | 31 | assert.deepStrictEqual(E.equals(t, make('a')), false) 32 | assert.deepStrictEqual(E.equals(t, make('a', [make('b')])), false) 33 | assert.deepStrictEqual(E.equals(t, make('a', [make('b'), make('d')])), false) 34 | assert.deepStrictEqual( 35 | E.equals(t, make('a', [make('b'), make('c'), make('d')])), 36 | false 37 | ) 38 | assert.deepStrictEqual(E.equals(t, make('a', [make('b'), make('c')])), true) 39 | -------------------------------------------------------------------------------- /src/exercises/Eq03.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modellare un orologio (minuti e ore) 3 | * 4 | * Per completare l'esercizio occorre definire il tipo `Clock`, una sia istanza di `Eq` 5 | */ 6 | import { Eq } from 'fp-ts/Eq' 7 | 8 | // It's a 24 hour clock going from "00:00" to "23:59". 9 | type Clock = unknown 10 | 11 | declare const eqClock: Eq 12 | 13 | // takes an hour and minute, and returns an instance of Clock with those hours and minutes 14 | declare const fromHourMin: (h: number, m: number) => Clock 15 | 16 | // ------------------------------------ 17 | // tests 18 | // ------------------------------------ 19 | 20 | import * as assert from 'assert' 21 | 22 | assert.deepStrictEqual( 23 | eqClock.equals(fromHourMin(0, 0), fromHourMin(24, 0)), 24 | true 25 | ) 26 | assert.deepStrictEqual( 27 | eqClock.equals(fromHourMin(12, 30), fromHourMin(36, 30)), 28 | true 29 | ) 30 | -------------------------------------------------------------------------------- /src/exercises/FEH01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Semigroup` per `Option` 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as S from 'fp-ts/string' 6 | import { Option, some, none } from 'fp-ts/Option' 7 | 8 | declare const getSemigroup: (S: Semigroup) => Semigroup> 9 | 10 | // ------------------------------------ 11 | // tests 12 | // ------------------------------------ 13 | 14 | import * as assert from 'assert' 15 | 16 | const SO = getSemigroup(S.Semigroup) 17 | 18 | assert.deepStrictEqual(SO.concat(none, none), none) 19 | assert.deepStrictEqual(SO.concat(some('a'), none), none) 20 | assert.deepStrictEqual(SO.concat(none, some('b')), none) 21 | assert.deepStrictEqual(SO.concat(some('a'), some('b')), some('ab')) 22 | -------------------------------------------------------------------------------- /src/exercises/FEH02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Monoid` per `Option` 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as O from 'fp-ts/Option' 6 | import { Monoid, concatAll } from 'fp-ts/Monoid' 7 | import * as N from 'fp-ts/number' 8 | 9 | declare const getMonoid: (S: Semigroup) => Monoid> 10 | 11 | // ------------------------------------ 12 | // tests 13 | // ------------------------------------ 14 | 15 | import * as assert from 'assert' 16 | 17 | const M = getMonoid(N.SemigroupSum) 18 | 19 | assert.deepStrictEqual(M.concat(O.none, O.none), O.none) 20 | assert.deepStrictEqual(M.concat(O.some(1), O.none), O.some(1)) 21 | assert.deepStrictEqual(M.concat(O.none, O.some(2)), O.some(2)) 22 | assert.deepStrictEqual(M.concat(O.some(1), O.some(2)), O.some(3)) 23 | assert.deepStrictEqual(M.concat(O.some(1), M.empty), O.some(1)) 24 | assert.deepStrictEqual(M.concat(M.empty, O.some(2)), O.some(2)) 25 | 26 | assert.deepStrictEqual( 27 | concatAll(M)([O.some(1), O.some(2), O.none, O.some(3)]), 28 | O.some(6) 29 | ) 30 | -------------------------------------------------------------------------------- /src/exercises/FEH03.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Semigroup` per `Either` 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as S from 'fp-ts/string' 6 | import { Either, right, left } from 'fp-ts/Either' 7 | 8 | declare const getSemigroup: (S: Semigroup) => Semigroup> 9 | 10 | // ------------------------------------ 11 | // tests 12 | // ------------------------------------ 13 | 14 | import * as assert from 'assert' 15 | 16 | const SE = getSemigroup(S.Semigroup) 17 | 18 | assert.deepStrictEqual(SE.concat(left(1), left(2)), left(1)) 19 | assert.deepStrictEqual(SE.concat(right('a'), left(2)), left(2)) 20 | assert.deepStrictEqual(SE.concat(left(1), right('b')), left(1)) 21 | assert.deepStrictEqual(SE.concat(right('a'), right('b')), right('ab')) 22 | -------------------------------------------------------------------------------- /src/exercises/FEH04.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Semigroup` per `Either` che accumula gli errori 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as S from 'fp-ts/string' 6 | import * as N from 'fp-ts/number' 7 | import { Either, right, left } from 'fp-ts/Either' 8 | 9 | declare const getSemigroup: ( 10 | SE: Semigroup, 11 | SA: Semigroup 12 | ) => Semigroup> 13 | 14 | // ------------------------------------ 15 | // tests 16 | // ------------------------------------ 17 | 18 | import * as assert from 'assert' 19 | 20 | const SE = getSemigroup(N.SemigroupSum, S.Semigroup) 21 | 22 | assert.deepStrictEqual(SE.concat(left(1), left(2)), left(3)) 23 | assert.deepStrictEqual(SE.concat(right('a'), left(2)), left(2)) 24 | assert.deepStrictEqual(SE.concat(left(1), right('b')), left(1)) 25 | assert.deepStrictEqual(SE.concat(right('a'), right('b')), right('ab')) 26 | -------------------------------------------------------------------------------- /src/exercises/FEH05.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convertire la funzione `parseJSON` in stile funzionale usando l'implementazione di `Either` in `fp-ts` 3 | */ 4 | import { right, left } from 'fp-ts/Either' 5 | import { Json } from 'fp-ts/Json' 6 | 7 | // may throw a SyntaxError 8 | export const parseJSON = (input: string): Json => JSON.parse(input) 9 | 10 | // ------------------------------------ 11 | // tests 12 | // ------------------------------------ 13 | 14 | import * as assert from 'assert' 15 | 16 | assert.deepStrictEqual(parseJSON('1'), right(1)) 17 | assert.deepStrictEqual(parseJSON('"a"'), right('a')) 18 | assert.deepStrictEqual(parseJSON('{}'), right({})) 19 | assert.deepStrictEqual(parseJSON('{'), left(new SyntaxError())) 20 | -------------------------------------------------------------------------------- /src/exercises/Functor01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare l'istanza di `Functor` per `IO` 3 | */ 4 | import { URI } from 'fp-ts/IO' 5 | import { Functor1 } from 'fp-ts/Functor' 6 | 7 | const Functor: Functor1 = { 8 | URI, 9 | map: null as any 10 | } 11 | 12 | // ------------------------------------ 13 | // tests 14 | // ------------------------------------ 15 | 16 | import * as assert from 'assert' 17 | 18 | const double = (n: number): number => n * 2 19 | 20 | assert.deepStrictEqual(Functor.map(() => 1, double)(), 2) 21 | -------------------------------------------------------------------------------- /src/exercises/Functor02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare l'istanza di `Functor` per `Either` 3 | */ 4 | import { URI, right, left } from 'fp-ts/Either' 5 | import { Functor2 } from 'fp-ts/Functor' 6 | 7 | const Functor: Functor2 = { 8 | URI, 9 | map: null as any 10 | } 11 | 12 | // ------------------------------------ 13 | // tests 14 | // ------------------------------------ 15 | 16 | import * as assert from 'assert' 17 | 18 | const double = (n: number): number => n * 2 19 | 20 | assert.deepStrictEqual(Functor.map(right(1), double), right(2)) 21 | assert.deepStrictEqual(Functor.map(left('a'), double), left('a')) 22 | -------------------------------------------------------------------------------- /src/exercises/Functor03.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare le seguenti funzioni 3 | */ 4 | import { IO } from 'fp-ts/IO' 5 | 6 | /** 7 | * Returns a random number between 0 (inclusive) and 1 (exclusive). This is a direct wrapper around JavaScript's 8 | * `Math.random()`. 9 | */ 10 | export declare const random: IO 11 | 12 | /** 13 | * Takes a range specified by `low` (the first argument) and `high` (the second), and returns a random integer uniformly 14 | * distributed in the closed interval `[low, high]`. It is unspecified what happens if `low > high`, or if either of 15 | * `low` or `high` is not an integer. 16 | */ 17 | export declare const randomInt: (low: number, high: number) => IO 18 | 19 | /** 20 | * Returns a random element in `as` 21 | */ 22 | export declare const randomElem: (as: ReadonlyArray) => IO 23 | -------------------------------------------------------------------------------- /src/exercises/Magma01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare la funzione `fromReadonlyArray` 3 | */ 4 | import { Magma } from 'fp-ts/Magma' 5 | 6 | declare const fromReadonlyArray: ( 7 | M: Magma 8 | ) => (as: ReadonlyArray) => Readonly> 9 | 10 | // ------------------------------------ 11 | // tests 12 | // ------------------------------------ 13 | 14 | import * as assert from 'assert' 15 | 16 | const magmaSum: Magma = { 17 | concat: (first, second) => first + second 18 | } 19 | 20 | // una istanza di Magma che semplicemente ignora il primo argomento 21 | const lastMagma: Magma = { 22 | concat: (_first, second) => second 23 | } 24 | 25 | // una istanza di Magma che semplicemente ignora il secondo argomento 26 | const firstMagma: Magma = { 27 | concat: (first, _second) => first 28 | } 29 | 30 | const input: ReadonlyArray = [ 31 | ['a', 1], 32 | ['b', 2], 33 | ['a', 3] 34 | ] 35 | 36 | assert.deepStrictEqual(fromReadonlyArray(magmaSum)(input), { a: 4, b: 2 }) 37 | assert.deepStrictEqual(fromReadonlyArray(lastMagma)(input), { a: 3, b: 2 }) 38 | assert.deepStrictEqual(fromReadonlyArray(firstMagma)(input), { a: 1, b: 2 }) 39 | -------------------------------------------------------------------------------- /src/exercises/Monad01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire l'istanza di `Monad` per `Either` 3 | */ 4 | import { Monad2 } from 'fp-ts/Monad' 5 | import * as E from 'fp-ts/Either' 6 | 7 | const Monad: Monad2 = { 8 | URI: E.URI, 9 | map: null as any, 10 | of: null as any, 11 | ap: null as any, 12 | chain: null as any 13 | } 14 | 15 | // ------------------------------------ 16 | // tests 17 | // ------------------------------------ 18 | 19 | import * as assert from 'assert' 20 | 21 | assert.deepStrictEqual( 22 | Monad.map(Monad.of(1), (n: number) => n * 2), 23 | E.right(2) 24 | ) 25 | assert.deepStrictEqual( 26 | Monad.chain(Monad.of(1), (n: number) => 27 | n > 0 ? Monad.of(n * 2) : E.left('error') 28 | ), 29 | E.right(2) 30 | ) 31 | assert.deepStrictEqual( 32 | Monad.chain(Monad.of(-1), (n: number) => 33 | n > 0 ? Monad.of(n * 2) : E.left('error') 34 | ), 35 | E.left('error') 36 | ) 37 | -------------------------------------------------------------------------------- /src/exercises/Monad02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire l'istanza di `Monad` per `type TaskEither = Task>` 3 | */ 4 | import * as T from 'fp-ts/Task' 5 | import * as E from 'fp-ts/Either' 6 | import { Monad2 } from 'fp-ts/Monad' 7 | import { URI } from 'fp-ts/TaskEither' 8 | 9 | const Monad: Monad2 = { 10 | URI: URI, 11 | map: null as any, 12 | of: null as any, 13 | ap: null as any, 14 | chain: null as any 15 | } 16 | 17 | // ------------------------------------ 18 | // tests 19 | // ------------------------------------ 20 | 21 | import * as assert from 'assert' 22 | 23 | async function test() { 24 | assert.deepStrictEqual( 25 | await Monad.map(Monad.of(1), (n: number) => n * 2)(), 26 | E.right(2) 27 | ) 28 | assert.deepStrictEqual( 29 | await Monad.chain(Monad.of(1), (n: number) => 30 | n > 0 ? Monad.of(n * 2) : T.of(E.left('error')) 31 | )(), 32 | E.right(2) 33 | ) 34 | assert.deepStrictEqual( 35 | await Monad.chain(Monad.of(-1), (n: number) => 36 | n > 0 ? Monad.of(n * 2) : T.of(E.left('error')) 37 | )(), 38 | E.left('error') 39 | ) 40 | } 41 | 42 | test() 43 | -------------------------------------------------------------------------------- /src/exercises/Monad03.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convertire il seguente codice in stile funzionale 3 | */ 4 | 5 | interface User { 6 | readonly name: string 7 | } 8 | 9 | // If Page X doesn't have more users it will return empty array ([]) 10 | declare function getUsersByPage(page: number): Promise> 11 | 12 | async function getAllUsers(): Promise> { 13 | let currentUsers: Array = [] 14 | let totalUsers: Array = [] 15 | let page = 0 16 | do { 17 | currentUsers = await getUsersByPage(page) 18 | totalUsers = totalUsers.concat(currentUsers) 19 | page++ 20 | } while (currentUsers.length > 0) 21 | return totalUsers 22 | } 23 | 24 | // ------------------------------------ 25 | // tests 26 | // ------------------------------------ 27 | 28 | import * as assert from 'assert' 29 | import * as E from 'fp-ts/Either' 30 | 31 | async function test() { 32 | assert.deepStrictEqual( 33 | await getAllUsers(), 34 | E.right(['a', 'b', 'a', 'b', 'a', 'b']) 35 | ) 36 | } 37 | 38 | test() 39 | -------------------------------------------------------------------------------- /src/exercises/Monoid01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supponiamo di avere un `ReadonlyArray` ma di non disporre di una istanza di monoide per `A`, 3 | * possiamo sempre mappare la lista e farla diventare di un tipo per il quale abbiamo una istanza. 4 | * 5 | * Questa operazione è realizzata dalla seguente funzione `foldMap` che dovete implementare. 6 | */ 7 | import { Monoid } from 'fp-ts/Monoid' 8 | import * as N from 'fp-ts/number' 9 | 10 | declare const foldMap: ( 11 | M: Monoid 12 | ) => (f: (a: A) => B) => (as: ReadonlyArray) => B 13 | 14 | // ------------------------------------ 15 | // tests 16 | // ------------------------------------ 17 | 18 | import * as assert from 'assert' 19 | import { pipe } from 'fp-ts/function' 20 | 21 | interface Bonifico { 22 | readonly causale: string 23 | readonly importo: number 24 | } 25 | 26 | const bonifici: ReadonlyArray = [ 27 | { causale: 'causale1', importo: 1000 }, 28 | { causale: 'causale2', importo: 500 }, 29 | { causale: 'causale3', importo: 350 } 30 | ] 31 | 32 | // calcola la somma dei bonifici 33 | assert.deepStrictEqual( 34 | pipe( 35 | bonifici, 36 | foldMap(N.MonoidSum)((bonifico) => bonifico.importo) 37 | ), 38 | 1850 39 | ) 40 | -------------------------------------------------------------------------------- /src/exercises/Ord01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Ord` per `ReadonlyArray` 3 | */ 4 | import { Ord } from 'fp-ts/Ord' 5 | import * as N from 'fp-ts/number' 6 | 7 | declare const getOrd: (O: Ord) => Ord> 8 | 9 | // ------------------------------------ 10 | // tests 11 | // ------------------------------------ 12 | 13 | import * as assert from 'assert' 14 | 15 | const O = getOrd(N.Ord) 16 | 17 | assert.deepStrictEqual(O.compare([1], [1]), 0) 18 | assert.deepStrictEqual(O.compare([1], [1, 2]), -1) 19 | assert.deepStrictEqual(O.compare([1, 2], [1]), 1) 20 | assert.deepStrictEqual(O.compare([1, 2], [1, 2]), 0) 21 | assert.deepStrictEqual(O.compare([1, 1], [1, 2]), -1) 22 | assert.deepStrictEqual(O.compare([1, 1], [2]), -1) 23 | -------------------------------------------------------------------------------- /src/exercises/Semigroup01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare la funzione `concatAll` 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as N from 'fp-ts/number' 6 | import * as S from 'fp-ts/string' 7 | 8 | declare const concatAll: ( 9 | M: Semigroup 10 | ) => (startWith: A) => (as: ReadonlyArray) => A 11 | 12 | // ------------------------------------ 13 | // tests 14 | // ------------------------------------ 15 | 16 | import * as assert from 'assert' 17 | 18 | assert.deepStrictEqual(concatAll(N.SemigroupSum)(0)([1, 2, 3, 4]), 10) 19 | assert.deepStrictEqual(concatAll(N.SemigroupProduct)(1)([1, 2, 3, 4]), 24) 20 | assert.deepStrictEqual(concatAll(S.Semigroup)('a')(['b', 'c']), 'abc') 21 | -------------------------------------------------------------------------------- /src/exercises/Semigroup02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire un semigruppo per i predicati su `Point` 3 | */ 4 | import { Predicate } from 'fp-ts/function' 5 | import { Semigroup } from 'fp-ts/Semigroup' 6 | 7 | type Point = { 8 | readonly x: number 9 | readonly y: number 10 | } 11 | 12 | const isPositiveX: Predicate = (p) => p.x >= 0 13 | const isPositiveY: Predicate = (p) => p.y >= 0 14 | 15 | declare const S: Semigroup> 16 | 17 | // ------------------------------------ 18 | // tests 19 | // ------------------------------------ 20 | 21 | import * as assert from 'assert' 22 | 23 | // restituisce `true` se il punto appartiene al primo quadrante, ovvero se ambedue le sue `x` e `y` sono positive 24 | const isPositiveXY = S.concat(isPositiveX, isPositiveY) 25 | 26 | assert.deepStrictEqual(isPositiveXY({ x: 1, y: 1 }), true) 27 | assert.deepStrictEqual(isPositiveXY({ x: 1, y: -1 }), false) 28 | assert.deepStrictEqual(isPositiveXY({ x: -1, y: 1 }), false) 29 | assert.deepStrictEqual(isPositiveXY({ x: -1, y: -1 }), false) 30 | -------------------------------------------------------------------------------- /src/functional-error-handling/README.md: -------------------------------------------------------------------------------- 1 | ## 함수적 오류 처리 2 | 3 | 오류를 어떻게 함수적으로 다루는지 알아봅시다. 4 | 5 | 오류를 반환하거나 던지는 함수는 부분 함수의 예입니다. 6 | 7 | 이전 챕터에서 모든 부분 함수 `f` 는 전체 함수인 `f` 로 만들 수 있음을 보았습니다. 8 | 9 | ``` 10 | f': X ⟶ Option(Y) 11 | ``` 12 | 13 | 이제 TypeScript 에서의 합타입에 대해 알고 있으니 큰 문제없이 `Option` 을 정의할 수 있습니다. 14 | -------------------------------------------------------------------------------- /src/functional-error-handling/either.md: -------------------------------------------------------------------------------- 1 | ### `Either` 타입 2 | 3 | 계산의 실패나 오류를 던지는 부분 함수를 다루기 위해 어떻게 `Option` 자료형을 활용하는지 살펴보았습니다. 4 | 5 | 하미잔 이 자료형은 어떤 상황에서는 제한적일 수 있습니다. 성공하는 경우 `A` 의 정보를 포함한 `Some` 을 얻지만 `None` 은 어떠한 데이터도 가지고 있지 않습니다. 즉 실패했다는 것은 알지만 그 이유를 알 수 없습니다. 6 | 7 | 이를 해결하기 위해 실패를 표현하는 새로운 자료형이 필요하며 이를 `Left` 로 부르겠습니다. 또한 `Some` 도 `Right` 로 변경됩니다. 8 | 9 | ```typescript 10 | // 실패를 표현 11 | interface Left { 12 | readonly _tag: 'Left' 13 | readonly left: E 14 | } 15 | 16 | // 성공을 표현 17 | interface Right { 18 | readonly _tag: 'Right' 19 | readonly right: A 20 | } 21 | 22 | type Either = Left | Right 23 | ``` 24 | 25 | 생성자와 패턴 매칭은 다음과 같습니다: 26 | 27 | ```typescript 28 | const left = (left: E): Either => ({ _tag: 'Left', left }) 29 | 30 | const right = (right: A): Either => ({ _tag: 'Right', right }) 31 | 32 | const match = (onLeft: (left: E) => R, onRight: (right: A) => R) => ( 33 | fa: Either 34 | ): R => { 35 | switch (fa._tag) { 36 | case 'Left': 37 | return onLeft(fa.left) 38 | case 'Right': 39 | return onRight(fa.right) 40 | } 41 | } 42 | ``` 43 | 44 | 이전 callback 예제로 돌아가봅시다: 45 | 46 | ```typescript 47 | declare function readFile( 48 | path: string, 49 | callback: (err?: Error, data?: string) => void 50 | ): void 51 | 52 | readFile('./myfile', (err, data) => { 53 | let message: string 54 | if (err !== undefined) { 55 | message = `Error: ${err.message}` 56 | } else if (data !== undefined) { 57 | message = `Data: ${data.trim()}` 58 | } else { 59 | // should never happen 60 | message = 'The impossible happened' 61 | } 62 | console.log(message) 63 | }) 64 | ``` 65 | 66 | 이제 다음과 같이 signature 를 변경할 수 있습니다: 67 | 68 | ```typescript 69 | declare function readFile( 70 | path: string, 71 | callback: (result: Either) => void 72 | ): void 73 | ``` 74 | 75 | 그리고 다음과 같이 사용합니다: 76 | 77 | ```typescript 78 | readFile('./myfile', (e) => 79 | pipe( 80 | e, 81 | match( 82 | (err) => `Error: ${err.message}`, 83 | (data) => `Data: ${data.trim()}` 84 | ), 85 | console.log 86 | ) 87 | ) 88 | ``` 89 | -------------------------------------------------------------------------------- /src/functional-error-handling/eq.md: -------------------------------------------------------------------------------- 1 | ### `Eq` 인스턴스 2 | 3 | 두 개의 `Option` 타입을 가지고 있고 두 값이 동일한지 확인하고 싶다고 해봅시다: 4 | 5 | ```typescript 6 | import { pipe } from 'fp-ts/function' 7 | import { match, Option } from 'fp-ts/Option' 8 | 9 | declare const o1: Option 10 | declare const o2: Option 11 | 12 | const result: boolean = pipe( 13 | o1, 14 | match( 15 | // o1 이 none 인 경우 16 | () => 17 | pipe( 18 | o2, 19 | match( 20 | // o2 가 none 인 경우 21 | () => true, 22 | // o2 가 some 인 경우 23 | () => false 24 | ) 25 | ), 26 | // o1 이 some 인 경우 27 | (s1) => 28 | pipe( 29 | o2, 30 | match( 31 | // o2 가 none 인 경우 32 | () => false, 33 | // o2 가 some 인 경우 34 | (s2) => s1 === s2 // 여기서 엄격한 동등을 사용합니다 35 | ) 36 | ) 37 | ) 38 | ) 39 | ``` 40 | 41 | 만약 두 개의 `Option` 가 있다면 어떻게 될까요? 아마 위와 비슷한 코드를 또 작성하는건 번거로울 것입니다. 결국 차이가 발생하는 부분은 `Option` 에 포함된 두 값을 비교하는 방법이라 할 수 있습니다. 42 | 43 | 따라서 사용자에게 `A` 에 대한 `Eq` 인스턴스를 받아 `Option` 에 대한 `Eq` 인스턴스를 만드는 방법으로 일반화 할 수 있습니다. 44 | 45 | 다른 말로 하자면 우리는 `getEq` **combinator** 를 정의할 수 있습니다: 임의의 `Eq` 를 받아 `Eq>` 를 반환합니다: 46 | 47 | ```typescript 48 | import { Eq } from 'fp-ts/Eq' 49 | import { pipe } from 'fp-ts/function' 50 | import { match, Option, none, some } from 'fp-ts/Option' 51 | 52 | export const getEq = (E: Eq): Eq> => ({ 53 | equals: (first, second) => 54 | pipe( 55 | first, 56 | match( 57 | () => 58 | pipe( 59 | second, 60 | match( 61 | () => true, 62 | () => false 63 | ) 64 | ), 65 | (a1) => 66 | pipe( 67 | second, 68 | match( 69 | () => false, 70 | (a2) => E.equals(a1, a2) // 여기서 `A` 를 비교합니다 71 | ) 72 | ) 73 | ) 74 | ) 75 | }) 76 | 77 | import * as S from 'fp-ts/string' 78 | 79 | const EqOptionString = getEq(S.Eq) 80 | 81 | console.log(EqOptionString.equals(none, none)) // => true 82 | console.log(EqOptionString.equals(none, some('b'))) // => false 83 | console.log(EqOptionString.equals(some('a'), none)) // => false 84 | console.log(EqOptionString.equals(some('a'), some('b'))) // => false 85 | console.log(EqOptionString.equals(some('a'), some('a'))) // => true 86 | ``` 87 | 88 | `Option` 타입에 대한 `Eq` 인스턴스를 정의함으로 얻게되는 가장 좋은 점은 이전에 보았던 `Eq` 에 대한 모든 조합에 대해 응용할 수 있다는 점입니다. 89 | > (원문) The best thing about being able to define an `Eq` instance for a type `Option` is being able to leverage all of the combiners we've seen previously for `Eq`. 90 | 91 | **예제**: 92 | 93 | `Option` 타입에 대한 `Eq` 인스턴스: 94 | 95 | ```typescript 96 | import { tuple } from 'fp-ts/Eq' 97 | import * as N from 'fp-ts/number' 98 | import { getEq, Option, some } from 'fp-ts/Option' 99 | import * as S from 'fp-ts/string' 100 | 101 | type MyTuple = readonly [string, number] 102 | 103 | const EqMyTuple = tuple(S.Eq, N.Eq) 104 | 105 | const EqOptionMyTuple = getEq(EqMyTuple) 106 | 107 | const o1: Option = some(['a', 1]) 108 | const o2: Option = some(['a', 2]) 109 | const o3: Option = some(['b', 1]) 110 | 111 | console.log(EqOptionMyTuple.equals(o1, o1)) // => true 112 | console.log(EqOptionMyTuple.equals(o1, o2)) // => false 113 | console.log(EqOptionMyTuple.equals(o1, o3)) // => false 114 | ``` 115 | 116 | 위 코드에서 import 부분만 살짝 바꾸면 `Ord` 에 대한 비슷한 결과를 얻을 수 있습니다: 117 | 118 | ```typescript 119 | import * as N from 'fp-ts/number' 120 | import { getOrd, Option, some } from 'fp-ts/Option' 121 | import { tuple } from 'fp-ts/Ord' 122 | import * as S from 'fp-ts/string' 123 | 124 | type MyTuple = readonly [string, number] 125 | 126 | const OrdMyTuple = tuple(S.Ord, N.Ord) 127 | 128 | const OrdOptionMyTuple = getOrd(OrdMyTuple) 129 | 130 | const o1: Option = some(['a', 1]) 131 | const o2: Option = some(['a', 2]) 132 | const o3: Option = some(['b', 1]) 133 | 134 | console.log(OrdOptionMyTuple.compare(o1, o1)) // => 0 135 | console.log(OrdOptionMyTuple.compare(o1, o2)) // => -1 136 | console.log(OrdOptionMyTuple.compare(o1, o3)) // => -1 137 | ``` 138 | -------------------------------------------------------------------------------- /src/functional-error-handling/option.md: -------------------------------------------------------------------------------- 1 | ### `Option` 타입 2 | 3 | `Option` 타입은 실패하거나 (`None` 의 경우) `A` 타입을 반환하는 (`Some` 의 경우) 계산의 효과를 표현합니다. 4 | 5 | ```typescript 6 | // 실패를 표현합니다 7 | interface None { 8 | readonly _tag: 'None' 9 | } 10 | 11 | // 성공을 표현합니다 12 | interface Some { 13 | readonly _tag: 'Some' 14 | readonly value: A 15 | } 16 | 17 | type Option = None | Some 18 | ``` 19 | 20 | 생성자와 패턴 매칭은 다음과 같습니다: 21 | 22 | ```typescript 23 | const none: Option = { _tag: 'None' } 24 | 25 | const some = (value: A): Option => ({ _tag: 'Some', value }) 26 | 27 | const match = (onNone: () => R, onSome: (a: A) => R) => ( 28 | fa: Option 29 | ): R => { 30 | switch (fa._tag) { 31 | case 'None': 32 | return onNone() 33 | case 'Some': 34 | return onSome(fa.value) 35 | } 36 | } 37 | ``` 38 | 39 | `Option` 타입은 오류를 던지는 것을 방지하거나 선택적인 값을 표현하는데 사용할 수 있습니다, 따라서 다음과 같은 코드를 개선할 수 있습니다: 40 | 41 | ```typescript 42 | // 거짓말 입니다 ↓ 43 | const head = (as: ReadonlyArray): A => { 44 | if (as.length === 0) { 45 | throw new Error('Empty array') 46 | } 47 | return as[0] 48 | } 49 | 50 | let s: string 51 | try { 52 | s = String(head([])) 53 | } catch (e) { 54 | s = e.message 55 | } 56 | ``` 57 | 58 | 위 타입 시스템은 실패의 가능성을 무시하고 있습니다. 59 | 60 | ```typescript 61 | import { pipe } from 'fp-ts/function' 62 | 63 | // ↓ 타입 시스템은 계산이 실패할 수 있음을 "표현"합니다 64 | const head = (as: ReadonlyArray): Option => 65 | as.length === 0 ? none : some(as[0]) 66 | 67 | declare const numbers: ReadonlyArray 68 | 69 | const result = pipe( 70 | head(numbers), 71 | match( 72 | () => 'Empty array', 73 | (n) => String(n) 74 | ) 75 | ) 76 | ``` 77 | 78 | 위 예제는 **에러 발생 가능성이 타입 시스템에 들어있습니다**. 79 | 80 | 만약 `Option` 의 `value` 값을 검증없이 접근하는 경우, 타입 시스템이 오류의 가능성을 알려줍니다: 81 | 82 | ```typescript 83 | declare const numbers: ReadonlyArray 84 | 85 | const result = head(numbers) 86 | result.value // type checker 오류: 'value' 프로퍼티가 'Option' 에 존재하지 않습니다 87 | ``` 88 | 89 | `Option` 에 들어있는 값을 접근할 수 있는 유일한 방법은 `match` 함수를 사용해 실패 상황도 같이 처리하는 것입니다. 90 | 91 | ```typescript 92 | pipe(result, match( 93 | () => ...오류 처리... 94 | (n) => ...이후 비즈니스 로직을 처리합니다... 95 | )) 96 | ``` 97 | 98 | 이전 챕터에서 보았던 추상화에 대한 인스턴스틀 정의할 수 있을까요? `Eq` 부터 시작해봅시다. 99 | -------------------------------------------------------------------------------- /src/functional-error-handling/semigroup-monoid.md: -------------------------------------------------------------------------------- 1 | ### `Semigroup`, `Monoid` 인스턴스 2 | 3 | 이제, 두 개의 다른 `Option` 를 "병합" 한다고 가정합시다: 다음과 같은 네 가지 경우가 있습니다: 4 | 5 | | x | y | concat(x, y) | 6 | | ------- | ------- | ------------ | 7 | | none | none | none | 8 | | some(a) | none | none | 9 | | none | some(a) | none | 10 | | some(a) | some(b) | ? | 11 | 12 | 마지막 조건에서 문제가 발생합니다. 두 개의 `A` 를 "병합" 하는 방법이 필요합니다. 13 | 14 | 그런 방법이 있다면 좋을텐데... 그러고보니 우리의 옛 친구 `Semigroup` 이 하는 일 아닌가요? 15 | 16 | | x | y | concat(x, y) | 17 | | -------- | -------- | ---------------------- | 18 | | some(a1) | some(a2) | some(S.concat(a1, a2)) | 19 | 20 | 이제 우리가 할 일은 사용자로부터 `A` 에 대한 `Semigroup` 을 받고 `Option` 에 대한 `Semigroup` 인스턴스를 만드는 것입니다. 21 | 22 | ```typescript 23 | // 구현은 연습문제로 남겨두겠습니다 24 | declare const getApplySemigroup: (S: Semigroup) => Semigroup> 25 | ``` 26 | 27 | **문제**. 위 semigroup 을 monoid 로 만들기 위한 항등원을 추가할 수 있을까요? 28 | 29 | ```typescript 30 | // 구현은 연습문제로 남겨두겠습니다 31 | declare const getApplicativeMonoid: (M: Monoid) => Monoid> 32 | ``` 33 | 34 | 다음과 같이 동작하는 `Option` 의 monoid 인스턴스를 정의할 수 있습니다: 35 | 36 | | x | y | concat(x, y) | 37 | | -------- | -------- | ---------------------- | 38 | | none | none | none | 39 | | some(a1) | none | some(a1) | 40 | | none | some(a2) | some(a2) | 41 | | some(a1) | some(a2) | some(S.concat(a1, a2)) | 42 | 43 | ```typescript 44 | // 구현은 연습문제로 남겨두겠습니다 45 | declare const getMonoid: (S: Semigroup) => Monoid> 46 | ``` 47 | 48 | **문제**. 이 monoid 의 `empty` 멤버는 무엇일까요? 49 | 50 | **예제** 51 | 52 | `getMonoid` 를 사용해 다음 두 개의 유용한 monoid 을 얻을 수 있습니다: 53 | 54 | (가장 왼쪽에 있는 `None` 이 아닌 값을 반환하는 Monoid) 55 | 56 | | x | y | concat(x, y) | 57 | | -------- | -------- | ------------ | 58 | | none | none | none | 59 | | some(a1) | none | some(a1) | 60 | | none | some(a2) | some(a2) | 61 | | some(a1) | some(a2) | some(a1) | 62 | 63 | ```typescript 64 | import { Monoid } from 'fp-ts/Monoid' 65 | import { getMonoid, Option } from 'fp-ts/Option' 66 | import { first } from 'fp-ts/Semigroup' 67 | 68 | export const getFirstMonoid = (): Monoid> => 69 | getMonoid(first()) 70 | ``` 71 | 72 | (가장 오른쪽에 있는 `None` 이 아닌 값을 반환하는 Monoid) 73 | 74 | | x | y | concat(x, y) | 75 | | -------- | -------- | ------------ | 76 | | none | none | none | 77 | | some(a1) | none | some(a1) | 78 | | none | some(a2) | some(a2) | 79 | | some(a1) | some(a2) | some(a2) | 80 | 81 | ```typescript 82 | import { Monoid } from 'fp-ts/Monoid' 83 | import { getMonoid, Option } from 'fp-ts/Option' 84 | import { last } from 'fp-ts/Semigroup' 85 | 86 | export const getLastMonoid = (): Monoid> => 87 | getMonoid(last()) 88 | ``` 89 | 90 | **Example** 91 | 92 | `getLastMonoid` 는 선택적인 값을 관리하는데 유용합니다. 다음 VSCode 텍스트 편집기의 사용자 설정을 알아내는 예제를 살펴봅시다. 93 | 94 | ```typescript 95 | import { Monoid, struct } from 'fp-ts/Monoid' 96 | import { getMonoid, none, Option, some } from 'fp-ts/Option' 97 | import { last } from 'fp-ts/Semigroup' 98 | 99 | /** VSCode 설정 */ 100 | interface Settings { 101 | /** 글꼴 조정 */ 102 | readonly fontFamily: Option 103 | /** 픽셀 단위의 글꼴 크기 조정 */ 104 | readonly fontSize: Option 105 | /** 일정 개수의 열로 표현하기 위해 minimap 의 길이를 제한 */ 106 | readonly maxColumn: Option 107 | } 108 | 109 | const monoidSettings: Monoid = struct({ 110 | fontFamily: getMonoid(last()), 111 | fontSize: getMonoid(last()), 112 | maxColumn: getMonoid(last()) 113 | }) 114 | 115 | const workspaceSettings: Settings = { 116 | fontFamily: some('Courier'), 117 | fontSize: none, 118 | maxColumn: some(80) 119 | } 120 | 121 | const userSettings: Settings = { 122 | fontFamily: some('Fira Code'), 123 | fontSize: some(12), 124 | maxColumn: none 125 | } 126 | 127 | /** userSettings 은 workspaceSettings 을 덮어씌웁니다 */ 128 | console.log(monoidSettings.concat(workspaceSettings, userSettings)) 129 | /* 130 | { fontFamily: some("Fira Code"), 131 | fontSize: some(12), 132 | maxColumn: some(80) } 133 | */ 134 | ``` 135 | 136 | **문제**. 만약 VSCode 가 한 줄당 `80` 개 이상의 열을 관리할 수 없다고 가정해봅시다. 그렇다면 `monoidSettings` 을 어떻게 수정하면 이 제한사항을 반영할 수 있을까요? 137 | -------------------------------------------------------------------------------- /src/functor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/functor/README.md: -------------------------------------------------------------------------------- 1 | # Functors 2 | 3 | 이전 섹션에서 _TS_ category (TypeScript category) 와 함수 합성의 핵심 문제에 대해 살펴보았습니다: 4 | 5 | > 어떻게 두 일반적인 함수 `f: (a: A) => B` 와 `g: (c: C) => D` 를 합성할 수 있을까요? 6 | 7 | 왜 이 문제의 해결법을 찾는게 그렇게 중요할까요? 8 | 9 | 왜냐하면, 만약 category 로 프로그래밍 언어를 모델링하는데 사용할 수 있다면, morphism (_TS_ category 에서 함수) 을 활용해 **프로그램** 을 모델링할 수 있기 때문입니다. 10 | 11 | 따라서, 이 추상적인 문제를 푸는 것은 **일반적인 방법으로 프로그램을 합성** 하는 구체적인 방법을 찾는 것을 의미합니다. 그리고 _이것은_ 우리 개발자들에게 정말 흥미로운 일입니다, 그렇지 않나요? 12 | -------------------------------------------------------------------------------- /src/functor/compose.md: -------------------------------------------------------------------------------- 1 | ## Functor 합성 2 | 3 | Functor 합성이란, 주어진 두 개의 functor `F` 와 `G` 를 합성한 `F>` 를 말하며 이 또한 functor 입니다. 여기서 합성한 functor 의 `map` 함수는 각 functor 의 `map` 함수의 합성입니다. 4 | 5 | **예제** (`F = Task`, `G = Option`) 6 | 7 | ```typescript 8 | import { flow } from 'fp-ts/function' 9 | import * as O from 'fp-ts/Option' 10 | import * as T from 'fp-ts/Task' 11 | 12 | type TaskOption = T.Task> 13 | 14 | export const map: ( 15 | f: (a: A) => B 16 | ) => (fa: TaskOption) => TaskOption = flow(O.map, T.map) 17 | 18 | // ------------------- 19 | // 사용 예제 20 | // ------------------- 21 | 22 | interface User { 23 | readonly id: number 24 | readonly name: string 25 | } 26 | 27 | // 더미 원격 데이터베이스 28 | const database: Record = { 29 | 1: { id: 1, name: 'Ruth R. Gonzalez' }, 30 | 2: { id: 2, name: 'Terry R. Emerson' }, 31 | 3: { id: 3, name: 'Marsha J. Joslyn' } 32 | } 33 | 34 | const getUser = (id: number): TaskOption => () => 35 | Promise.resolve(O.fromNullable(database[id])) 36 | const getName = (user: User): string => user.name 37 | 38 | // getUserName: number -> TaskOption 39 | const getUserName = flow(getUser, map(getName)) 40 | 41 | getUserName(1)().then(console.log) // => some('Ruth R. Gonzalez') 42 | getUserName(4)().then(console.log) // => none 43 | ``` 44 | -------------------------------------------------------------------------------- /src/functor/contravariant-functor.md: -------------------------------------------------------------------------------- 1 | ## Contravariant Functors 2 | 3 | 사실 이전 장에서 우리는 정의를 완전히 철저히 따지지 않았습니다. 이전 장에서 "functor" 는 **covariant (공변) functor** 라고 부르는게 적절합니다. 4 | 5 | 이번 장에서 우리는 또 다른 functor 인 **contravariant (반변)** functor 를 살펴보려 합니다. 6 | 7 | contravariant functor 의 정의는 기본 연산의 시그니쳐를 제외하면 covariant 와 거의 동일합니다. 해당 연산은 `map` 보다는 `contramap` 으로 불립니다. 8 | 9 | contramap 10 | 11 | **예제** 12 | 13 | ```typescript 14 | import { map } from 'fp-ts/Option' 15 | import { contramap } from 'fp-ts/Eq' 16 | 17 | type User = { 18 | readonly id: number 19 | readonly name: string 20 | } 21 | 22 | const getId = (_: User): number => _.id 23 | 24 | //`map` 이 동작하는 방식입니다 25 | // const getIdOption: (fa: Option) => Option 26 | const getIdOption = map(getId) 27 | 28 | // `contramap` 이 동작하는 방식입니다 29 | // const getIdEq: (fa: Eq) => Eq 30 | const getIdEq = contramap(getId) 31 | 32 | import * as N from 'fp-ts/number' 33 | 34 | const EqID = getIdEq(N.Eq) 35 | 36 | /* 37 | 38 | 이전 `Eq` 챕터에서 확인한 내용입니다: 39 | 40 | const EqID: Eq = pipe( 41 | N.Eq, 42 | contramap((_: User) => _.id) 43 | ) 44 | */ 45 | ``` 46 | -------------------------------------------------------------------------------- /src/functor/definition.md: -------------------------------------------------------------------------------- 1 | ## 정의 2 | 3 | functor 는 다음을 만족하는 쌍 `(F, map)` 입니다: 4 | 5 | - `F` 는 한 개 이상의 파라미터를 가지는 type constructor 로 모든 타입 `X` 를 `F` 로 매핑합니다 (**object 간의 매핑**) 6 | - `map` 은 다음 시그니쳐를 가진 함수입니다: 7 | 8 | ```typescript 9 | map: (f: (a: A) => B) => ((fa: F) => F) 10 | ``` 11 | 12 | 이 함수는 모든 함수 `f: (a: A) => B` 를 `map(f): (fa: F) => F` 로 매핑합니다 (**morphism 간의 매핑**) 13 | 14 | 다음 두 성질을 만족해야 합니다: 15 | 16 | - `map(1`X`)` = `1`F(X) (**항등원은 항등원으로 매핑됩니다**) 17 | - `map(g ∘ f) = map(g) ∘ map(f)` (**합성의 상(image)는 상들의 합성과 동일합니다**) 18 | 19 | 두 번째 법칙은 다음과 같은 상황에서 계산을 최적화할 때 사용할 수 있습니다: 20 | 21 | ```typescript 22 | import { flow, increment, pipe } from 'fp-ts/function' 23 | import { map } from 'fp-ts/ReadonlyArray' 24 | 25 | const double = (n: number): number => n * 2 26 | 27 | // 배열을 두 번 순회합니다 28 | console.log(pipe([1, 2, 3], map(double), map(increment))) // => [ 3, 5, 7 ] 29 | 30 | // 배열을 한 번 순회합니다 31 | console.log(pipe([1, 2, 3], map(flow(double, increment)))) // => [ 3, 5, 7 ] 32 | ``` 33 | -------------------------------------------------------------------------------- /src/functor/error-handling.md: -------------------------------------------------------------------------------- 1 | ## Functor 와 함수형 에러처리 2 | 3 | Functor 는 함수형 에러처리에 긍적적인 효과를 발휘합니다: 다음 실용적인 예제를 살펴봅시다: 4 | 5 | ```typescript 6 | declare const doSomethingWithIndex: (index: number) => string 7 | 8 | export const program = (ns: ReadonlyArray): string => { 9 | // -1 는 원하는 요소가 없다는 것을 의미합니다 10 | const i = ns.findIndex((n) => n > 0) 11 | if (i !== -1) { 12 | return doSomethingWithIndex(i) 13 | } 14 | throw new Error('양수를 찾을 수 없습니다') 15 | } 16 | ``` 17 | 18 | 기본 `findIndex` API 를 사용하면 결과가 `-1` 이 아닌지 확인해야 하는 `if` 문을 무조건 사용해야합니다. 만약 깜박한 경우, `doSomethingWithIndex` 에 의도하지 않은 입력인 `-1` 을 전달할 수 있습니다. 19 | 20 | `Option` 과 해당 functor 인스턴스를 사용해 동일한 결과를 얻는 것이 얼마나 쉬운지 살펴봅시다: 21 | 22 | ```typescript 23 | import { pipe } from 'fp-ts/function' 24 | import { map, Option } from 'fp-ts/Option' 25 | import { findIndex } from 'fp-ts/ReadonlyArray' 26 | 27 | declare const doSomethingWithIndex: (index: number) => string 28 | 29 | export const program = (ns: ReadonlyArray): Option => 30 | pipe( 31 | ns, 32 | findIndex((n) => n > 0), 33 | map(doSomethingWithIndex) 34 | ) 35 | ``` 36 | 37 | `Option` 을 사용하면, 우리는 `happy path` 에 대해서만 생각할 수 있으며, 에러처리는 `map` 안에서 내부적으로 이뤄집니다. 38 | 39 | **데모** (optional) 40 | 41 | [`04_functor.ts`](../04_functor.ts) 42 | 43 | **문제**. `Task` 는 항상 성공하는 비동기 호출을 의미합니다. 그렇다면 실패할 수 있는 계산작업은 어떻게 모델링할까요? 44 | -------------------------------------------------------------------------------- /src/functor/functions-as-programs.md: -------------------------------------------------------------------------------- 1 | ## 프로그램으로서의 함수 2 | 3 | 만약 함수로 프로그램을 모델링하고 싶다면 다음과 같은 문제를 해결해야합니다: 4 | 5 | > 어떻게 순수함수로 부작용을 발생시키는 프로그램을 모델링 할 수 있는가? 6 | 7 | 정답은 **효과 (effects)** 를 통해 부작용을 모델링하는 것인데, 이는 부작용을 **표현** 하는 수단으로 생각할 수 있습니다. 8 | 9 | JavaScript 에서 가능한 두 가지 기법을 살펴보겠습니다: 10 | 11 | - 효과를 위한 DSL (domain specific language) 을 정의 12 | - _thunk_ 를 사용 13 | 14 | DSL 을 사용하는 첫 번째 방법은 다음과 같은 프로그램을 15 | 16 | ```typescript 17 | function log(message: string): void { 18 | console.log(message) // 부작용 19 | } 20 | ``` 21 | 22 | 아래와 부작용에 대한 **설명** 을 반환하는 함수로 수정해 공역을 변경하는 것입니다: 23 | 24 | ```typescript 25 | type DSL = ... // 시스템이 처리할 수 있는 모든 effect 의 합타입 26 | 27 | function log(message: string): DSL { 28 | return { 29 | type: "log", 30 | message 31 | } 32 | } 33 | ``` 34 | 35 | **문제**. 새롭게 정의한 `log` 함수는 정말로 순수한가요? `log('foo') !== log('foo')` 임을 유의해주세요! 36 | 37 | 이 기법은 effect 와 최종 프로그램을 시작할 때 부작용을 실행할 수 있는 인터프리터의 정의를 결합하는 방법이 필요합니다. 38 | 39 | TypeScript 에서는 더 간단한 방법인 두 번째 기법은, 계산작업을 _thunk_ 로 감싸는 것입니다: 40 | 41 | ```typescript 42 | // 비동기적인 부작용을 의미하는 thunk 43 | type IO = () => A 44 | 45 | const log = (message: string): IO => { 46 | return () => console.log(message) // thunk 를 반환합니다 47 | } 48 | ``` 49 | 50 | `log` 함수는 호출 시에는 부작용을 발생시키진 않지만, (_action_ 이라 불리는) **계산작업을 나타내는 값** 을 반환합니다. 51 | 52 | ```typescript 53 | import { IO } from 'fp-ts/IO' 54 | 55 | export const log = (message: string): IO => { 56 | return () => console.log(message) // thunk 를 반환합니다 57 | } 58 | 59 | export const main = log('hello!') 60 | // 이 시점에서는 로그를 출력하지 않습니다 61 | // 왜냐하면 `main` 은 단지 계산작업을 나타내는 비활성 값이기 때문입니다. 62 | 63 | main() 64 | // 프로그램을 실행시킬 때 결과를 확인할 수 있습니다 65 | ``` 66 | 67 | 68 | 함수형 프로그래밍에서는 (effect 의 형태를 가진) 부작용을 (`main` 함수로 불리는) 시스템의 경계에 밀어넣는 경향이 있습니다. 즉 시스템은 다음과 같은 형태를 가지며 인터프리터에 의해 실행됩니다. 69 | 70 | > system = pure core + imperative shell 71 | 72 | (Haskell, PureScript 또는 Elm 과 같은) _순수 함수형_ 언어들은 언어 자체가 위 내용을 엄격하고 명확하게 지킬것을 요구합니다. 73 | > (원문) In _purely functional_ languages (like Haskell, PureScript or Elm) this division is strict and clear and imposed by the very languages. 74 | 75 | (`fp-ts` 에서 사용된) 이 thunk 기법또한 effect 를 결합할 수 있는 방법이 필요한데, 이는 일반적인 방법으로 프로그램을 합성하는 법을 찾아야 함을 의미합니다. 76 | 77 | 그 전에 우선 (비공식적인) 용어가 필요합니다: 다음 시그니쳐를 가진 함수를 **순수 프로그램** 이라 합시다: 78 | 79 | ```typescript 80 | (a: A) => B 81 | ``` 82 | 83 | 이러한 시그니처는 `A` 타입의 입력을 받아 `B` 타입의 결과를 아무런 effect 없이 반환하는 프로그램을 의미합니다. 84 | 85 | **예제** 86 | 87 | `len` 프로그램: 88 | 89 | ```typescript 90 | const len = (s: string): number => s.length 91 | ``` 92 | 93 | 이제 다음 시그니쳐를 가진 함수를 **effectful 프로그램** 이라 합시다: 94 | 95 | ```typescript 96 | (a: A) => F 97 | ``` 98 | 99 | 이러한 시그니쳐는 `A` 타입의 입력을 받아 `B` 타입과 **effect** `F` 를 함께 반환하는 프로그램을 의미합니다. 여기서 `F` 는 일종의 type constructor 입니다. 100 | 101 | [type constructor](https://en.wikipedia.org/wiki/Type_constructor) 는 `n` 개의 타입 연산자로 하나 이상의 타입을 받아 또 다른 타입을 반환합니다. 이전에 본 `Option`, `ReadonlyArray`, `Either` 와 같은 것이 type constructor 에 해당합니다. 102 | 103 | **예제** 104 | 105 | `head` 프로그램: 106 | 107 | ```typescript 108 | import { Option, some, none } from 'fp-ts/Option' 109 | 110 | const head = (as: ReadonlyArray): Option => 111 | as.length === 0 ? none : some(as[0]) 112 | ``` 113 | 114 | 이 프로그램은 `Option` effect 를 가집니다. 115 | 116 | effect 를 다루다보면 다음과 같은 `n` 개의 타입을 받는 type constructor 를 살펴봐야 합니다. 117 | 118 | | Type constructor | Effect (interpretation) | 119 | |--------------------|:------------------------| 120 | | `ReadonlyArray` | 비 결정적 계산작업 | 121 | | `Option` | 실패할 수 있는 계산작업 | 122 | | `Either` | 실패할 수 있는 계산작업 | 123 | | `IO` | **절대 실패하지 않는** 동기 계산작업 | 124 | | `Task` | **절대 실패하지 않는** 비동기 계잔작업 | 125 | | `Reader` | 외부 환경의 값 읽기 | 126 | 127 | 여기서 128 | 129 | ```typescript 130 | // `Promise` 를 반환하는 thunk 131 | type Task = () => Promise 132 | ``` 133 | 134 | ```typescript 135 | // `R` 은 계산에 필요한 "environment" 를 의미합니다 136 | // 그 값을 읽을 수 있으며 `A` 를 결과로 반환합니다 137 | type Reader = (r: R) => A 138 | ``` 139 | 140 | 이전의 핵심 문제로 돌아가봅시다: 141 | 142 | > 어떻게 두 일반적인 함수 `f: (a: A) => B` 와 `g: (c: C) => D` 를 합성할 수 있을까요? 143 | 144 | 지금까지 알아본 규칙으로는 이 일반적인 문제를 해결할 수 없습니다. 우리는 `B` 와 `C` 에 약간의 _경계_ 를 추가해야 합니다. 145 | 146 | `B = C` 의 경우에는 일반적인 함수 합성으로 해결할 수 있음을 알고 있습니다. 147 | 148 | ```typescript 149 | function flow(f: (a: A) => B, g: (b: B) => C): (a: A) => C { 150 | return (a) => g(f(a)) 151 | } 152 | ``` 153 | 154 | 하지만 다른 경우에는 어떻게 해야할까요? 155 | -------------------------------------------------------------------------------- /src/functor/functor-in-fp-ts.md: -------------------------------------------------------------------------------- 1 | ## `fp-ts` 에서의 functor 2 | 3 | `fp-ts` 에서는 어떻게 functor 를 정의할까요? 예제를 살펴봅시다. 4 | 5 | 다음 인터페이스는 어떤 HTTP API 의 결과를 표현한 것입니다: 6 | 7 | ```typescript 8 | interface Response { 9 | url: string 10 | status: number 11 | headers: Record 12 | body: A 13 | } 14 | ``` 15 | 16 | `boby` 는 타입 파라미터를 받기 때문에 이는 `Response` 가 functor 인스턴스의 후보가 된다는 것을 확인해주세요. 즉 `Response` 는 `n` 개의 파라미터를 받는 type constructor 조건을 만족합니다. (필요조건) 17 | 18 | `Response` 의 functor 인스턴스를 만들기 위해, `fp-ts` 가 요구하는 [기술적인 상세](https://gcanti.github.io/fp-ts/modules/HKT.ts.html) 와 함께 `map` 함수를 정의해야 합니다. 19 | 20 | ```typescript 21 | // `Response.ts` module 22 | 23 | import { pipe } from 'fp-ts/function' 24 | import { Functor1 } from 'fp-ts/Functor' 25 | 26 | declare module 'fp-ts/HKT' { 27 | interface URItoKind { 28 | readonly Response: Response 29 | } 30 | } 31 | 32 | export interface Response { 33 | readonly url: string 34 | readonly status: number 35 | readonly headers: Record 36 | readonly body: A 37 | } 38 | 39 | export const map = (f: (a: A) => B) => ( 40 | fa: Response 41 | ): Response => ({ 42 | ...fa, 43 | body: f(fa.body) 44 | }) 45 | 46 | // `Response` 의 functor 인스턴스 47 | export const Functor: Functor1<'Response'> = { 48 | URI: 'Response', 49 | map: (fa, f) => pipe(fa, map(f)) 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /src/functor/solve-general-problem.md: -------------------------------------------------------------------------------- 1 | ## 이제 functor 로 일반적인 문제를 해결할 수 있나요? 2 | 3 | 아직 아닙니다. Functor 는 effectful 프로그램인 `f` 를 순수 함수인 `g` 를 합성할 수 있게 해줍니다. 하지만 `g` 는 오직 하나의 인자를 받은 **unary** 함수이어야 합니다. 만약 `g` 가 두 개 이상의 인자를 받는다면 어떻게 될까요? 4 | 5 | | 프로그램 f | 프로그램 g | 합성 | 6 | |-----------|-------------------------|--------------| 7 | | pure | pure | `g ∘ f` | 8 | | effectful | pure (unary) | `map(g) ∘ f` | 9 | | effectful | pure (`n`-ary, `n > 1`) | ? | 10 | 11 | 12 | 이 상황을 해결하려면 무언가 _더_ 필요합니다. 다음 장에서 함수형 프로그래밍에서 또 다른 중요한 추상화인 **applicative functor** 를 살펴볼 예정입니다. 13 | -------------------------------------------------------------------------------- /src/images/adt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/adt.png -------------------------------------------------------------------------------- /src/images/associativity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/associativity.png -------------------------------------------------------------------------------- /src/images/bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/bird.png -------------------------------------------------------------------------------- /src/images/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/category.png -------------------------------------------------------------------------------- /src/images/chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/chain.png -------------------------------------------------------------------------------- /src/images/composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/composition.png -------------------------------------------------------------------------------- /src/images/contramap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/contramap.png -------------------------------------------------------------------------------- /src/images/eilenberg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/eilenberg.jpg -------------------------------------------------------------------------------- /src/images/flatMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/flatMap.png -------------------------------------------------------------------------------- /src/images/functor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/functor.png -------------------------------------------------------------------------------- /src/images/identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/identity.png -------------------------------------------------------------------------------- /src/images/kleisli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/kleisli.jpg -------------------------------------------------------------------------------- /src/images/kleisli_arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/kleisli_arrows.png -------------------------------------------------------------------------------- /src/images/kleisli_category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/kleisli_category.png -------------------------------------------------------------------------------- /src/images/kleisli_composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/kleisli_composition.png -------------------------------------------------------------------------------- /src/images/liftA2-first-step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/liftA2-first-step.png -------------------------------------------------------------------------------- /src/images/liftA2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/liftA2.png -------------------------------------------------------------------------------- /src/images/maclane.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/maclane.jpg -------------------------------------------------------------------------------- /src/images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/map.png -------------------------------------------------------------------------------- /src/images/moggi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/moggi.jpg -------------------------------------------------------------------------------- /src/images/monoid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/monoid.png -------------------------------------------------------------------------------- /src/images/morphism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/morphism.png -------------------------------------------------------------------------------- /src/images/mutable-immutable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/mutable-immutable.jpg -------------------------------------------------------------------------------- /src/images/objects-morphisms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/objects-morphisms.png -------------------------------------------------------------------------------- /src/images/of.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/of.png -------------------------------------------------------------------------------- /src/images/semigroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/semigroup.png -------------------------------------------------------------------------------- /src/images/semigroupVector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/semigroupVector.png -------------------------------------------------------------------------------- /src/images/spoiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/spoiler.png -------------------------------------------------------------------------------- /src/images/wadler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbl428/functional-programming/4c267fbbd9e35c743e91df9f140ab2162d7fe033/src/images/wadler.jpg -------------------------------------------------------------------------------- /src/laws.ts: -------------------------------------------------------------------------------- 1 | import { Semigroup } from 'fp-ts/Semigroup' 2 | import * as fc from 'fast-check' 3 | 4 | // ------------------------------------------------------------------------------------- 5 | // laws 6 | // ------------------------------------------------------------------------------------- 7 | 8 | // prima di tutto ho codificato la proprietà associativa come una funzione che restituisce un `boolean` 9 | 10 | export const laws = { 11 | semigroup: { 12 | associativity: (S: Semigroup) => (a: A, b: A, c: A): boolean => 13 | S.concat(S.concat(a, b), c) === S.concat(a, S.concat(b, c)) 14 | } 15 | } 16 | 17 | // ------------------------------------------------------------------------------------- 18 | // properties 19 | // ------------------------------------------------------------------------------------- 20 | 21 | // poi ho definito una `property` di `fast-check` tramite una funzione che accetta il semigruppo da testare (parametro `S`) 22 | // e un `Arbitrary` di `fast-check`. 23 | // Un `Arbitrary` è un data type che rappresenta la possiblità di creare valori di tipo `A` in modo random. 24 | 25 | export const properties = { 26 | semigroup: { 27 | associativity: (S: Semigroup, arb: fc.Arbitrary) => 28 | // dato che la legge che ho definito necessita di tre parametri (`a`, `b`, `c`), 29 | // passo l'arbitrary a `fc.property` tre volte, una per ogni parametro. 30 | fc.property(arb, arb, arb, laws.semigroup.associativity(S)) 31 | } 32 | } 33 | 34 | // La libreria a questo punto fa tutto da sola una volta che chiamo `fc.assert`, bombarda cioè la proprietà 35 | // che ho definito con delle terne casuali da usare come `a`, `b`, `c` e verifica che la proprietà restituisca sempre `true` 36 | 37 | // Nel caso non avvenga, la libreria è in grado di mostrare un controesempio. 38 | // Se guardate più sotto vado a testare la proprietà associativa per il magma `MagmaSub` (che abbiamo visto nel corso) 39 | 40 | // ------------------------------------------------------------------------------------- 41 | // tests 42 | // ------------------------------------------------------------------------------------- 43 | 44 | import { Magma } from 'fp-ts/Magma' 45 | 46 | export const MagmaSub: Magma = { 47 | concat: (x, y) => x - y 48 | } 49 | 50 | // prova che `MagmaSub` non è un semigruppo se viene trovato un controesempio 51 | // in questo caso ho scelto di bombardare `MagmaSub` con interi casuali usando 52 | // l'Arbitrary `fc.integer` messo a disposizione da `fast-check` 53 | fc.assert(properties.semigroup.associativity(MagmaSub, fc.integer())) 54 | 55 | // se lanciate questo file con `ts-node src/laws.ts` dovreste vedere un output di questo tipo: 56 | /* 57 | Error: Property failed after 1 tests 58 | { seed: 1835229399, path: "0:0:0:1:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0", endOnFailure: true } 59 | Counterexample: [0,0,1] 60 | Shrunk 32 time(s) 61 | Got error: Property failed by returning false 62 | */ 63 | 64 | // Il controesempio trovato è [0,0,1] (che sarebbe la tupla di parametri `[a, b, c]` che fanno fallire la legge) 65 | // Infatti: 66 | // console.log(pipe(0, MagmaSub.concat(0), MagmaSub.concat(1))) // => -1 67 | // console.log(pipe(0, MagmaSub.concat(pipe(0, MagmaSub.concat(1))))) // => 1 68 | 69 | // ------------------------------------- 70 | 71 | // `last` e `first` producono dei semigruppi? non posso dirlo con certezza usando 72 | // property testing perchè i seguenti assert NON producono controesempi 73 | 74 | // fc.assert(properties.semigroup.associativity(Se.first(), fc.integer())) 75 | // fc.assert(properties.semigroup.associativity(Se.last(), fc.integer())) 76 | -------------------------------------------------------------------------------- /src/monad/README.md: -------------------------------------------------------------------------------- 1 | # Monads 2 | 3 | ![Eugenio Moggi](../images/moggi.jpg) 4 | 5 | (Eugenio Moggi 는 이탈리아 Genoa 대학교의 컴퓨터 공학 교수입니다. 그는 먼저 프로그램을 만들기 위한 monad 의 일반적인 사용법을 발견했습니다) 6 | 7 | ![Philip Lee Wadler](../images/wadler.jpg) 8 | 9 | (Philip Lee Wadler 는 프로그래밍 언어 디자인과 타입 이론에 기여한 것으로 알려진 미국의 컴퓨터 과학자입니다) 10 | 11 | 이전 장에서 type constructor `F` 가 applicative functor 인스턴스를 가지는 경우에만 1 개 이상의 파라미터를 가지는 순수 프로그램 `g` 와 effectful 프로그램 `f: (a: A) => F` 를 합성할 수 있음을 살펴보았습니다: 12 | 13 | | 프로그램 f | 프로그램 g | 합성 | 14 | |-----------|---------------|-----------------| 15 | | pure | pure | `g ∘ f` | 16 | | effectful | pure (unary) | `map(g) ∘ f` | 17 | | effectful | pure, `n`-ary | `liftAn(g) ∘ f` | 18 | 19 | 하지만 꽤 자주 발생하는 다음 상황에 대한 문제를 해결해야합니다. 두 프로그램 **모두** effectful 인 경우입니다: 20 | 21 | ```typescript 22 | f: (a: A) => F 23 | g: (b: B) => F 24 | ``` 25 | 26 | `f` 와 `g` 의 합성이란 무엇일까요? 27 | -------------------------------------------------------------------------------- /src/monad/defining-chain.md: -------------------------------------------------------------------------------- 1 | ## 단계별 `chain` 정의하기 2 | 3 | Monad 의 첫 번째 정의는 `M` 은 functor 인스턴스를 만족해야 함을 의미하며 `g: (b: B) => M` 함수를 `map(g): (mb: M) => M>` 로 변경할 수 있다는 사실을 알 수 있습니다. 4 | 5 | ![h 함수를 얻는 방법](../images/flatMap.png) 6 | 7 | 이제 문제가 발생합니다: functor 인스턴스를 위한 `M>` 타입을 `M` 로 만들어주는 연산이 필요한 상황이며 그러한 연산자를 `flatten` 이라 부르도록 합시다. 8 | 9 | 만약 이 연산자를 정의할 수 있다면 우리가 원하는 합성 방법을 찾을 수 있습니다: 10 | 11 | ``` 12 | h = flatten ∘ map(g) ∘ f 13 | ``` 14 | 15 | `flatten ∘ map(g)` 이름을 합쳐서 "flatMap" 이라는 이름을 얻을 수 있습니다! 16 | 17 | `chain` 도 이 방식으로 얻을 수 있습니다 18 | 19 | ``` 20 | chain = flatten ∘ map(g) 21 | ``` 22 | 23 | ![chain 이 함수 g 에 동작하는 방식](../images/chain.png) 24 | 25 | 이제 합성 테이블을 갱신할 수 있습니다 26 | 27 | | 프로그램 f | 프로그램 g | 합성 | 28 | |-----------|---------------|-----------------| 29 | | pure | pure | `g ∘ f` | 30 | | effectful | pure (unary) | `map(g) ∘ f` | 31 | | effectful | pure, `n`-ary | `liftAn(g) ∘ f` | 32 | | effectful | effectful | `chain(g) ∘ f` | 33 | 34 | `of` 는 경우는 어떤가요? 사실 `of` 는 _K_ 의 identity morphism 에서 왔습니다. _K_ 의 임의의 identity morphism 인 1A 에 대해 `A` 에서 `M` 로 매칭되는 함수가 존재합니다 (즉 `of: (a: A) => M`). 35 | > (원문) What about `of`? Well, `of` comes from the identity morphisms in _K_: for every identity morphism 1A in _K_ there has to be a corresponding function from `A` to `M` (that is, `of: (a: A) => M`). 36 | 37 | ![of 는 어디서 왔는가](../images/of.png) 38 | 39 | `of` 가 `chain` 에 대한 중립 원소라는 사실은 다음과 같은 종류의 흐름 제어를 가능하게 합니다: 40 | 41 | ```typescript 42 | pipe( 43 | mb, 44 | M.chain((b) => (predicate(b) ? M.of(b) : g(b))) 45 | ) 46 | ``` 47 | 48 | 여기서 `predicate: (b: B) => boolean`, `mb: M` 이며 `g: (b: B) => M`. 49 | 50 | 마지막 질문: Monad 법칙은 어디에서 온걸까요? 법칙은 단순히 _K_ 에서의 범주형 법칙이 _TS_ 로 변환된 것입니다: 51 | 52 | | Law | _K_ | _TS_ | 53 | |------|-----------------------------------|---------------------------------------------------------| 54 | | 좌동등성 | 1B ∘ `f'` = `f'` | `chain(of) ∘ f = f` | 55 | | 우동등성 | `f'` ∘ 1A = `f'` | `chain(f) ∘ of = f` | 56 | | 결합법칙 | `h' ∘ (g' ∘ f') = (h' ∘ g') ∘ f'` | `chain(h) ∘ (chain(g) ∘ f) = chain((chain(h) ∘ g)) ∘ f` | 57 | 58 | 이제 이전에 본 중첩된 context 문제를 `chain` 을 통해 해결할 수 있습니다: 59 | 60 | ```typescript 61 | import { pipe } from 'fp-ts/function' 62 | import * as O from 'fp-ts/Option' 63 | import * as A from 'fp-ts/ReadonlyArray' 64 | 65 | interface User { 66 | readonly id: number 67 | readonly name: string 68 | readonly followers: ReadonlyArray 69 | } 70 | 71 | const getFollowers = (user: User): ReadonlyArray => user.followers 72 | 73 | declare const user: User 74 | 75 | const followersOfFollowers: ReadonlyArray = pipe( 76 | user, 77 | getFollowers, 78 | A.chain(getFollowers) 79 | ) 80 | 81 | const inverse = (n: number): O.Option => 82 | n === 0 ? O.none : O.some(1 / n) 83 | 84 | const inverseHead: O.Option = pipe([1, 2, 3], A.head, O.chain(inverse)) 85 | ``` 86 | 87 | 지금까지 보았던 type constructor 에 대해 `chain` 함수를 어떻게 구현했는지 살펴봅시다: 88 | 89 | **예제** (`F = ReadonlyArray`) 90 | 91 | ```typescript 92 | // 함수 `B -> ReadonlyArray` 를 함수 `ReadonlyArray -> ReadonlyArray` 로 변환합니다 93 | const chain = (g: (b: B) => ReadonlyArray) => ( 94 | mb: ReadonlyArray 95 | ): ReadonlyArray => { 96 | const out: Array = [] 97 | for (const b of mb) { 98 | out.push(...g(b)) 99 | } 100 | return out 101 | } 102 | ``` 103 | 104 | **예제** (`F = Option`) 105 | 106 | ```typescript 107 | import { match, none, Option } from 'fp-ts/Option' 108 | 109 | // 함수 `B -> Option` 를 함수 `Option -> Option` 로 변환합니다 110 | const chain = (g: (b: B) => Option): ((mb: Option) => Option) => 111 | match(() => none, g) 112 | ``` 113 | 114 | **에제** (`F = IO`) 115 | 116 | ```typescript 117 | import { IO } from 'fp-ts/IO' 118 | 119 | // 함수 `B -> IO` 를 함수 `IO -> IO` 로 변환합니다 120 | const chain = (g: (b: B) => IO) => (mb: IO): IO => () => 121 | g(mb())() 122 | ``` 123 | 124 | **예제** (`F = Task`) 125 | 126 | ```typescript 127 | import { Task } from 'fp-ts/Task' 128 | 129 | // 함수 `B -> Task` 를 함수 `Task -> Task` 로 변환합니다 130 | const chain = (g: (b: B) => Task) => (mb: Task): Task => () => 131 | mb().then((b) => g(b)()) 132 | ``` 133 | 134 | **예제** (`F = Reader`) 135 | 136 | ```typescript 137 | import { Reader } from 'fp-ts/Reader' 138 | 139 | // 함수 `B -> Reader` 를 함수 `Reader -> Reader` 로 변환합니다 140 | const chain = (g: (b: B) => Reader) => ( 141 | mb: Reader 142 | ): Reader => (r) => g(mb(r))(r) 143 | ``` 144 | -------------------------------------------------------------------------------- /src/monad/definition.md: -------------------------------------------------------------------------------- 1 | ## Monad 정의 2 | 3 | **정의**. Monad 는 다음 세 가지 항목으로 정의합니다: 4 | 5 | (1) Functor 인스턴스를 만족하는 type constructor `M` 6 | 7 | (2) 다음 시그니처를 가진 (**pure** 나 **return** 으로도 불리는) 함수 `of`: 8 | 9 | ```typescript 10 | of: (a: A) => M 11 | ``` 12 | 13 | (3) 다음 시그니처를 가진 (**flatMap** 이나 **bind** 로도 불리는) 함수 `chain`: 14 | 15 | ```typescript 16 | chain: (f: (a: A) => M) => (ma: M) => M 17 | ``` 18 | 19 | `of` 와 `chain` 함수는 아래 세 가지 법칙을 만족해야 합니다: 20 | 21 | - `chain(of) ∘ f = f` (**좌동등성**) 22 | - `chain(f) ∘ of = f` (**우동등성**) 23 | - `chain(h) ∘ (chain(g) ∘ f) = chain((chain(h) ∘ g)) ∘ f` (**결합법칙**) 24 | 25 | 여기서 `f`, `g`, `h` 는 모두 effectful 함수이며 `∘` 는 보통의 함수 합성을 말합니다. 26 | 27 | 처음 이 정의를 보았을 때 많은 의문이 생겼습니다: 28 | 29 | - `of` 와 `chain` 연산이란 무엇인가? 왜 그러한 시그니처를 가지고 있는가? 30 | - 왜 "pure" 나 "flatMap" 와 같은 동의어를 가지고 있는가? 31 | - 왜 그러한 법칙을 만족해야 하는가? 그것은 무엇을 의미하는가? 32 | - `flatten` 이 monad 에서 그렇게 중요하다면, 왜 정의에는 보이지 않는걸까? 33 | 34 | 이번 장에서 위 의문들을 모두 해소할 것입니다. 35 | 36 | 핵심 문제로 돌아가봅시다: 두 effectful 함수 `f` 와 `g` 의 합성이란 무엇일까요? 37 | 38 | ![two Kleisli arrows, what's their composition?](../images/kleisli_arrows.png) 39 | 40 | **참고**. Effectful 함수는 **Kleisli arrow** 라고도 불립니다. 41 | 42 | 당분간은 그러한 합성의 **타입** 조차 알지 못합니다. 43 | 44 | 하지만 우리는 이미 합성에 대해 구체적으로 이야기하는 추상적인 개념들을 살펴보았습니다. 우리가 category 에 대해 말했던 것을 기억하나요? 45 | 46 | > category 는 합성의 본질이라 할 수 있습니다. 47 | 48 | 우리는 당면한 문제를 category 문제로 바꿀 수 있습니다: Kleisli arrows 의 합성을 모델링하는 category 를 찾을 수 있을까요? 49 | -------------------------------------------------------------------------------- /src/monad/kleisli-category.md: -------------------------------------------------------------------------------- 1 | ## Kleisli category 2 | 3 | ![Heinrich Kleisli](../images/kleisli.jpg) 4 | 5 | Kleisli arrow 로만 이루어진 (**Kleisli category** 로 불리는) category _K_ 를 만들어봅시다: 6 | 7 | - **object** 는 _TS_ category 에서의 object 와 동일합니다, 즉 모든 TypeScript 타입입니다. 8 | - **morphism** 은 다음 방식으로 만듭니다: _TS_ 에서의 모든 Kleisli arrow `f: A ⟼ M` 는 _K_ 에서 `f': A ⟼ B` 로 매핑됩니다. 9 | 10 | ![위는 TS category, 아래는 K construction](../images/kleisli_category.png) 11 | 12 | 그렇다면 _K_ 에서 `f` 와 `g` 의 합성은 무엇일까요? 아래 아미지에서 `h'` 로 표시된 붉은 화살표입니다: 13 | 14 | ![위는 TS category 에서의 합성, 아래는 K construction 에서의 합성](../images/kleisli_composition.png) 15 | 16 | `K` 에 속하는 `A` 에서 `C` 로 향하는 화살표 `h'` 가 주어지면, 그에 해당하는 _TS_ 에 속하는 `A` 에서 `M` 로 향하는 함수 `h` 를 찾을 수 있습니다. 17 | 18 | 따라서 _TS_ 에서 `f` 와 `g` 의 합성을 위한 좋은 후보는 다음 시그니처를 가진 Kleisli arrow 입니다: `(a: A) => M` 19 | 20 | 이제 이러한 함수를 구현해봅시다. 21 | -------------------------------------------------------------------------------- /src/monad/nested-context-problem.md: -------------------------------------------------------------------------------- 1 | ## 중첩된 context 문제 2 | 3 | 일반적인 문제를 풀기 위해 무언가 더 필요한 이유를 보여주는 몇가지 예제를 살펴봅시다. 4 | 5 | **예제** (`F = Array`) 6 | 7 | Follower 들의 follower 가 필요한 상황이라 가정합시다. 8 | 9 | 10 | ```typescript 11 | import { pipe } from 'fp-ts/function' 12 | import * as A from 'fp-ts/ReadonlyArray' 13 | 14 | interface User { 15 | readonly id: number 16 | readonly name: string 17 | readonly followers: ReadonlyArray 18 | } 19 | 20 | const getFollowers = (user: User): ReadonlyArray => user.followers 21 | 22 | declare const user: User 23 | 24 | // followersOfFollowers: ReadonlyArray> 25 | const followersOfFollowers = pipe(user, getFollowers, A.map(getFollowers)) 26 | ``` 27 | 28 | 여기서 문제가 발생합니다, `followersOfFollowers` 의 타입은 `ReadonlyArray>` 이지만 우리가 원하는 것은 `ReadonlyArray` 입니다. 29 | 30 | 중첩된 배열의 **평탄화(flatten)** 가 필요합니다. 31 | 32 | `fp-ts/ReadonlyArray` 모듈에 있는 함수 `flatten: (mma: ReadonlyArray>) => ReadonlyArray` 가 바로 우리가 원하는 것입니다: 33 | 34 | ```typescript 35 | // followersOfFollowers: ReadonlyArray 36 | const followersOfFollowers = pipe( 37 | user, 38 | getFollowers, 39 | A.map(getFollowers), 40 | A.flatten 41 | ) 42 | ``` 43 | 44 | 좋습니다! 다른 자료형도 살펴봅시다. 45 | 46 | **예제** (`F = Option`) 47 | 48 | 숫자 배열의 첫 번째 요소의 역수를 계산한다고 가정합시다: 49 | 50 | ```typescript 51 | import { pipe } from 'fp-ts/function' 52 | import * as O from 'fp-ts/Option' 53 | import * as A from 'fp-ts/ReadonlyArray' 54 | 55 | const inverse = (n: number): O.Option => 56 | n === 0 ? O.none : O.some(1 / n) 57 | 58 | // inverseHead: O.Option> 59 | const inverseHead = pipe([1, 2, 3], A.head, O.map(inverse)) 60 | ``` 61 | 62 | 이런, 같은 현상이 발생합니다, `inverseHead` 의 타입은 `Option>` 이지만 우리가 원하는 것은 `Option` 입니다. 63 | 64 | 이번에도 중첩된 `Option` 를 평평하게 만들어야 합니다. 65 | 66 | `fp-ts/Option` 모듈에 있는 함수 `flatten: (mma: Option>) => Option` 가 우리가 원하는 것입니다: 67 | 68 | ```typescript 69 | // inverseHead: O.Option 70 | const inverseHead = pipe([1, 2, 3], A.head, O.map(inverse), O.flatten) 71 | ``` 72 | 73 | 모든 곳에 `flatten` 함수가 있는데... 이것은 우연은 아닙니다. 다음과 같은 함수형 패턴이 존재합니다: 74 | 75 | 두 type constructor `ReadonlyArray` 와 `Option` (그 외 여러) 은 **monad 인스턴스** 를 가지고 있습니다. 76 | 77 | > `flatten` 은 monad 의 가장 특별한 연산입니다 78 | 79 | **참고**. `flatten` 의 자주 사용되는 동의어로 **join** 이 있습니다. 80 | 81 | 그럼 monad 란 무엇일까요? 82 | 83 | 보통 다음과 같이 표현합니다... 84 | -------------------------------------------------------------------------------- /src/monoid-modeling/README.md: -------------------------------------------------------------------------------- 1 | # Monoids 를 통한 합성 모델링 2 | 3 | 지금까지 배운것을 다시 정리해봅시다. 4 | 5 | **대수**이 아래 조합으로 이루어져 있다는 것을 보았습니다: 6 | 7 | - 타입 `A` 8 | - 타입 `A` 와 연관된 몇가지 연산들 9 | - 조합을 위한 몇가지 법칙과 속성 10 | 11 | 처음 살펴본 대수는 `concat` 으로 불리는 연산을 하나 가진 magma 였습니다. `Magma` 에 대한 특별한 법칙은 없었고 다만 `concat` 연산이 `A` 에 대해 닫혀있어야 했습니다. 즉 다음 연산의 결과는 12 | 13 | ```typescript 14 | concat(first: A, second: A) => A 15 | ``` 16 | 17 | 여전히 `A` 에 속해야 합니다. 18 | 19 | 이후 여기에 하나의 간단한 _결합법칙_ 을 추가함으로써, `Magma` 를 더 다듬어진 `Semigroup` 을 얻게 되었습니다. 이를 통해 결합법칙이 병렬계산을 가능하게 해주는지 알게 되었습니다. 20 | 21 | 이제 Semigroup 에 또 다른 규칙을 추가하고자 합니다. 22 | 23 | `concat` 연산이 있는 집합 `A` 에 대한 `Semigorup` 이 주어진 경우, 만약 집합 `A` 의 어떤 한 요소가 `A` 의 모든 요소 `a` 에 대해 다음 두 조건을 만족한다면 (이 요소를 _empty_ 라 부르겠습니다) 24 | 25 | - **우동등성(Right identity)**: `concat(a, empty) = a` 26 | - **좌동등성(Left identity)**: `concat(empty, a) = a` 27 | 28 | 이 `Semigroup` 은 또한 `Moniod` 입니다. 29 | 30 | **참고**: 이후 내용에서는 `empty` 를 **unit** 으로 부르겠습니다. 다른 여러 동의어들이 있으며, 그 중 가장 많이 쓰이는 것은 _neutral element_ 과 _identity element_ 입니다. 31 | 32 | ```typescript 33 | import { Semigroup } from 'fp-ts/Semigroup' 34 | 35 | interface Monoid extends Semigroup { 36 | readonly empty: A 37 | } 38 | ``` 39 | 40 | 이전까지 본 많은 semigroup 들이 `Monid` 로 확장할 수 있었습니다. `A` 에 대해 우동등성과 좌동등성을 만족하는 요소를 찾기만 하면 됩니다. 41 | 42 | ```typescript 43 | import { Monoid } from 'fp-ts/Monoid' 44 | 45 | /** 덧셈에 대한 number `Monoid` */ 46 | const MonoidSum: Monoid = { 47 | concat: (first, second) => first + second, 48 | empty: 0 49 | } 50 | 51 | /** 곰셈에 대한 number `Monoid` */ 52 | const MonoidProduct: Monoid = { 53 | concat: (first, second) => first * second, 54 | empty: 1 55 | } 56 | 57 | const MonoidString: Monoid = { 58 | concat: (first, second) => first + second, 59 | empty: '' 60 | } 61 | 62 | /** 논리곱에 대한 boolean monoid */ 63 | const MonoidAll: Monoid = { 64 | concat: (first, second) => first && second, 65 | empty: true 66 | } 67 | 68 | /** 논리합에 대한 boolean monoid */ 69 | const MonoidAny: Monoid = { 70 | concat: (first, second) => first || second, 71 | empty: false 72 | } 73 | ``` 74 | 75 | **문제**. 이전 섹션에서 타입 `ReadonlyArray` 에 대한 `Semigorup` 인스턴스를 정의했습니다: 76 | 77 | ```typescript 78 | import { Semigroup } from 'fp-ts/Semigroup' 79 | 80 | const Semigroup: Semigroup> = { 81 | concat: (first, second) => first.concat(second) 82 | } 83 | ``` 84 | 85 | 이 semigroup 에 대한 `unit` 을 찾을 수 있을까요? 만약 그렇다면, `ReadonlyArray` 뿐만 아니라 `ReadonlyArray` 에 대해서도 그렇다고 일반화할 수 있을까요? 86 | 87 | **문제** (더 복잡함). 임의의 monoid 에 대해, unit 이 오직 하나만 있음을 증명해보세요. 88 | 89 | 위 증명을 통해 monoid 당 오직 하나의 unit 만 있다는 것이 보증되기에, 우리는 monoid 에서 unit 을 하나 찾았다면 더 이상 찾지 않아도 됩니다. 90 | 91 | 모든 semigroup 은 magma 이지만, 역은 성립하지 않았듯이, 모든 monoid 는 semigroup 이지만, 모든 semigroup 이 monoid 는 아닙니다. 92 | 93 | ![Magma vs Semigroup vs Monoid](../images/monoid.png) 94 | 95 | **예제** 96 | 97 | 다음 예제를 살펴봅시다: 98 | 99 | ```typescript 100 | import { pipe } from 'fp-ts/function' 101 | import { intercalate } from 'fp-ts/Semigroup' 102 | import * as S from 'fp-ts/string' 103 | 104 | const SemigroupIntercalate = pipe(S.Semigroup, intercalate('|')) 105 | 106 | console.log(S.Semigroup.concat('a', 'b')) // => 'ab' 107 | console.log(SemigroupIntercalate.concat('a', 'b')) // => 'a|b' 108 | console.log(SemigroupIntercalate.concat('a', '')) // => 'a|' 109 | ``` 110 | 111 | 이 semigroup 은 `concat(a, empty) = a` 를 만족하는 `string` 타입인 `empty` 가 존재하지 않는점을 확인해주세요. 112 | 113 | 마지막으로, 함수가 포함된 더 "난해한" 예제입니다: 114 | 115 | **예제** 116 | 117 | **endomorphism** 은 입력과 출력 타입이 같은 함수를 말합니다: 118 | 119 | ```typescript 120 | type Endomorphism = (a: A) => A 121 | ``` 122 | 123 | 임의의 타입 `A` 에 대해, `A` 의 endomorphism 에 대해 다음과 같이 정의된 인스턴스는 monoid 입니다: 124 | 125 | - `concat` 연산은 일반적인 함수 합성과 같습니다 126 | - unit 값은 항등함수 입니다 127 | 128 | ```typescript 129 | import { Endomorphism, flow, identity } from 'fp-ts/function' 130 | import { Monoid } from 'fp-ts/Monoid' 131 | 132 | export const getEndomorphismMonoid = (): Monoid> => ({ 133 | concat: flow, 134 | empty: identity 135 | }) 136 | ``` 137 | 138 | **참고**: `identity` 함수는 다음과 같은 구현 하나만 존재합니다: 139 | 140 | ```typescript 141 | const identity = (a: A) => a 142 | ``` 143 | 144 | 입력으로 무엇을 받든지, 그것을 그대로 결과로 돌려줍니다. 145 | 146 | 150 | -------------------------------------------------------------------------------- /src/monoid-modeling/concat-all.md: -------------------------------------------------------------------------------- 1 | ## `concatAll` 함수 2 | 3 | semigroup 과 비교해서 monoid 의 한 가지 큰 특징은 다수의 요소를 결합하는게 훨씬 쉬워진다는 것입니다: 더 이상 초기값을 제공하지 않아도 됩니다. 4 | 5 | ```typescript 6 | import { concatAll } from 'fp-ts/Monoid' 7 | import * as S from 'fp-ts/string' 8 | import * as N from 'fp-ts/number' 9 | import * as B from 'fp-ts/boolean' 10 | 11 | console.log(concatAll(N.MonoidSum)([1, 2, 3, 4])) // => 10 12 | console.log(concatAll(N.MonoidProduct)([1, 2, 3, 4])) // => 24 13 | console.log(concatAll(S.Monoid)(['a', 'b', 'c'])) // => 'abc' 14 | console.log(concatAll(B.MonoidAll)([true, false, true])) // => false 15 | console.log(concatAll(B.MonoidAny)([true, false, true])) // => true 16 | ``` 17 | 18 | **문제**. 왜 초기값이 더 이상 필요없을까요? 19 | -------------------------------------------------------------------------------- /src/monoid-modeling/product-monoid.md: -------------------------------------------------------------------------------- 1 | ## Product monoid 2 | 3 | 이미 semigroup 에서 보았듯이, 각 필드에 대해 monoid 인스턴스를 정의할 수 있다면 `구조체`에 대한 monoid 인스턴스를 정의할 수 있습니다. 4 | 5 | **예제** 6 | 7 | ```typescript 8 | import { Monoid, struct } from 'fp-ts/Monoid' 9 | import * as N from 'fp-ts/number' 10 | 11 | type Point = { 12 | readonly x: number 13 | readonly y: number 14 | } 15 | 16 | const Monoid: Monoid = struct({ 17 | x: N.MonoidSum, 18 | y: N.MonoidSum 19 | }) 20 | ``` 21 | 22 | **참고**. `struct` 와 비슷하게 tuple 에 적용하는 combinator 가 있습니다: `tuple`. 23 | 24 | ```typescript 25 | import { Monoid, tuple } from 'fp-ts/Monoid' 26 | import * as N from 'fp-ts/number' 27 | 28 | type Point = readonly [number, number] 29 | 30 | const Monoid: Monoid = tuple(N.MonoidSum, N.MonoidSum) 31 | ``` 32 | 33 | **문제**. 일반적인 타입 `A` 의 "free monoid" 를 정의할 수 있을까요? 34 | 35 | **데모** (캔버스에 기하학적 도형을 그리는 시스템 구현) 36 | 37 | [`03_shapes.ts`](../03_shapes.ts) 38 | -------------------------------------------------------------------------------- /src/mutable-is-unsafe-in-typescript.ts: -------------------------------------------------------------------------------- 1 | const xs: Array = ['a', 'b', 'b'] 2 | const ys: Array = xs 3 | ys.push(undefined) 4 | xs.map((s) => s.trim()) // explosion at runtime 5 | -------------------------------------------------------------------------------- /src/onion-architecture/1.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Adattato da https://github.com/matteobaglini/onion-with-functional-programming 4 | 5 | Porting dell'originale in TypeScript 6 | 7 | */ 8 | 9 | import * as fs from 'fs' 10 | 11 | class Employee { 12 | constructor( 13 | readonly lastName: string, 14 | readonly firstName: string, 15 | readonly dateOfBirth: Date, 16 | readonly email: string 17 | ) {} 18 | isBirthday(today: Date): boolean { 19 | return ( 20 | this.dateOfBirth.getMonth() === today.getMonth() && 21 | this.dateOfBirth.getDate() === today.getDate() 22 | ) 23 | } 24 | } 25 | 26 | const sendMessage = ( 27 | smtpHost: string, 28 | smtpPort: number, 29 | from: string, 30 | subject: string, 31 | body: string, 32 | recipient: string 33 | ): void => { 34 | console.log(smtpHost, smtpPort, from, subject, body, recipient) 35 | } 36 | 37 | const sendGreetings = ( 38 | fileName: string, 39 | today: Date, 40 | smtpHost: string, 41 | smtpPort: number 42 | ): void => { 43 | const input = fs.readFileSync(fileName, { 44 | encoding: 'utf8' 45 | }) 46 | const lines = input.split('\n').slice(1) // skip header 47 | for (let i = 0; i < lines.length; i++) { 48 | const employeeData = lines[i].split(', ') 49 | const employee = new Employee( 50 | employeeData[0], 51 | employeeData[1], 52 | new Date(employeeData[2]), 53 | employeeData[3] 54 | ) 55 | if (employee.isBirthday(today)) { 56 | const recipient = employee.email 57 | const body = `Happy Birthday, dear ${employee.firstName}!` 58 | const subject = 'Happy Birthday!' 59 | sendMessage( 60 | smtpHost, 61 | smtpPort, 62 | 'sender@here.com', 63 | subject, 64 | body, 65 | recipient 66 | ) 67 | } 68 | } 69 | } 70 | 71 | sendGreetings( 72 | 'src/onion-architecture/employee_data.txt', 73 | new Date(2008, 9, 8), 74 | 'localhost', 75 | 80 76 | ) 77 | -------------------------------------------------------------------------------- /src/onion-architecture/2.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Primo refactoring: estrarre le funzioni 4 | 5 | */ 6 | 7 | import * as fs from 'fs' 8 | 9 | class Employee { 10 | constructor( 11 | readonly lastName: string, 12 | readonly firstName: string, 13 | readonly dateOfBirth: Date, 14 | readonly email: string 15 | ) {} 16 | isBirthday(today: Date): boolean { 17 | return ( 18 | this.dateOfBirth.getMonth() === today.getMonth() && 19 | this.dateOfBirth.getDate() === today.getDate() 20 | ) 21 | } 22 | } 23 | 24 | class Email { 25 | constructor( 26 | readonly from: string, 27 | readonly subject: string, 28 | readonly body: string, 29 | readonly recipient: string 30 | ) {} 31 | } 32 | 33 | // pure 34 | const toEmail = (employee: Employee): Email => { 35 | const recipient = employee.email 36 | const body = `Happy Birthday, dear ${employee.firstName}!` 37 | const subject = 'Happy Birthday!' 38 | return new Email('sender@here.com', subject, body, recipient) 39 | } 40 | 41 | // pure 42 | const getGreetings = ( 43 | today: Date, 44 | employees: ReadonlyArray 45 | ): ReadonlyArray => { 46 | return employees.filter((e) => e.isBirthday(today)).map(toEmail) 47 | } 48 | 49 | // pure 50 | const parse = (input: string): ReadonlyArray => { 51 | const lines = input.split('\n').slice(1) // skip header 52 | return lines.map((line) => { 53 | const employeeData = line.split(', ') 54 | return new Employee( 55 | employeeData[0], 56 | employeeData[1], 57 | new Date(employeeData[2]), 58 | employeeData[3] 59 | ) 60 | }) 61 | } 62 | 63 | // impure 64 | const sendMessage = ( 65 | smtpHost: string, 66 | smtpPort: number, 67 | email: Email 68 | ): void => { 69 | console.log(smtpHost, smtpPort, email) 70 | } 71 | 72 | // impure 73 | const read = (fileName: string): string => { 74 | return fs.readFileSync(fileName, { encoding: 'utf8' }) 75 | } 76 | 77 | // impure 78 | const sendGreetings = ( 79 | fileName: string, 80 | today: Date, 81 | smtpHost: string, 82 | smtpPort: number 83 | ): void => { 84 | const input = read(fileName) 85 | const employees = parse(input) 86 | const emails = getGreetings(today, employees) 87 | emails.forEach((email) => sendMessage(smtpHost, smtpPort, email)) 88 | } 89 | 90 | sendGreetings( 91 | 'src/onion-architecture/employee_data.txt', 92 | new Date(2008, 9, 8), 93 | 'localhost', 94 | 80 95 | ) 96 | -------------------------------------------------------------------------------- /src/onion-architecture/3.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Secondo refactoring: inversion of control 4 | 5 | Le funzioni impure le trasformo in dipendenze 6 | 7 | */ 8 | 9 | import * as fs from 'fs' 10 | 11 | class Employee { 12 | constructor( 13 | readonly lastName: string, 14 | readonly firstName: string, 15 | readonly dateOfBirth: Date, 16 | readonly email: string 17 | ) {} 18 | isBirthday(today: Date): boolean { 19 | return ( 20 | this.dateOfBirth.getMonth() === today.getMonth() && 21 | this.dateOfBirth.getDate() === today.getDate() 22 | ) 23 | } 24 | } 25 | 26 | class Email { 27 | constructor( 28 | readonly from: string, 29 | readonly subject: string, 30 | readonly body: string, 31 | readonly recipient: string 32 | ) {} 33 | } 34 | 35 | // 36 | // ports 37 | // 38 | 39 | interface EmailService { 40 | readonly sendMessage: (email: Email) => void 41 | } 42 | 43 | interface FileSystemService { 44 | readonly read: (fileName: string) => string 45 | } 46 | 47 | interface AppService extends EmailService, FileSystemService {} 48 | 49 | // pure 50 | const toEmail = (employee: Employee): Email => { 51 | const recipient = employee.email 52 | const body = `Happy Birthday, dear ${employee.firstName}!` 53 | const subject = 'Happy Birthday!' 54 | return new Email('sender@here.com', subject, body, recipient) 55 | } 56 | 57 | // pure 58 | const getGreetings = ( 59 | today: Date, 60 | employees: ReadonlyArray 61 | ): ReadonlyArray => { 62 | return employees.filter((e) => e.isBirthday(today)).map(toEmail) 63 | } 64 | 65 | // pure 66 | const parse = (input: string): ReadonlyArray => { 67 | const lines = input.split('\n').slice(1) // skip header 68 | return lines.map((line) => { 69 | const employeeData = line.split(', ') 70 | return new Employee( 71 | employeeData[0], 72 | employeeData[1], 73 | new Date(employeeData[2]), 74 | employeeData[3] 75 | ) 76 | }) 77 | } 78 | 79 | // impure 80 | const sendGreetings = (services: AppService) => ( 81 | fileName: string, 82 | today: Date 83 | ): void => { 84 | const input = services.read(fileName) 85 | const employees = parse(input) 86 | const emails = getGreetings(today, employees) 87 | emails.forEach((email) => services.sendMessage(email)) 88 | } 89 | 90 | // 91 | // adapters 92 | // 93 | 94 | const getAppService = (smtpHost: string, smtpPort: number): AppService => { 95 | return { 96 | sendMessage: (email: Email): void => { 97 | console.log(smtpHost, smtpPort, email) 98 | }, 99 | read: (fileName: string): string => { 100 | return fs.readFileSync(fileName, { encoding: 'utf8' }) 101 | } 102 | } 103 | } 104 | 105 | const program = sendGreetings(getAppService('localhost', 80)) 106 | program('src/onion-architecture/employee_data.txt', new Date(2008, 9, 8)) 107 | /* 108 | localhost 80 Email { 109 | from: 'sender@here.com', 110 | subject: 'Happy Birthday!', 111 | body: 'Happy Birthday, dear John!', 112 | recipient: 'john.doe@foobar.com' } 113 | */ 114 | -------------------------------------------------------------------------------- /src/onion-architecture/4.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Terzo refactoring: rendere le funzioni pure con `IO` 4 | 5 | Modifichiamo la firma delle funzioni impure usando `IO` e cambiamo il nome di alcune astrazioni per renderle più idiomatiche 6 | 7 | */ 8 | 9 | import * as fs from 'fs' 10 | import * as IO from 'fp-ts/IO' 11 | import { pipe } from 'fp-ts/function' 12 | 13 | class Employee { 14 | constructor( 15 | readonly lastName: string, 16 | readonly firstName: string, 17 | readonly dateOfBirth: Date, 18 | readonly email: string 19 | ) {} 20 | isBirthday(today: Date): boolean { 21 | return ( 22 | this.dateOfBirth.getMonth() === today.getMonth() && 23 | this.dateOfBirth.getDate() === today.getDate() 24 | ) 25 | } 26 | } 27 | 28 | class Email { 29 | constructor( 30 | readonly from: string, 31 | readonly subject: string, 32 | readonly body: string, 33 | readonly recipient: string 34 | ) {} 35 | } 36 | 37 | // 38 | // type classes 39 | // 40 | 41 | interface MonadEmail { 42 | readonly sendMessage: (email: Email) => IO.IO 43 | } 44 | 45 | interface MonadFileSystem { 46 | readonly read: (fileName: string) => IO.IO 47 | } 48 | 49 | interface MonadApp extends MonadEmail, MonadFileSystem {} 50 | 51 | // pure 52 | const toEmail = (employee: Employee): Email => { 53 | const recipient = employee.email 54 | const body = `Happy Birthday, dear ${employee.firstName}!` 55 | const subject = 'Happy Birthday!' 56 | return new Email('sender@here.com', subject, body, recipient) 57 | } 58 | 59 | // pure 60 | const getGreetings = ( 61 | today: Date, 62 | employees: ReadonlyArray 63 | ): ReadonlyArray => { 64 | return employees.filter((e) => e.isBirthday(today)).map(toEmail) 65 | } 66 | 67 | // pure 68 | const parse = (input: string): ReadonlyArray => { 69 | const lines = input.split('\n').slice(1) // skip header 70 | return lines.map((line) => { 71 | const employeeData = line.split(', ') 72 | return new Employee( 73 | employeeData[0], 74 | employeeData[1], 75 | new Date(employeeData[2]), 76 | employeeData[3] 77 | ) 78 | }) 79 | } 80 | 81 | // pure 82 | const sendGreetings = (M: MonadApp) => ( 83 | fileName: string, 84 | today: Date 85 | ): IO.IO => { 86 | return pipe( 87 | M.read(fileName), 88 | IO.map((input) => getGreetings(today, parse(input))), 89 | IO.chain(IO.traverseArray(M.sendMessage)), 90 | IO.map(() => undefined) 91 | ) 92 | } 93 | 94 | // 95 | // instances 96 | // 97 | 98 | const getMonadApp = (smtpHost: string, smtpPort: number): MonadApp => { 99 | return { 100 | sendMessage: (email) => () => console.log(smtpHost, smtpPort, email), 101 | read: (fileName) => () => fs.readFileSync(fileName, { encoding: 'utf8' }) 102 | } 103 | } 104 | 105 | const program = sendGreetings(getMonadApp('localhost', 80)) 106 | program('src/onion-architecture/employee_data.txt', new Date(2008, 9, 8))() 107 | /* 108 | localhost 80 Email { 109 | from: 'sender@here.com', 110 | subject: 'Happy Birthday!', 111 | body: 'Happy Birthday, dear John!', 112 | recipient: 'john.doe@foobar.com' } 113 | */ 114 | -------------------------------------------------------------------------------- /src/onion-architecture/5.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | E se volessimo far girare il programma in un contesto asincrono? 4 | 5 | */ 6 | 7 | import * as fs from 'fs' 8 | import * as T from 'fp-ts/Task' 9 | import { pipe } from 'fp-ts/function' 10 | 11 | class Employee { 12 | constructor( 13 | readonly lastName: string, 14 | readonly firstName: string, 15 | readonly dateOfBirth: Date, 16 | readonly email: string 17 | ) {} 18 | isBirthday(today: Date): boolean { 19 | return ( 20 | this.dateOfBirth.getMonth() === today.getMonth() && 21 | this.dateOfBirth.getDate() === today.getDate() 22 | ) 23 | } 24 | } 25 | 26 | class Email { 27 | constructor( 28 | readonly from: string, 29 | readonly subject: string, 30 | readonly body: string, 31 | readonly recipient: string 32 | ) {} 33 | } 34 | 35 | // 36 | // type classes 37 | // 38 | 39 | interface MonadEmail { 40 | readonly sendMessage: (email: Email) => T.Task 41 | } 42 | 43 | interface MonadFileSystem { 44 | readonly read: (fileName: string) => T.Task 45 | } 46 | 47 | interface MonadApp extends MonadEmail, MonadFileSystem {} 48 | 49 | // pure 50 | const toEmail = (employee: Employee): Email => { 51 | const recipient = employee.email 52 | const body = `Happy Birthday, dear ${employee.firstName}!` 53 | const subject = 'Happy Birthday!' 54 | return new Email('sender@here.com', subject, body, recipient) 55 | } 56 | 57 | // pure 58 | const getGreetings = ( 59 | today: Date, 60 | employees: ReadonlyArray 61 | ): ReadonlyArray => { 62 | return employees.filter((e) => e.isBirthday(today)).map(toEmail) 63 | } 64 | 65 | // pure 66 | const parse = (input: string): ReadonlyArray => { 67 | const lines = input.split('\n').slice(1) // skip header 68 | return lines.map((line) => { 69 | const employeeData = line.split(', ') 70 | return new Employee( 71 | employeeData[0], 72 | employeeData[1], 73 | new Date(employeeData[2]), 74 | employeeData[3] 75 | ) 76 | }) 77 | } 78 | 79 | // pure 80 | const sendGreetings = (M: MonadApp) => ( 81 | fileName: string, 82 | today: Date 83 | ): T.Task => { 84 | return pipe( 85 | M.read(fileName), 86 | T.map((input) => getGreetings(today, parse(input))), 87 | T.chain(T.traverseArray(M.sendMessage)), 88 | T.map(() => undefined) 89 | ) 90 | } 91 | 92 | // 93 | // instances 94 | // 95 | 96 | const getMonadApp = (smtpHost: string, smtpPort: number): MonadApp => { 97 | return { 98 | sendMessage: (email) => () => 99 | new Promise((resolve) => { 100 | console.log('sending email...') 101 | setTimeout(() => resolve(console.log(smtpHost, smtpPort, email)), 1000) 102 | }), 103 | read: (fileName) => () => 104 | new Promise((resolve) => { 105 | console.log('reading file...') 106 | setTimeout( 107 | () => 108 | fs.readFile(fileName, { encoding: 'utf8' }, (_, data) => 109 | resolve(data) 110 | ), 111 | 1000 112 | ) 113 | }) 114 | } 115 | } 116 | 117 | const program = sendGreetings(getMonadApp('localhost', 80)) 118 | program('src/onion-architecture/employee_data.txt', new Date(2008, 9, 8))() 119 | /* 120 | reading file... 121 | sending email... 122 | localhost 80 Email { 123 | from: 'sender@here.com', 124 | subject: 'Happy Birthday!', 125 | body: 'Happy Birthday, dear John!', 126 | recipient: 'john.doe@foobar.com' } 127 | */ 128 | -------------------------------------------------------------------------------- /src/onion-architecture/6.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | E se volessimo far girare il programma in un qualsiasi contesto monadico? 4 | 5 | */ 6 | 7 | import * as fs from 'fs' 8 | import * as T from 'fp-ts/Task' 9 | import * as RA from 'fp-ts/ReadonlyArray' 10 | import { URIS, Kind } from 'fp-ts/HKT' 11 | import { Monad1 } from 'fp-ts/Monad' 12 | import { Applicative1 } from 'fp-ts/Applicative' 13 | import { constVoid, pipe } from 'fp-ts/function' 14 | 15 | class Employee { 16 | constructor( 17 | readonly lastName: string, 18 | readonly firstName: string, 19 | readonly dateOfBirth: Date, 20 | readonly email: string 21 | ) {} 22 | isBirthday(today: Date): boolean { 23 | return ( 24 | this.dateOfBirth.getMonth() === today.getMonth() && 25 | this.dateOfBirth.getDate() === today.getDate() 26 | ) 27 | } 28 | } 29 | 30 | class Email { 31 | constructor( 32 | readonly from: string, 33 | readonly subject: string, 34 | readonly body: string, 35 | readonly recipient: string 36 | ) {} 37 | } 38 | 39 | // 40 | // type classes 41 | // 42 | 43 | interface MonadEmail1 { 44 | readonly sendMessage: (email: Email) => Kind 45 | } 46 | 47 | interface MonadFileSystem1 { 48 | readonly read: (fileName: string) => Kind 49 | } 50 | 51 | interface MonadApp 52 | extends MonadEmail1, 53 | MonadFileSystem1, 54 | Monad1, 55 | Applicative1 {} 56 | 57 | // pure 58 | const toEmail = (employee: Employee): Email => { 59 | const recipient = employee.email 60 | const body = `Happy Birthday, dear ${employee.firstName}!` 61 | const subject = 'Happy Birthday!' 62 | return new Email('sender@here.com', subject, body, recipient) 63 | } 64 | 65 | // pure 66 | const getGreetings = ( 67 | today: Date, 68 | employees: ReadonlyArray 69 | ): ReadonlyArray => { 70 | return employees.filter((e) => e.isBirthday(today)).map(toEmail) 71 | } 72 | 73 | // pure 74 | const parse = (input: string): ReadonlyArray => { 75 | const lines = input.split('\n').slice(1) // skip header 76 | return lines.map((line) => { 77 | const employeeData = line.split(', ') 78 | return new Employee( 79 | employeeData[0], 80 | employeeData[1], 81 | new Date(employeeData[2]), 82 | employeeData[3] 83 | ) 84 | }) 85 | } 86 | 87 | // pure 88 | const sendGreetings = (M: MonadApp) => ( 89 | fileName: string, 90 | today: Date 91 | ): Kind => { 92 | return M.map( 93 | M.chain( 94 | M.map(M.read(fileName), (input) => getGreetings(today, parse(input))), 95 | RA.traverse(M)(M.sendMessage) 96 | ), 97 | constVoid 98 | ) 99 | } 100 | 101 | // 102 | // instances 103 | // 104 | 105 | const getMonadApp = (smtpHost: string, smtpPort: number): MonadApp => { 106 | return { 107 | ...T.Monad, 108 | ...T.ApplicativePar, 109 | sendMessage: (email) => () => 110 | new Promise((resolve) => { 111 | console.log('sending email...') 112 | setTimeout(() => resolve(console.log(smtpHost, smtpPort, email)), 1000) 113 | }), 114 | read: (fileName) => () => 115 | new Promise((resolve) => { 116 | console.log('reading file...') 117 | setTimeout( 118 | () => 119 | fs.readFile(fileName, { encoding: 'utf8' }, (_, data) => 120 | resolve(data) 121 | ), 122 | 1000 123 | ) 124 | }) 125 | } 126 | } 127 | 128 | const program = sendGreetings(getMonadApp('localhost', 80)) 129 | program('src/onion-architecture/employee_data.txt', new Date(2008, 9, 8))() 130 | /* 131 | reading file... 132 | sending email... 133 | localhost 80 Email { 134 | from: 'sender@here.com', 135 | subject: 'Happy Birthday!', 136 | body: 'Happy Birthday, dear John!', 137 | recipient: 'john.doe@foobar.com' } 138 | */ 139 | -------------------------------------------------------------------------------- /src/onion-architecture/employee_data.txt: -------------------------------------------------------------------------------- 1 | last_name, first_name, date_of_birth, email 2 | Doe, John, 1982/10/08, john.doe@foobar.com 3 | Ann, Mary, 1975/09/11, mary.ann@foobar.com -------------------------------------------------------------------------------- /src/ord-modeling/README.md: -------------------------------------------------------------------------------- 1 | ## `Ord` 를 활용한 순서 관계 모델링 2 | 3 | 이전 `Eq` 챕터에서 **동등**의 개념을 다루었습니다. 이번에는 **순서**에 대한 개념을 다루고자 합니다. 4 | 5 | 전순서 관계는 Typescript 로 다음과 같이 구현할 수 있습니다: 6 | 7 | ```typescript 8 | import { Eq } from 'fp-ts/lib/Eq' 9 | 10 | type Ordering = -1 | 0 | 1 11 | 12 | interface Ord extends Eq { 13 | readonly compare: (x: A, y: A) => Ordering 14 | } 15 | ``` 16 | 17 | 결과적으로: 18 | 19 | - `x < y` if and only if `compare(x, y) = -1` 20 | - `x = y` if and only if `compare(x, y) = 0` 21 | - `x > y` if and only if `compare(x, y) = 1` 22 | 23 | > `if and only if` 는 주어진 두 명제가 필요충분조건이라는 뜻입니다 24 | 25 | **Example** 26 | 27 | `number` 타입에 대한 `Ord` 인스턴스를 만들어봅시다: 28 | 29 | ```typescript 30 | import { Ord } from 'fp-ts/Ord' 31 | 32 | const OrdNumber: Ord = { 33 | equals: (first, second) => first === second, 34 | compare: (first, second) => (first < second ? -1 : first > second ? 1 : 0) 35 | } 36 | ``` 37 | 38 | 다음과 같은 조건을 만족합니다: 39 | 40 | 1. **반사적**: `A` 의 모든 `x` 에 대해 `compare(x, x) <= 0` 이다 41 | 2. **반대칭적**: `A` 의 모든 `x`, `y` 에 대해 만약 `compare(x, y) <= 0` 이고 `compare(y, x) <= 0` 이면 `x = y` 이다 42 | 3. **추이적**: `A` 의 모든 `x`, `y`, `z` 에 대해 만약 `compare(x, y) <= 0` 이고 `compare(y, z) <= 0` 이면 `compare(x, z) <= 0` 이다 43 | 44 | `Eq` 의 `equals` 연산자는 `compare` 연산자와도 호환됩니다: 45 | 46 | `A` 의 모든 `x`, `y` 에 대해 다음 두 명제는 필요충분조건입니다 47 | 48 | `compare(x, y) === 0` if and only if `equals(x, y) === true` 49 | 50 | **참고**. `equals` 는 `compare` 를 활용해 다음 방법으로 도출할 수 있습니다: 51 | 52 | ```typescript 53 | equals: (first, second) => compare(first, second) === 0 54 | ``` 55 | 56 | 사실 `fp-ts/Ord` 모듈에 있는 `fromComapre` 하는 경우 `compare` 함수만 제공하면 `Ord` 인스턴스를 만들어줍니다: 57 | 58 | ```typescript 59 | import { Ord, fromCompare } from 'fp-ts/Ord' 60 | 61 | const OrdNumber: Ord = fromCompare((first, second) => 62 | first < second ? -1 : first > second ? 1 : 0 63 | ) 64 | ``` 65 | 66 | **문제**. 가위바위보 게임에 대한 `Ord` 인스턴스를 정의할 수 있을까요? `move1 <= move2` 이면 `move2` 가 `move1` 를 이깁니다. 67 | 68 | `ReadonlyArray` 요소의 정렬을 위한 `sort` 함수를 만들어보면서 `Ord` 인스턴스의 실용적인 사용법을 확인해봅시다. 69 | 70 | ```typescript 71 | import { pipe } from 'fp-ts/function' 72 | import * as N from 'fp-ts/number' 73 | import { Ord } from 'fp-ts/Ord' 74 | 75 | export const sort = (O: Ord) => ( 76 | as: ReadonlyArray 77 | ): ReadonlyArray => as.slice().sort(O.compare) 78 | 79 | pipe([3, 1, 2], sort(N.Ord), console.log) // => [1, 2, 3] 80 | ``` 81 | 82 | **문제** (JavaScript). 왜 `sort` 구현에서 내장 배열 메소드 `slice` 를 사용한걸까요? 83 | 84 | 주어진 두 값중 작은것을 반환하는 `min` 함수를 만들어보면서 또 다른 `Ord` 활용법을 살펴봅시다: 85 | 86 | ```typescript 87 | import { pipe } from 'fp-ts/function' 88 | import * as N from 'fp-ts/number' 89 | import { Ord } from 'fp-ts/Ord' 90 | 91 | const min = (O: Ord) => (second: A) => (first: A): A => 92 | O.compare(first, second) === 1 ? second : first 93 | 94 | pipe(2, min(N.Ord)(1), console.log) // => 1 95 | ``` 96 | -------------------------------------------------------------------------------- /src/program5.ts: -------------------------------------------------------------------------------- 1 | import * as TE from 'fp-ts/TaskEither' 2 | import { pipe } from 'fp-ts/function' 3 | 4 | // ----------------------------------------- 5 | // effetto del nostro programma 6 | // ----------------------------------------- 7 | 8 | interface FileSystem extends TE.TaskEither {} 9 | 10 | // ----------------------------------------- 11 | // dipendenze 12 | // ----------------------------------------- 13 | 14 | interface Deps { 15 | readonly readFile: (filename: string) => FileSystem 16 | readonly writeFile: (filename: string, data: string) => FileSystem 17 | readonly log: (a: A) => FileSystem 18 | readonly chain: ( 19 | f: (a: A) => FileSystem 20 | ) => (ma: FileSystem) => FileSystem 21 | } 22 | 23 | // ----------------------------------------- 24 | // programma 25 | // ----------------------------------------- 26 | 27 | const program5 = (D: Deps) => { 28 | const modifyFile = (filename: string, f: (s: string) => string) => 29 | pipe( 30 | D.readFile(filename), 31 | D.chain((s) => D.writeFile(filename, f(s))) 32 | ) 33 | 34 | return pipe( 35 | D.readFile('file.txt'), 36 | D.chain(D.log), 37 | D.chain(() => modifyFile('file.txt', (s) => s + '\n// eof')), 38 | D.chain(() => D.readFile('file.txt')), 39 | D.chain(D.log) 40 | ) 41 | } 42 | 43 | // ----------------------------------------- 44 | // istanza per `Deps` 45 | // ----------------------------------------- 46 | 47 | import * as fs from 'fs' 48 | import { log } from 'fp-ts/Console' 49 | 50 | const readFile = TE.taskify(fs.readFile) 51 | 52 | const DepsAsync: Deps = { 53 | readFile: (filename) => readFile(filename, 'utf-8'), 54 | writeFile: TE.taskify(fs.writeFile), 55 | log: (a) => TE.fromIO(log(a)), 56 | chain: TE.chain 57 | } 58 | 59 | // dependency injection 60 | program5(DepsAsync)().then(console.log) 61 | -------------------------------------------------------------------------------- /src/pure-and-partial-functions/README.md: -------------------------------------------------------------------------------- 1 | # 순수함수와 부분함수 2 | 3 | 첫 번째 챕터에서 순수함수에 대한 비공식적인 정의를 보았습니다: 4 | 5 | > 순수함수란 같은 입력에 항상 같은 결과를 내는 관찰 가능한 부작용없는 절차입니다. 6 | 7 | 위와같은 비공식적 문구를 보고 다음과 같은 의문점이 생길 수 있습니다: 8 | 9 | - "부작용"이란 무엇인가? 10 | - "관찰가능하다"는 것은 무엇을 의미하는가? 11 | - "같다"라는게 무엇을 의미하는가? 12 | 13 | 이 함수의 공식적인 정의를 살펴봅시다. 14 | 15 | **참고**. 만약 `X` 와 `Y` 가 집합이면, `X × Y` 은 _곱집합_ 이라 불리며 다음과 같은 집합을 의미합니다 16 | 17 | ``` 18 | X × Y = { (x, y) | x ∈ X, y ∈ Y } 19 | ``` 20 | 21 | 다음 [정의](https://en.wikipedia.org/wiki/History_of_the_function_concept) 는 한 세기 전에 만들어졌습니다: 22 | 23 | **정의**. 함수 `f: X ⟶ Y` 는 `X × Y` 의 부분집합이면서 다음 조건을 만족합니다, 24 | 모든 `x ∈ X` 에 대해 `(x, y) ∈ f` 를 만족하는 오직 하나의 `y ∈ Y` 가 존재합니다. 25 | 26 | 집합 `X` 는 함수 `f` 의 _정의역_ 이라 하며, `Y` 는 `f` 의 _공역_ 이라 합니다. 27 | 28 | **예제** 29 | 30 | 함수 `double: Nat ⟶ Nat` 곱집합 `Nat × Nat` 의 부분집합이며 형태는 다음과 같습니다: `{ (1, 2), (2, 4), (3, 6), ...}` 31 | 32 | Typescript 에서는 `f` 를 다음과 같이 정의할 수 있습니다 33 | 34 | ```typescript 35 | const f: Record = { 36 | 1: 2, 37 | 2: 4, 38 | 3: 6 39 | ... 40 | } 41 | ``` 42 | 43 | 48 | 49 | 위 예제는 함수의 _확장적_ 정의라고 불리며, 이는 정의역의 요소들을 꺼내 그것들 각각에 대해 공역의 원소에 대응하는 것을 의미합니다. 50 | 51 | 당연하게도, 이러한 집합이 무한이라면 문제가 발생한다는 것을 알 수 있습니다. 모든 함수의 정의역과 공역을 나열할 수 없기 때문입니다. 52 | 53 | 이 문제는 _조건적_ 정의라고 불리는 것을 통해 해결할 수 있습니다. 예를들면 `(x, y) ∈ f` 인 모든 순서쌍에 대해 `y = x * 2` 가 만족한다는 조건을 표현하는 것입니다. 54 | 55 | TypeScript 에서 다음과 같은 익숙한 형태인 `double` 함수의 정의를 볼 수 있습니다: 56 | 57 | ```typescript 58 | const double = (x: number): number => x * 2 59 | ``` 60 | 61 | 곱집합의 부분집합으로서의 함수의 정의는 수작적으로 어떻게 모든 함수가 순수한지 보여줍니다: 어떠한 작용도, 상태 변경과 요소의 수정도 없습니다. 62 | 함수형 프로그래밍에서 함수의 구현은 가능한 한 이상적인 모델을 따라야합니다. 63 | 64 | **문제**. 다음 절차(procedure) 중 순수 함수는 무엇일까요? 65 | 66 | ```typescript 67 | const coefficient1 = 2 68 | export const f1 = (n: number) => n * coefficient1 69 | 70 | // ------------------------------------------------------ 71 | 72 | let coefficient2 = 2 73 | export const f2 = (n: number) => n * coefficient2++ 74 | 75 | // ------------------------------------------------------ 76 | 77 | let coefficient3 = 2 78 | export const f3 = (n: number) => n * coefficient3 79 | 80 | // ------------------------------------------------------ 81 | 82 | export const f4 = (n: number) => { 83 | const out = n * 2 84 | console.log(out) 85 | return out 86 | } 87 | 88 | // ------------------------------------------------------ 89 | 90 | interface User { 91 | readonly id: number 92 | readonly name: string 93 | } 94 | 95 | export declare const f5: (id: number) => Promise 96 | 97 | // ------------------------------------------------------ 98 | 99 | import * as fs from 'fs' 100 | 101 | export const f6 = (path: string): string => 102 | fs.readFileSync(path, { encoding: 'utf8' }) 103 | 104 | // ------------------------------------------------------ 105 | 106 | export const f7 = ( 107 | path: string, 108 | callback: (err: Error | null, data: string) => void 109 | ): void => fs.readFile(path, { encoding: 'utf8' }, callback) 110 | ``` 111 | 112 | 함수가 순수하다는 것이 꼭 **지역변수의 변경** (local mutability)을 금지한다는 의미는 아닙니다. 변수가 함수 범위를 벗어나지 않는다면 변경할 수 있습니다. 113 | 114 | ![mutable / immutable](../images/mutable-immutable.jpg) 115 | 116 | **예제** (monoid 용 `concatALl` 함수 구현) 117 | 118 | ```typescript 119 | import { Monoid } from 'fp-ts/Monoid' 120 | 121 | const concatAll = (M: Monoid) => (as: ReadonlyArray): A => { 122 | let out: A = M.empty // <= local mutability 123 | for (const a of as) { 124 | out = M.concat(out, a) 125 | } 126 | return out 127 | } 128 | ``` 129 | 130 | 궁극적인 목적은 **참조 투명성** 을 만족하는 것입니다. 131 | 132 | 우리의 API 사용자와의 계약은 API 의 signature 와 참조 투명성을 준수하겠다는 약속에 의해 정의됩니다. 133 | 134 | ```typescript 135 | declare const concatAll: (M: Monoid) => (as: ReadonlyArray) => A 136 | ``` 137 | 138 | 함수가 구현되는 방법에 대한 기술적 세부 사항은 관련이 없으므로, 구현 측면에서 많은 자유를 누릴 수 있습니다. 139 | 140 | 그렇다면, 우리는 부작용을 어떻게 정의해야 할까요? 단순히 참조 투명성 정의의 반대가 됩니다: 141 | 142 | > 어떤 표현식이 참조 투명성을 지키지 않는다면 "부작용" 을 가지고 있습니다 143 | 144 | 함수는 함수형 프로그래밍의 두 가지 요소 중 하나인 참조 투명성의 완벽한 예시일 뿐만 아니라, 두 번째 요소한 **합성** 의 예시이기도 합니다. 145 | 146 | 함수 합성: 147 | 148 | **정의**. 두 함수 `f: Y ⟶ Z` 와 `g: X ⟶ Y` 에 대해, 함수 `h: X ⟶ Z` 는 다음과 같이 정의된다: 149 | 150 | ``` 151 | h(x) = f(g(x)) 152 | ``` 153 | 154 | 이는 `f` 와 `g` 의 _합성_ 이라하며 `h = f ∘ g` 라고 쓴다. 155 | 156 | `f` 와 `g` 를 합성하려면, `f` 의 정의역이 `g` 의 공역에 포함되어야 한다는 점에 유의하시기 바랍니다. 157 | 158 | **정의**. 정의역의 하나 이상의 값에 대해 정의되지 않는 함수는 _부분함수_ 라고 합니다. 159 | 160 | 반대로, 정의역의 모든 원소에 대해 정의된 함수는 _전체함수_ 라고 합니다. 161 | 162 | **예제** 163 | 164 | ``` 165 | f(x) = 1 / x 166 | ``` 167 | 168 | 함수 `f: number ⟶ number` 는 `x = 0` 에 대해서는 정의되지 않습니다. 169 | 170 | **예제** 171 | 172 | ```typescript 173 | // `ReadonlyArray` 의 첫 번째 요소를 얻습니다 174 | declare const head: (as: ReadonlyArray) => A 175 | ``` 176 | 177 | **문제**. 왜 `head` 는 부분함수 인가요? 178 | 179 | **문제**. `JSON.parse` 는 전체함수 일까요? 180 | 181 | ```typescript 182 | parse: (text: string, reviver?: (this: any, key: string, value: any) => any) => 183 | any 184 | ``` 185 | 186 | **문제**. `JSON.stringify` 는 전체함수 일까요? 187 | 188 | ```typescript 189 | stringify: ( 190 | value: any, 191 | replacer?: (this: any, key: string, value: any) => any, 192 | space?: string | number 193 | ) => string 194 | ``` 195 | 196 | 함수형 프로그래밍에서는 **순수 및 전체함수** 만 정의하는 경향이 있습니다. 지금부터 함수라는 용어는 "순수하면서 전체함수" 를 의미합니다. 그렇다면 애플리케이션에 부분함수가 있다면 어떻게 해야 할까요? 197 | 198 | 부분함수 `f: X ⟶ Y` 는 특별한 값(`None` 이라 부르겠습니다)을 공역에 더함으로써 언제나 전체함수로 되돌릴 수 있습니다. 199 | 정의되지 않은 모든 `X` 의 값을 `None` 에 대응하면 됩니다. 200 | 201 | ``` 202 | f': X ⟶ Y ∪ None 203 | ``` 204 | 205 | `Y ∪ None` 는 `Option(Y)` 와 동일하다고 정의한다면 206 | 207 | ``` 208 | f': X ⟶ Option(Y) 209 | ``` 210 | 211 | TypeScript 에서 `Option` 을 정의할 수 있을까요? 다음 장에서 그 방법을 살펴보겠습니다. 212 | -------------------------------------------------------------------------------- /src/semigroup-modeling/README.md: -------------------------------------------------------------------------------- 1 | # Semigroup 으로 합성 모델링 2 | 3 | semigroup 은 두 개 이상의 값을 조합하는 설계도입니다. 4 | 5 | semigroup 은 **대수 (algebra)** 이며, 다음과 같은 조합으로 정의됩니다. 6 | 7 | - 하나 이상의 집합 8 | - 해당 집합에 대한 하나 이상의 연산 9 | - 이전 연산에 대한 0개 이상의 법칙 10 | 11 | 대수학은 수학자들이 어떤 개념을 불필요한 모든 것을 제거한 가장 순수한 형태로 만드려는 방법입니다. 12 | 13 | > 대수는 자신의 법칙에 따라 대수 그 자체로 정의되는 연산에 의해서만 변경이 허용된다. 14 | > 15 | > (원문) When an algebra is modified the only allowed operations are those defined by the algebra itself according to its own laws 16 | 17 | 대수학은 **인터페이스** 의 추상화로 생각할 수 있습니다. 18 | 19 | > 인터페이스는 자신의 법칙에 따라 인터페이스 그 자체로 정의되는 연산에 의해서만 변경이 허용된다. 20 | > 21 | > (원문) When an interface is modified the only allowed operations are those defined by the interface itself according to its own laws 22 | 23 | semigroups 에 대해 알아보기 전에, 첫 대수의 예인 _magma_ 를 살펴봅시다. 24 | -------------------------------------------------------------------------------- /src/semigroup-modeling/concat-all.md: -------------------------------------------------------------------------------- 1 | ## `concatAll` 함수 2 | 3 | 정의상 `concat` 은 단지 2개의 요소 `A` 를 조합합니다. 몇 개라도 조합이 가능할까요? 4 | 5 | `concatAll` 함수는 다음 값을 요구합니다: 6 | 7 | - semigroup 인스턴스 8 | - 초기값 9 | - 요소의 배열 10 | 11 | ```typescript 12 | import * as S from 'fp-ts/Semigroup' 13 | import * as N from 'fp-ts/number' 14 | 15 | const sum = S.concatAll(N.SemigroupSum)(2) 16 | 17 | console.log(sum([1, 2, 3, 4])) // => 12 18 | 19 | const product = S.concatAll(N.SemigroupProduct)(3) 20 | 21 | console.log(product([1, 2, 3, 4])) // => 72 22 | ``` 23 | 24 | **문제**. 왜 초기값을 제공해야 할까요? 25 | 26 | **예제** 27 | 28 | Javascript 기본 라이브러리의 유명한 함수 몇가지를 `concatAll` 으로 구현해봅시다. 29 | 30 | ```typescript 31 | import * as B from 'fp-ts/boolean' 32 | import { concatAll } from 'fp-ts/Semigroup' 33 | import * as S from 'fp-ts/struct' 34 | 35 | const every = (predicate: (a: A) => boolean) => ( 36 | as: ReadonlyArray 37 | ): boolean => concatAll(B.SemigroupAll)(true)(as.map(predicate)) 38 | 39 | const some = (predicate: (a: A) => boolean) => ( 40 | as: ReadonlyArray 41 | ): boolean => concatAll(B.SemigroupAny)(false)(as.map(predicate)) 42 | 43 | const assign: (as: ReadonlyArray) => object = concatAll( 44 | S.getAssignSemigroup() 45 | )({}) 46 | ``` 47 | 48 | **문제**. 다음 인스턴스는 semigroup 법칙을 만족합니까? 49 | 50 | ```typescript 51 | import { Semigroup } from 'fp-ts/Semigroup' 52 | 53 | /** 항상 첫 번째 인자를 반환 */ 54 | const first = (): Semigroup => ({ 55 | concat: (first, _second) => first 56 | }) 57 | ``` 58 | 59 | **문제**. 다음 인스턴스는 semigroup 법칙을 만족합니까? 60 | 61 | ```typescript 62 | import { Semigroup } from 'fp-ts/Semigroup' 63 | 64 | /** 항상 두 번째 인자를 반환 */ 65 | const last = (): Semigroup => ({ 66 | concat: (_first, second) => second 67 | }) 68 | ``` 69 | -------------------------------------------------------------------------------- /src/semigroup-modeling/dual-semigroup.md: -------------------------------------------------------------------------------- 1 | ## Dual semigroup 2 | 3 | semigroup 인스턴스가 주어지면, 단순히 조합되는 피연산자의 순서를 변경해 새로운 semigroup 인스턴스를 얻을 수 있습니다. 4 | 5 | ```typescript 6 | import { pipe } from 'fp-ts/function' 7 | import { Semigroup } from 'fp-ts/Semigroup' 8 | import * as S from 'fp-ts/string' 9 | 10 | // Semigroup combinator 11 | const reverse = (S: Semigroup): Semigroup => ({ 12 | concat: (first, second) => S.concat(second, first) 13 | }) 14 | 15 | pipe(S.Semigroup.concat('a', 'b'), console.log) // => 'ab' 16 | pipe(reverse(S.Semigroup).concat('a', 'b'), console.log) // => 'ba' 17 | ``` 18 | 19 | **문제**. 위 `reverse` 는 유효한 combinator 이지만, 일반적으로 `concat` 연산은 [**교환법칙**](https://en.wikipedia.org/wiki/Commutative_property) 을 만족하지 않습니다, 교환법칙을 만족하는 `concat` 과 그렇지 않은것을 찾을 수 있습니까? 20 | -------------------------------------------------------------------------------- /src/semigroup-modeling/find-semigroup.md: -------------------------------------------------------------------------------- 1 | ## 임의의 타입에 대한 semigroup 인스턴스 찾기 2 | 3 | 결합법칙은 매우 까다로운 조건이기 때문에, 만약 어떤 타입 `A` 에 대한 결합법칙을 만족하는 연산을 찾을 수 없다면 어떻게될까요? 4 | 5 | 아래와 같은 `User` 를 정의했다고 가정합시다: 6 | 7 | ```typescript 8 | type User = { 9 | readonly id: number 10 | readonly name: string 11 | } 12 | ``` 13 | 그리고 데이터베이스에는 같은 `User` 에 대한 여러 복사본이 있다고 가정합니다 (예를들면 수정이력일 수 있습니다) 14 | 15 | ```typescript 16 | // 내부 API 17 | declare const getCurrent: (id: number) => User 18 | declare const getHistory: (id: number) => ReadonlyArray 19 | ``` 20 | 21 | 그리고 다음 외부 API 를 구현해야합니다. 22 | 23 | ```typescript 24 | export declare const getUser: (id: number) => User 25 | ``` 26 | 27 | API 는 다음 조건에 따라 적절한 `User` 를 가져와야 합니다. 조건은 가장 최근 또는 가장 오래된, 아니면 현재 값 등이 될 수 있습니다. 28 | 29 | 보통은 다음처럼 각 조건에 따라 여러 API 를 만들 수 있습니다: 30 | 31 | ```typescript 32 | export declare const getMostRecentUser: (id: number) => User 33 | export declare const getLeastRecentUser: (id: number) => User 34 | export declare const getCurrentUser: (id: number) => User 35 | // etc... 36 | ``` 37 | 38 | 따라서, `User` 를 반환하기 위해 모든 복사본에 대한 `병합` (이나 `선택`)이 필요합니다. 이는 조건에 대한 문제를 `Semigroup` 로 다룰 수 있다는 것을 의미합니다. 39 | 40 | 그렇지만, 아직 "두 `User`를 병합하기"가 어떤 의미인지, 그리고 해당 병합 연산이 결합법칙을 만족하는지 알기 쉽지 않습니다. 41 | 42 | 주어진 **어떠한** 타입 `A` 에 대해서도 **항상** semigroup 인스턴스를 만들 수 있습니다. `A` 자체에 대한 인스턴스가 아닌 `NonEmptyArray` 의 인스턴스로 만들 수 있으며 이는 `A` 의 **free semigroup** 이라고 불립니다. 43 | 44 | ```typescript 45 | import { Semigroup } from 'fp-ts/Semigroup' 46 | 47 | // 적어도 하나의 A 의 요소가 있는 배열을 표현합니다 48 | type ReadonlyNonEmptyArray = ReadonlyArray & { 49 | readonly 0: A 50 | } 51 | 52 | // 비어있지 않은 두 배열을 합해도 여전히 비어있지 않은 배열입니다 53 | const getSemigroup = (): Semigroup> => ({ 54 | concat: (first, second) => [first[0], ...first.slice(1), ...second] 55 | }) 56 | ``` 57 | 58 | 그러면 `A` 의 요소 `ReadonlyNonEmptyArray` 의 "싱글톤" 으로 만들 수 있으며 이는 하나를 하나의 요소만 있는 배열을 의미합니다. 59 | 60 | ```typescript 61 | // 비어있지 않은 배열에 값 하나를 넣습니다 62 | const of = (a: A): ReadonlyNonEmptyArray => [a] 63 | ``` 64 | 65 | 이 방식을 `User` 타입에도 적용해봅시다: 66 | 67 | ```typescript 68 | import { 69 | getSemigroup, 70 | of, 71 | ReadonlyNonEmptyArray 72 | } from 'fp-ts/ReadonlyNonEmptyArray' 73 | import { Semigroup } from 'fp-ts/Semigroup' 74 | 75 | type User = { 76 | readonly id: number 77 | readonly name: string 78 | } 79 | 80 | // 이 semigroup 은 `User` 타입이 아닌 `ReadonlyNonEmptyArray` 를 위한 것입니다 81 | const S: Semigroup> = getSemigroup() 82 | 83 | declare const user1: User 84 | declare const user2: User 85 | declare const user3: User 86 | 87 | // 병합: ReadonlyNonEmptyArray 88 | const merge = S.concat(S.concat(of(user1), of(user2)), of(user3)) 89 | 90 | // 배열에 직접 user 를 넣어서 같은 결과를 얻을 수 있습니다. 91 | const merge2: ReadonlyNonEmptyArray = [user1, user2, user3] 92 | ``` 93 | 94 | 따라서, `A` 의 free semigroup 이란 비어있지 않은 모든 유한 순열을 다루는 semigroup 일 뿐입니다. 95 | 96 | `A` 의 free semigroup 이란 데이터 내용을 유지한채로 `A` 의 요소들의 `결합`을 _게으른_ 방법으로 처리하는 것으로 볼 수 있습니다. 97 | 98 | 이전 예제에서 `[user1, user2, user3]` 을 가지는 `merge` 상수는 어떤 요소가 어떤 순서로 결합되어 있는지 알려줍니다. 99 | 100 | 이제 `getUser` API 설계를 위한 세 가지 옵션이 있습니다: 101 | 102 | 1. `Semigroup` 를 정의하고 바로 `병합`한다. 103 | 104 | ```typescript 105 | declare const SemigroupUser: Semigroup 106 | 107 | export const getUser = (id: number): User => { 108 | const current = getCurrent(id) 109 | const history = getHistory(id) 110 | return concatAll(SemigroupUser)(current)(history) 111 | } 112 | ``` 113 | 114 | 2. `Semigroup` 을 직접 정의하는 대신 병합 전략을 외부에서 구현하게 한다. 즉 API 사용자가 제공하도록 한다. 115 | 116 | ```typescript 117 | export const getUser = (SemigroupUser: Semigroup) => ( 118 | id: number 119 | ): User => { 120 | const current = getCurrent(id) 121 | const history = getHistory(id) 122 | // 바로 병합 123 | return concatAll(SemigroupUser)(current)(history) 124 | } 125 | ``` 126 | 127 | 3. `Semigroup` 를 정의할 수 없고 외부로 부터 제공받지 않는다. 128 | 129 | 이럴 때에는 `User` 의 free semigroup 을 활용합니다: 130 | 131 | ```typescript 132 | export const getUser = (id: number): ReadonlyNonEmptyArray => { 133 | const current = getCurrent(id) 134 | const history = getHistory(id) 135 | // 병합을 진행하지 않고 User 의 free semigroup 을 반환한다 136 | return [current, ...history] 137 | } 138 | ``` 139 | 140 | `Semigroup` 인스턴스를 만들수 있는 상황일 지라도 다음과 같은 이유로 free semigroup 을 사용하는게 여전히 유용할 수 있습니다: 141 | 142 | - 비싸고 무의미한 계산을 하지 않음 143 | - semigroup 인스턴스를 직접 사용하지 않음 144 | - API 사용자에게 (`concatAll` 을 사용해) 어떤 병합전략이 좋을지 결정할 수 있게함 145 | -------------------------------------------------------------------------------- /src/semigroup-modeling/magma.md: -------------------------------------------------------------------------------- 1 | ## Magma 의 정의 2 | 3 | `Magma` 는 매우 간단한 대수입니다: 4 | 5 | - 타입 (A) 의 집합 6 | - `concat` 연산 7 | - 지켜야 할 법칙은 없음 8 | 9 | **참고**: 대부분의 경우 _set_ 과 _type_ 은 같은 의미로 사용됩니다. 10 | 11 | Magma 를 정의하기 위해 Typescript 의 `interface` 를 활용할 수 있습니다. 12 | 13 | ```typescript 14 | interface Magma { 15 | readonly concat: (first: A, second: A) => A 16 | } 17 | ``` 18 | 19 | 이를통해, 대수를 위한 재료를 얻게됩니다. 20 | 21 | - 집합 `A` 22 | - 집합 `A` 에 대한 연산인 `concat`. 이 연산은 집합 `A` 에 대해 _닫혀있다_ 고 말합니다. 임의의 `A` 요소에 대해 연산의 결과도 항상 `A` 이며 이 값은 다시 `concat` 의 입력으로 쓸 수 있습니다. `concat` 은 다른 말로 타입 `A` 의 `combinator` 입니다. 23 | 24 | `Magma` 의 구체적인 인스턴스 하나를 구현해봅니다. 여기서 `A` 는 `number` 입니다. 25 | 26 | ```typescript 27 | import { Magma } from 'fp-ts/Magma' 28 | 29 | const MagmaSub: Magma = { 30 | concat: (first, second) => first - second 31 | } 32 | 33 | // helper 34 | const getPipeableConcat = (M: Magma) => (second: A) => (first: A): A => 35 | M.concat(first, second) 36 | 37 | const concat = getPipeableConcat(MagmaSub) 38 | 39 | // 사용 예제 40 | 41 | import { pipe } from 'fp-ts/function' 42 | 43 | pipe(10, concat(2), concat(3), concat(1), concat(2), console.log) 44 | // => 2 45 | ``` 46 | 47 | **문제**. 위 `concat` 이 _닫힌_ 연산이라는 점은 쉽게 알 수 있습니다. 만약 `A` JavaScript 의 number(양수와 음수 float 집합)가 아닌 가 자연수의 집합(양수로 정의된) 이라면, `MagmaSub` 에 구현된 `concat` 과 같은 연산을 가진 `Magma` 을 정의할 수 있을까요? 자연수에 대해 닫혀있지 않는 또다른 `concat` 연산을 생각해 볼 수 있나요? 48 | 49 | **정의**. 주어진 `A` 가 공집합이 아니고 이항연산 `*` 가 `A` 에 대해 닫혀있다면, 쌍 `(A, *)` 를 _magma_라 합니다. 50 | 51 | Magma 는 닫힘 조건외에 다른 법칙을 요구하지 않습니다. 이제 semigroup 이라는 추가 법칙을 요구하는 대수를 살펴봅시다. 52 | -------------------------------------------------------------------------------- /src/semigroup-modeling/order-derivable-semigroup.md: -------------------------------------------------------------------------------- 1 | ## Order-derivable Semigroups 2 | 3 | 만약 주어진 `number` 가 **total order** (전순서 집합, 어떤 임의의 `x` 와 `y` 를 선택해도, 다음 두 조건 중 하나가 참이다: `x <= y` 또는 `y <= x`) 라면 `min` 또는 `max` 연산을 활용해 또 다른 두 개의 `Semigroup` 인스턴스를 얻을 수 있습니다. 4 | 5 | ```typescript 6 | import { Semigroup } from 'fp-ts/Semigroup' 7 | 8 | const SemigroupMin: Semigroup = { 9 | concat: (first, second) => Math.min(first, second) 10 | } 11 | 12 | const SemigroupMax: Semigroup = { 13 | concat: (first, second) => Math.max(first, second) 14 | } 15 | ``` 16 | 17 | **문제**. 왜 `number` 가 _total order_ 이어야 할까요? 18 | 19 | 이러한 두 semigroup (`SemigroupMin` 과 `SemigroupMax`) 을 `number` 외 다른 타입에 대해서 정의한다면 유용할 것입니다. 20 | 21 | 다른 타입에 대해서도 _전순서 집합_ 이라는 개념을 적용할 수 있을까요? 22 | 23 | _순서_ 의 개념을 설명하기 앞서 _동등_ 의 개념을 생각할 필요가 있습니다. 24 | -------------------------------------------------------------------------------- /src/semigroup-modeling/semigroup-product.md: -------------------------------------------------------------------------------- 1 | ## Semigroup product 2 | 3 | 더 복잡한 semigroup 인스턴스를 정의해봅시다: 4 | 5 | ```typescript 6 | import * as N from 'fp-ts/number' 7 | import { Semigroup } from 'fp-ts/Semigroup' 8 | 9 | // 정점에서 시작하는 vector 를 모델링 10 | type Vector = { 11 | readonly x: number 12 | readonly y: number 13 | } 14 | 15 | // 두 vector 의 합을 모델링 16 | const SemigroupVector: Semigroup = { 17 | concat: (first, second) => ({ 18 | x: N.SemigroupSum.concat(first.x, second.x), 19 | y: N.SemigroupSum.concat(first.y, second.y) 20 | }) 21 | } 22 | ``` 23 | 24 | **예제** 25 | 26 | ```typescript 27 | const v1: Vector = { x: 1, y: 1 } 28 | const v2: Vector = { x: 1, y: 2 } 29 | 30 | console.log(SemigroupVector.concat(v1, v2)) // => { x: 2, y: 3 } 31 | ``` 32 | 33 | ![SemigroupVector](../images/semigroupVector.png) 34 | 35 | boilerplate 코드가 너무 많나요? 좋은 소식은 semigroup 의 **수학적 법칙**에 따르면 각 필드에 대한 semigroup 인스턴스를 만들 수 있다면 `Vector` 같은 구조체의 semigroup 인스턴스를 만들 수 있습니다. 36 | 37 | 편리하게도 `fp-ts/Semigroup` 모듈은 `struct` combinator 를 제공합니다: 38 | 39 | ```typescript 40 | import { struct } from 'fp-ts/Semigroup' 41 | 42 | // 두 vector 의 합을 모델링 43 | const SemigroupVector: Semigroup = struct({ 44 | x: N.SemigroupSum, 45 | y: N.SemigroupSum 46 | }) 47 | ``` 48 | 49 | **참고**. `struct` 와 유사한 tuple 에 대해 동작하는 combinator 도 존재합니다: `tuple` 50 | 51 | ```typescript 52 | import * as N from 'fp-ts/number' 53 | import { Semigroup, tuple } from 'fp-ts/Semigroup' 54 | 55 | // 정점에서 시작하는 vector 모델링 56 | type Vector = readonly [number, number] 57 | 58 | // 두 vector 의 합을 모델링 59 | const SemigroupVector: Semigroup = tuple(N.SemigroupSum, N.SemigroupSum) 60 | 61 | const v1: Vector = [1, 1] 62 | const v2: Vector = [1, 2] 63 | 64 | console.log(SemigroupVector.concat(v1, v2)) // => [2, 3] 65 | ``` 66 | 67 | **문제**. 만약 임의의 `Semigroup` 와 `A` 의 임의의 값 middle 을 두 `concat` 인자 사이에 넣도록 만든 인스턴스는 여전히 semigroup 일까요? 68 | 69 | ```typescript 70 | import { pipe } from 'fp-ts/function' 71 | import { Semigroup } from 'fp-ts/Semigroup' 72 | import * as S from 'fp-ts/string' 73 | 74 | export const intercalate = (middle: A) => ( 75 | S: Semigroup 76 | ): Semigroup => ({ 77 | concat: (first, second) => S.concat(S.concat(first, middle), second) 78 | }) 79 | 80 | const SemigroupIntercalate = pipe(S.Semigroup, intercalate('|')) 81 | 82 | pipe( 83 | SemigroupIntercalate.concat('a', SemigroupIntercalate.concat('b', 'c')), 84 | console.log 85 | ) // => 'a|b|c' 86 | ``` 87 | -------------------------------------------------------------------------------- /src/semigroup-modeling/semigroup.md: -------------------------------------------------------------------------------- 1 | ## Semigroup 의 정의 2 | 3 | > 어떤 `Magma` 의 `concat` 연산이 **결합법칙**을 만족하면 _semigroup_ 이다. 4 | 5 | 여기서 "결합법칙" 은 `A` 의 모든 `x`, `y`, `z` 에 대해 다음 등식이 성립하는 것을 의미합니다: 6 | 7 | ```typescript 8 | (x * y) * z = x * (y * z) 9 | 10 | // or 11 | concat(concat(a, b), c) = concat(a, concat(b, c)) 12 | ``` 13 | 14 | 쉽게 말하면 _결합법칙_은 표현식에서 괄호를 신경쓸 필요없이 단순히 `x * y * z` 로 쓸 수 있다는 사실을 알려줍니다. 15 | 16 | **예제** 17 | 18 | 문자열 연결은 결합법칙을 만족합니다. 19 | 20 | ```typescript 21 | ("a" + "b") + "c" = "a" + ("b" + "c") = "abc" 22 | ``` 23 | 24 | 모든 semigroup 은 magma 입니다, 하지만 모든 magma 가 semigroup 인것은 아닙니다. 25 | 26 | ![Magma vs Semigroup](../images/semigroup.png) 27 | 28 | **예제** 29 | 30 | 이전 예제 `MagmaSub` 는 `concat` 이 결합법칙을 만족하지 않기에 semigroup 이 아닙니다. 31 | 32 | ```typescript 33 | import { pipe } from 'fp-ts/function' 34 | import { Magma } from 'fp-ts/Magma' 35 | 36 | const MagmaSub: Magma = { 37 | concat: (first, second) => first - second 38 | } 39 | 40 | pipe(MagmaSub.concat(MagmaSub.concat(1, 2), 3), console.log) // => -4 41 | pipe(MagmaSub.concat(1, MagmaSub.concat(2, 3)), console.log) // => 2 42 | ``` 43 | 44 | Semigroup 은 병렬 연산이 가능하다는 의미를 내포합니다 45 | > (원문) Semigroups capture the essence of parallelizable operations 46 | 47 | 어떤 계산이 결합법칙을 만족한다는 것을 안다면, 계산을 두 개의 하위 계산으로 더 분할할 수 있고, 각각의 계산은 하위 계산으로 더 분할될 수 있습니다. 48 | 49 | ```typescript 50 | a * b * c * d * e * f * g * h = ((a * b) * (c * d)) * ((e * f) * (g * h)) 51 | ``` 52 | 53 | 하위 계산은 병렬로 실행할 수 있습니다. 54 | 55 | `Magga` 처럼, `Semigroup` 도 Typescript `interface` 로 정의할 수 있습니다: 56 | 57 | ```typescript 58 | // fp-ts/lib/Semigroup.ts 59 | 60 | interface Semigroup extends Magma {} 61 | ``` 62 | 63 | 다음 법칙을 만족해야 합니다: 64 | 65 | - **결합법칙**: 만약 `S` 가 semigroup 이면 타입 `A` 의 모든 `x`, `y`, `z` 에 대해 다음 등식이 성립합니다 66 | 67 | ```typescript 68 | S.concat(S.concat(x, y), z) = S.concat(x, S.concat(y, z)) 69 | ``` 70 | 71 | **참고**. 안타깝게도 Typescript 의 타입시스템 만으론 이 법칙을 강제할 수 없습니다. 72 | 73 | `ReadonlyArray` 에 대한 semigroup 을 구현해봅시다: 74 | 75 | ```typescript 76 | import * as Se from 'fp-ts/Semigroup' 77 | 78 | const Semigroup: Se.Semigroup> = { 79 | concat: (first, second) => first.concat(second) 80 | } 81 | ``` 82 | 83 | `concat` 이란 이름은 (이후 알게 되겠지만) 배열에 대해서는 적절합니다. 하지만 인스턴스를 만드려는 타입 `A` 와 문맥에 따라, `concat` 연산은 아래와 같은 다른 해석과 의미를 가질 수 있습니다. 84 | > (원문) The name `concat` makes sense for arrays (as we'll see later) but, depending on the context and the type `A` on whom we're implementing an instance, the `concat` semigroup operation may have different interpretations and meanings: 85 | 86 | - "concatenation" 87 | - "combination" 88 | - "merging" 89 | - "fusion" 90 | - "selection" 91 | - "sum" 92 | - "substitution" 93 | 94 | **예제** 95 | 96 | 다음은 semigroup `(number, +)` 을 정의한 것입니다. 여기서 `+` 는 숫자에 대한 덧셈을 의미합니다: 97 | 98 | ```typescript 99 | import { Semigroup } from 'fp-ts/Semigroup' 100 | 101 | /** 덧셈에 대한 number `Semigroup` */ 102 | const SemigroupSum: Semigroup = { 103 | concat: (first, second) => first + second 104 | } 105 | ``` 106 | 107 | **문제**. 이전 데모 의 [`01_retry.ts`](../01_retry.ts) 에 정의된 `concat` combinator 를 `RetryPolicy` 타입에 대한 `Semigroup` 인스턴스로 정의할 수 있을까요? 108 | 109 | 다음은 semigroup `(number, *)` 을 정의한 것입니다. 여기서 `*` 는 숫자에 대한 덧셈을 의미합니다: 110 | 111 | ```typescript 112 | import { Semigroup } from 'fp-ts/Semigroup' 113 | 114 | /** 곱셈에 대한 number `Semigroup` */ 115 | const SemigroupProduct: Semigroup = { 116 | concat: (first, second) => first * second 117 | } 118 | ``` 119 | 120 | **참고** 흔히 _number 의 semigroup_ 에 한정지어 생각하곤 하지만, 임의의 타입 `A` 에 대해 다른 `Semigroup` **인스턴스**를 정의하는 것도 가능합니다. `number` 타입의 _덧셈_ 과 _곱셈_ 연산에 대한 semigroup 을 정의한것처럼 다른 타입에 대해 같은 연산으로 `Semigroup` 을 만들 수 있습니다. 예들들어 `SemigoupSum` 은 `number` 와 같은 타입대신 자연수에 대해서도 구현할 수 있습니다. 121 | > (원문) It is a common mistake to think about the _semigroup of numbers_, but for the same type `A` it is possible to define more **instances** of `Semigroup`. We've seen how for `number` we can define a semigroup under _addition_ and _multiplication_. It is also possible to have `Semigroup`s that share the same operation but differ in types. `SemigroupSum` could've been implemented on natural numbers instead of unsigned floats like `number`. 122 | 123 | `string` 타입에 대한 다른 예제입니다: 124 | 125 | ```typescript 126 | import { Semigroup } from 'fp-ts/Semigroup' 127 | 128 | const SemigroupString: Semigroup = { 129 | concat: (first, second) => first + second 130 | } 131 | ``` 132 | 133 | 이번에는 `boolean` 타입에 대한 또 다른 2개의 에제입니다: 134 | 135 | ```typescript 136 | import { Semigroup } from 'fp-ts/Semigroup' 137 | 138 | const SemigroupAll: Semigroup = { 139 | concat: (first, second) => first && second 140 | } 141 | 142 | const SemigroupAny: Semigroup = { 143 | concat: (first, second) => first || second 144 | } 145 | ``` 146 | -------------------------------------------------------------------------------- /src/shapes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/solutions/ADT01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modellare un albero binario completo, il/i costruttore/i, la funzione di pattern matching 3 | * e una funzione che converte l'albero in un `ReadonlyArray` 4 | */ 5 | export type BinaryTree = 6 | | { readonly _tag: 'Leaf'; readonly value: A } 7 | | { 8 | readonly _tag: 'Node' 9 | readonly value: A 10 | readonly left: BinaryTree 11 | readonly right: BinaryTree 12 | } 13 | 14 | export const leaf = (value: A): BinaryTree => ({ _tag: 'Leaf', value }) 15 | 16 | export const node = ( 17 | value: A, 18 | left: BinaryTree, 19 | right: BinaryTree 20 | ): BinaryTree => ({ _tag: 'Node', value, left, right }) 21 | 22 | export const fold = ( 23 | onLeaf: (a: A) => B, 24 | onNode: (value: A, left: BinaryTree, right: BinaryTree) => B 25 | ) => (tree: BinaryTree): B => { 26 | switch (tree._tag) { 27 | case 'Leaf': 28 | return onLeaf(tree.value) 29 | case 'Node': 30 | return onNode(tree.value, tree.left, tree.right) 31 | } 32 | } 33 | 34 | export const toReadonlyArray: ( 35 | tree: BinaryTree 36 | ) => ReadonlyArray = fold( 37 | (value) => [value], 38 | (value, left, right) => 39 | [value].concat(toReadonlyArray(left)).concat(toReadonlyArray(right)) 40 | ) 41 | -------------------------------------------------------------------------------- /src/solutions/ADT02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modellare il punteggio di un game di tennis 3 | */ 4 | 5 | // ------------------------------------ 6 | // model 7 | // ------------------------------------ 8 | 9 | type Player = 'A' | 'B' 10 | 11 | type Score = 0 | 15 | 30 | 40 12 | 13 | type Game = 14 | | { readonly _tag: 'Score'; readonly A: Score; readonly B: Score } 15 | | { readonly _tag: 'Advantage'; readonly player: Player } 16 | | { readonly _tag: 'Deuce' } 17 | | { readonly _tag: 'Game'; readonly player: Player } 18 | 19 | // ------------------------------------ 20 | // constructors 21 | // ------------------------------------ 22 | 23 | const score = (A: Score, B: Score): Game => ({ _tag: 'Score', A, B }) 24 | const advantage = (player: Player): Game => ({ _tag: 'Advantage', player }) 25 | const deuce: Game = { _tag: 'Deuce' } 26 | const game = (player: Player): Game => ({ _tag: 'Game', player }) 27 | 28 | /** 29 | * Punteggio di partenza 30 | */ 31 | const start: Game = { 32 | _tag: 'Score', 33 | A: 0, 34 | B: 0 35 | } 36 | 37 | // ------------------------------------ 38 | // destructors 39 | // ------------------------------------ 40 | 41 | const fold = ( 42 | onScore: (scoreA: Score, scoreB: Score) => R, 43 | onAdvantage: (player: Player) => R, 44 | onDeuce: () => R, 45 | onGame: (player: Player) => R 46 | ) => (game: Game): R => { 47 | switch (game._tag) { 48 | case 'Score': 49 | return onScore(game.A, game.B) 50 | case 'Advantage': 51 | return onAdvantage(game.player) 52 | case 'Deuce': 53 | return onDeuce() 54 | case 'Game': 55 | return onGame(game.player) 56 | } 57 | } 58 | 59 | import * as O from 'fp-ts/Option' 60 | 61 | const next = (score: Score): O.Option => { 62 | switch (score) { 63 | case 0: 64 | return O.some(15) 65 | case 15: 66 | return O.some(30) 67 | case 30: 68 | return O.some(40) 69 | case 40: 70 | return O.none 71 | } 72 | } 73 | 74 | /** 75 | * Dato un punteggio e un giocatore che si è aggiudicato il punto restituisce il nuovo punteggio 76 | */ 77 | const win = (player: Player): ((game: Game) => Game) => 78 | fold( 79 | (A, B) => 80 | pipe( 81 | next(player === 'A' ? A : B), 82 | O.match( 83 | (): Game => (A === B ? advantage(player) : game(player)), 84 | (next) => (player === 'A' ? score(next, B) : score(A, next)) 85 | ) 86 | ), 87 | (current) => (player === current ? game(player) : deuce), 88 | () => advantage(player), 89 | game 90 | ) 91 | 92 | /** 93 | * Restituisce il punteggio in formato leggibile 94 | */ 95 | const show: (game: Game) => string = fold( 96 | (A, B) => `${A} - ${B === A ? 'all' : B}`, 97 | (player) => `advantage player ${player}`, 98 | () => 'deuce', 99 | (player) => `game player ${player}` 100 | ) 101 | 102 | // ------------------------------------ 103 | // tests 104 | // ------------------------------------ 105 | 106 | import * as assert from 'assert' 107 | import { pipe } from 'fp-ts/function' 108 | 109 | assert.deepStrictEqual( 110 | pipe(start, win('A'), win('A'), win('A'), win('A'), show), 111 | 'game player A' 112 | ) 113 | 114 | const fifteenAll = pipe(start, win('A'), win('B')) 115 | assert.deepStrictEqual(pipe(fifteenAll, show), '15 - all') 116 | 117 | const fourtyAll = pipe(fifteenAll, win('A'), win('B'), win('A'), win('B')) 118 | assert.deepStrictEqual(pipe(fourtyAll, show), '40 - all') 119 | 120 | const advantageA = pipe(fourtyAll, win('A')) 121 | assert.deepStrictEqual(pipe(advantageA, show), 'advantage player A') 122 | 123 | assert.deepStrictEqual(pipe(advantageA, win('B'), show), 'deuce') 124 | -------------------------------------------------------------------------------- /src/solutions/ADT03.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Rifattorizzare il seguente codice in modo da eliminare l'errore di compilazione. 4 | 5 | */ 6 | import { flow } from 'fp-ts/function' 7 | import { match, Option } from 'fp-ts/Option' 8 | 9 | interface User { 10 | readonly username: string 11 | } 12 | 13 | declare const queryByUsername: (username: string) => Option 14 | 15 | // ------------------------------------- 16 | // model 17 | // ------------------------------------- 18 | 19 | interface Ok { 20 | readonly code: 200 21 | readonly body: A 22 | } 23 | interface NotFound { 24 | readonly code: 404 25 | readonly message: string 26 | } 27 | type HttpResponse = Ok | NotFound 28 | 29 | // ------------------------------------- 30 | // constructors 31 | // ------------------------------------- 32 | 33 | const ok = (body: A): HttpResponse => ({ code: 200, body }) 34 | const notFound = (message: string): HttpResponse => ({ 35 | code: 404, 36 | message 37 | }) 38 | 39 | // ------------------------------------- 40 | // API 41 | // ------------------------------------- 42 | 43 | export const getByUsername: (username: string) => HttpResponse = flow( 44 | queryByUsername, 45 | match(() => notFound('User not found.'), ok) 46 | ) 47 | -------------------------------------------------------------------------------- /src/solutions/Applicative01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * E' possibile derivare una istanza di `Monoid` da una istanza di `Applicative`? 3 | */ 4 | import { Monoid } from 'fp-ts/Monoid' 5 | import * as O from 'fp-ts/Option' 6 | import * as S from 'fp-ts/string' 7 | import { pipe } from 'fp-ts/function' 8 | 9 | const getMonoid = (M: Monoid): Monoid> => ({ 10 | concat: (first, second) => 11 | pipe( 12 | first, 13 | O.map((a: A) => (b: A) => M.concat(a, b)), 14 | O.ap(second) 15 | ), 16 | empty: O.some(M.empty) 17 | }) 18 | 19 | // ------------------------------------ 20 | // tests 21 | // ------------------------------------ 22 | 23 | import * as assert from 'assert' 24 | 25 | const M = getMonoid(S.Monoid) 26 | 27 | assert.deepStrictEqual(M.concat(O.none, O.none), O.none) 28 | assert.deepStrictEqual(M.concat(O.some('a'), O.none), O.none) 29 | assert.deepStrictEqual(M.concat(O.none, O.some('a')), O.none) 30 | assert.deepStrictEqual(M.concat(O.some('a'), O.some('b')), O.some('ab')) 31 | assert.deepStrictEqual(M.concat(O.some('a'), M.empty), O.some('a')) 32 | assert.deepStrictEqual(M.concat(M.empty, O.some('a')), O.some('a')) 33 | -------------------------------------------------------------------------------- /src/solutions/Apply01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * E' possibile derivare una istanza di `Semigroup` da una istanza di `Apply`? 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as O from 'fp-ts/Option' 6 | import * as S from 'fp-ts/string' 7 | import { pipe } from 'fp-ts/function' 8 | 9 | const getSemigroup = (S: Semigroup): Semigroup> => ({ 10 | concat: (first, second) => 11 | pipe( 12 | first, 13 | O.map((a: A) => (b: A) => S.concat(a, b)), 14 | O.ap(second) 15 | ) 16 | }) 17 | 18 | // ------------------------------------ 19 | // tests 20 | // ------------------------------------ 21 | 22 | import * as assert from 'assert' 23 | 24 | const SO = getSemigroup(S.Semigroup) 25 | 26 | assert.deepStrictEqual(SO.concat(O.none, O.none), O.none) 27 | assert.deepStrictEqual(SO.concat(O.some('a'), O.none), O.none) 28 | assert.deepStrictEqual(SO.concat(O.none, O.some('a')), O.none) 29 | assert.deepStrictEqual(SO.concat(O.some('a'), O.some('b')), O.some('ab')) 30 | -------------------------------------------------------------------------------- /src/solutions/Eq01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Eq` per `ReadonlyArray` 3 | */ 4 | import { Eq, fromEquals } from 'fp-ts/Eq' 5 | import * as N from 'fp-ts/number' 6 | 7 | export const getEq = (E: Eq): Eq> => 8 | fromEquals( 9 | (first, second) => 10 | first.length === second.length && 11 | first.every((x, i) => E.equals(x, second[i])) 12 | ) 13 | 14 | // ------------------------------------ 15 | // tests 16 | // ------------------------------------ 17 | 18 | import * as assert from 'assert' 19 | 20 | const E = getEq(N.Eq) 21 | 22 | const as: ReadonlyArray = [1, 2, 3] 23 | 24 | assert.deepStrictEqual(E.equals(as, [1]), false) 25 | assert.deepStrictEqual(E.equals(as, [1, 2]), false) 26 | assert.deepStrictEqual(E.equals(as, [1, 2, 3, 4]), false) 27 | assert.deepStrictEqual(E.equals(as, [1, 2, 3]), true) 28 | -------------------------------------------------------------------------------- /src/solutions/Eq02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Eq` per `Tree` 3 | */ 4 | import { Eq, fromEquals } from 'fp-ts/Eq' 5 | import * as S from 'fp-ts/string' 6 | import * as A from 'fp-ts/ReadonlyArray' 7 | 8 | type Forest = ReadonlyArray> 9 | 10 | interface Tree { 11 | readonly value: A 12 | readonly forest: Forest 13 | } 14 | 15 | const getEq = (E: Eq): Eq> => { 16 | const R: Eq> = fromEquals( 17 | (first, second) => 18 | E.equals(first.value, second.value) && 19 | SA.equals(first.forest, second.forest) 20 | ) 21 | const SA = A.getEq(R) 22 | return R 23 | } 24 | 25 | // ------------------------------------ 26 | // tests 27 | // ------------------------------------ 28 | 29 | import * as assert from 'assert' 30 | 31 | const make = (value: A, forest: Forest = []): Tree => ({ 32 | value, 33 | forest 34 | }) 35 | 36 | const E = getEq(S.Eq) 37 | 38 | const t = make('a', [make('b'), make('c')]) 39 | 40 | assert.deepStrictEqual(E.equals(t, make('a')), false) 41 | assert.deepStrictEqual(E.equals(t, make('a', [make('b')])), false) 42 | assert.deepStrictEqual(E.equals(t, make('a', [make('b'), make('d')])), false) 43 | assert.deepStrictEqual( 44 | E.equals(t, make('a', [make('b'), make('c'), make('d')])), 45 | false 46 | ) 47 | assert.deepStrictEqual(E.equals(t, make('a', [make('b'), make('c')])), true) 48 | -------------------------------------------------------------------------------- /src/solutions/Eq03.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Modellare un orologio (minuti e ore) 3 | * 4 | * Per completare l'esercizio occorre definire il tipo `Clock`, una sia istanza di `Eq` 5 | */ 6 | import { Eq, tuple } from 'fp-ts/Eq' 7 | import * as N from 'fp-ts/number' 8 | 9 | type Hour = 10 | | 0 11 | | 1 12 | | 2 13 | | 3 14 | | 4 15 | | 5 16 | | 6 17 | | 7 18 | | 8 19 | | 9 20 | | 10 21 | | 11 22 | | 12 23 | | 13 24 | | 14 25 | | 15 26 | | 16 27 | | 17 28 | | 18 29 | | 19 30 | | 20 31 | | 21 32 | | 22 33 | | 23 34 | 35 | const hour = (h: number): Hour => (Math.floor(h) % 24) as any 36 | 37 | type Minute = 38 | | 0 39 | | 1 40 | | 2 41 | | 3 42 | | 4 43 | | 5 44 | | 6 45 | | 7 46 | | 8 47 | | 9 48 | | 10 49 | | 11 50 | | 12 51 | | 13 52 | | 14 53 | | 15 54 | | 16 55 | | 17 56 | | 18 57 | | 19 58 | | 20 59 | | 21 60 | | 22 61 | | 23 62 | | 24 63 | | 25 64 | | 26 65 | | 27 66 | | 28 67 | | 29 68 | | 30 69 | | 31 70 | | 32 71 | | 33 72 | | 34 73 | | 35 74 | | 36 75 | | 37 76 | | 38 77 | | 39 78 | | 40 79 | | 41 80 | | 42 81 | | 43 82 | | 44 83 | | 45 84 | | 46 85 | | 47 86 | | 48 87 | | 49 88 | | 50 89 | | 51 90 | | 52 91 | | 53 92 | | 54 93 | | 55 94 | | 46 95 | | 57 96 | | 58 97 | | 59 98 | 99 | const minute = (m: number): Minute => (Math.floor(m) % 60) as any 100 | 101 | // It's a 24 hour clock going from "00:00" to "23:59". 102 | type Clock = [Hour, Minute] 103 | 104 | const eqClock: Eq = tuple(N.Eq, N.Eq) 105 | 106 | // takes an hour and minute, and returns an instance of Clock with those hours and minutes 107 | const fromHourMin = (h: number, m: number): Clock => [hour(h), minute(m)] 108 | 109 | // ------------------------------------ 110 | // tests 111 | // ------------------------------------ 112 | 113 | import * as assert from 'assert' 114 | 115 | assert.deepStrictEqual( 116 | eqClock.equals(fromHourMin(0, 0), fromHourMin(24, 0)), 117 | true 118 | ) 119 | assert.deepStrictEqual( 120 | eqClock.equals(fromHourMin(12, 30), fromHourMin(36, 30)), 121 | true 122 | ) 123 | -------------------------------------------------------------------------------- /src/solutions/FEH01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Semigroup` per `Option` 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import { Option, some, none, isSome } from 'fp-ts/Option' 6 | import * as S from 'fp-ts/string' 7 | 8 | const getSemigroup = (S: Semigroup): Semigroup> => ({ 9 | concat: (first, second) => 10 | isSome(first) && isSome(second) 11 | ? some(S.concat(first.value, second.value)) 12 | : none 13 | }) 14 | 15 | // ------------------------------------ 16 | // tests 17 | // ------------------------------------ 18 | 19 | import * as assert from 'assert' 20 | 21 | const SO = getSemigroup(S.Semigroup) 22 | 23 | assert.deepStrictEqual(SO.concat(none, none), none) 24 | assert.deepStrictEqual(SO.concat(some('a'), none), none) 25 | assert.deepStrictEqual(SO.concat(none, some('b')), none) 26 | assert.deepStrictEqual(SO.concat(some('a'), some('b')), some('ab')) 27 | -------------------------------------------------------------------------------- /src/solutions/FEH02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Monoid` per `Option` 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as N from 'fp-ts/number' 6 | import * as O from 'fp-ts/Option' 7 | import { Monoid, concatAll } from 'fp-ts/Monoid' 8 | 9 | const getMonoid = (S: Semigroup): Monoid> => ({ 10 | concat: (first, second) => 11 | O.isNone(first) 12 | ? second 13 | : O.isNone(second) 14 | ? first 15 | : O.some(S.concat(first.value, second.value)), 16 | empty: O.none 17 | }) 18 | 19 | // ------------------------------------ 20 | // tests 21 | // ------------------------------------ 22 | 23 | import * as assert from 'assert' 24 | 25 | const M = getMonoid(N.SemigroupSum) 26 | 27 | assert.deepStrictEqual(M.concat(O.none, O.none), O.none) 28 | assert.deepStrictEqual(M.concat(O.some(1), O.none), O.some(1)) 29 | assert.deepStrictEqual(M.concat(O.none, O.some(2)), O.some(2)) 30 | assert.deepStrictEqual(M.concat(O.some(1), O.some(2)), O.some(3)) 31 | assert.deepStrictEqual(M.concat(O.some(1), M.empty), O.some(1)) 32 | assert.deepStrictEqual(M.concat(M.empty, O.some(2)), O.some(2)) 33 | 34 | assert.deepStrictEqual( 35 | concatAll(M)([O.some(1), O.some(2), O.none, O.some(3)]), 36 | O.some(6) 37 | ) 38 | -------------------------------------------------------------------------------- /src/solutions/FEH03.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Semigroup` per `Either` 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import { Either, right, left, isLeft } from 'fp-ts/Either' 6 | import * as S from 'fp-ts/string' 7 | 8 | const getSemigroup = (S: Semigroup): Semigroup> => ({ 9 | concat: (first, second) => 10 | isLeft(first) 11 | ? first 12 | : isLeft(second) 13 | ? second 14 | : right(S.concat(first.right, second.right)) 15 | }) 16 | 17 | // ------------------------------------ 18 | // tests 19 | // ------------------------------------ 20 | 21 | import * as assert from 'assert' 22 | 23 | const SE = getSemigroup(S.Semigroup) 24 | 25 | assert.deepStrictEqual(SE.concat(left(1), left(2)), left(1)) 26 | assert.deepStrictEqual(SE.concat(right('a'), left(2)), left(2)) 27 | assert.deepStrictEqual(SE.concat(left(1), right('b')), left(1)) 28 | assert.deepStrictEqual(SE.concat(right('a'), right('b')), right('ab')) 29 | -------------------------------------------------------------------------------- /src/solutions/FEH04.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Semigroup` per `Either` che accumula gli errori 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as N from 'fp-ts/number' 6 | import { Either, right, left, isLeft } from 'fp-ts/Either' 7 | import * as S from 'fp-ts/string' 8 | 9 | const getSemigroup = ( 10 | SE: Semigroup, 11 | SA: Semigroup 12 | ): Semigroup> => ({ 13 | concat: (first, second) => 14 | isLeft(first) 15 | ? isLeft(second) 16 | ? left(SE.concat(first.left, second.left)) 17 | : first 18 | : isLeft(second) 19 | ? second 20 | : right(SA.concat(first.right, second.right)) 21 | }) 22 | 23 | // ------------------------------------ 24 | // tests 25 | // ------------------------------------ 26 | 27 | import * as assert from 'assert' 28 | 29 | const SE = getSemigroup(N.SemigroupSum, S.Semigroup) 30 | 31 | assert.deepStrictEqual(SE.concat(left(1), left(2)), left(3)) 32 | assert.deepStrictEqual(SE.concat(right('a'), left(2)), left(2)) 33 | assert.deepStrictEqual(SE.concat(left(1), right('b')), left(1)) 34 | assert.deepStrictEqual(SE.concat(right('a'), right('b')), right('ab')) 35 | -------------------------------------------------------------------------------- /src/solutions/FEH05.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convertire la funzione `parseJSON` in stile funzionale usando l'implementazione di `Either` in `fp-ts` 3 | */ 4 | import { right, left, Either } from 'fp-ts/Either' 5 | import { Json } from 'fp-ts/Json' 6 | 7 | export const parseJSON = (input: string): Either => { 8 | try { 9 | return right(JSON.parse(input)) 10 | } catch (e) { 11 | return left(new SyntaxError()) 12 | } 13 | } 14 | 15 | // ------------------------------------ 16 | // tests 17 | // ------------------------------------ 18 | 19 | import * as assert from 'assert' 20 | 21 | assert.deepStrictEqual(parseJSON('1'), right(1)) 22 | assert.deepStrictEqual(parseJSON('"a"'), right('a')) 23 | assert.deepStrictEqual(parseJSON('{}'), right({})) 24 | assert.deepStrictEqual(parseJSON('{'), left(new SyntaxError())) 25 | -------------------------------------------------------------------------------- /src/solutions/Functor01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare l'istanza di `Functor` per `IO` 3 | */ 4 | import { URI } from 'fp-ts/IO' 5 | import { Functor1 } from 'fp-ts/Functor' 6 | 7 | const Functor: Functor1 = { 8 | URI, 9 | map: (fa, f) => () => f(fa()) 10 | } 11 | 12 | // ------------------------------------ 13 | // tests 14 | // ------------------------------------ 15 | 16 | import * as assert from 'assert' 17 | 18 | const double = (n: number): number => n * 2 19 | 20 | assert.deepStrictEqual(Functor.map(() => 1, double)(), 2) 21 | -------------------------------------------------------------------------------- /src/solutions/Functor02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare l'istanza di `Functor` per `Either` 3 | */ 4 | import { URI, right, left, isLeft } from 'fp-ts/Either' 5 | import { Functor2 } from 'fp-ts/Functor' 6 | 7 | const Functor: Functor2 = { 8 | URI, 9 | map: (fa, f) => (isLeft(fa) ? fa : right(f(fa.right))) 10 | } 11 | 12 | // ------------------------------------ 13 | // tests 14 | // ------------------------------------ 15 | 16 | import * as assert from 'assert' 17 | 18 | const double = (n: number): number => n * 2 19 | 20 | assert.deepStrictEqual(Functor.map(right(1), double), right(2)) 21 | assert.deepStrictEqual(Functor.map(left('a'), double), left('a')) 22 | -------------------------------------------------------------------------------- /src/solutions/Functor03.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare le seguenti funzioni 3 | */ 4 | import { pipe } from 'fp-ts/function' 5 | import { IO, map } from 'fp-ts/IO' 6 | 7 | /** 8 | * Returns a random number between 0 (inclusive) and 1 (exclusive). This is a direct wrapper around JavaScript's 9 | * `Math.random()`. 10 | */ 11 | export const random: IO = () => Math.random() 12 | 13 | /** 14 | * Takes a range specified by `low` (the first argument) and `high` (the second), and returns a random integer uniformly 15 | * distributed in the closed interval `[low, high]`. It is unspecified what happens if `low > high`, or if either of 16 | * `low` or `high` is not an integer. 17 | */ 18 | export const randomInt = (low: number, high: number): IO => 19 | pipe( 20 | random, 21 | map((n) => Math.floor((high - low + 1) * n + low)) 22 | ) 23 | 24 | /** 25 | * Returns a random element in `as` 26 | */ 27 | export const randomElem = (as: ReadonlyArray): IO => 28 | pipe( 29 | randomInt(0, as.length - 1), 30 | map((i) => as[i]) 31 | ) 32 | -------------------------------------------------------------------------------- /src/solutions/Magma01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare la funzione `fromReadonlyArray` 3 | */ 4 | import { Magma } from 'fp-ts/Magma' 5 | 6 | const fromReadonlyArray = (M: Magma) => ( 7 | as: ReadonlyArray 8 | ): Readonly> => { 9 | const out: Record = {} 10 | for (const [k, a] of as) { 11 | if (out.hasOwnProperty(k)) { 12 | out[k] = M.concat(out[k], a) 13 | } else { 14 | out[k] = a 15 | } 16 | } 17 | return out 18 | } 19 | 20 | // ------------------------------------ 21 | // tests 22 | // ------------------------------------ 23 | 24 | import * as assert from 'assert' 25 | 26 | const magmaSum: Magma = { 27 | concat: (first, second) => first + second 28 | } 29 | 30 | // una istanza di Magma che semplicemente ignora il primo argomento 31 | const lastMagma: Magma = { 32 | concat: (_first, second) => second 33 | } 34 | 35 | // una istanza di Magma che semplicemente ignora il secondo argomento 36 | const firstMagma: Magma = { 37 | concat: (first, _second) => first 38 | } 39 | 40 | const input: ReadonlyArray = [ 41 | ['a', 1], 42 | ['b', 2], 43 | ['a', 3] 44 | ] 45 | 46 | assert.deepStrictEqual(fromReadonlyArray(magmaSum)(input), { a: 4, b: 2 }) 47 | assert.deepStrictEqual(fromReadonlyArray(lastMagma)(input), { a: 3, b: 2 }) 48 | assert.deepStrictEqual(fromReadonlyArray(firstMagma)(input), { a: 1, b: 2 }) 49 | -------------------------------------------------------------------------------- /src/solutions/Monad01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire l'istanza di `Monad` per `Either` 3 | */ 4 | import { Monad2 } from 'fp-ts/Monad' 5 | import * as E from 'fp-ts/Either' 6 | 7 | const Monad: Monad2 = { 8 | URI: E.URI, 9 | map: (fa, f) => (E.isLeft(fa) ? fa : E.right(f(fa.right))), 10 | of: E.right, 11 | ap: (fab, fa) => 12 | E.isLeft(fab) ? fab : E.isLeft(fa) ? fa : E.right(fab.right(fa.right)), 13 | chain: (ma, f) => (E.isLeft(ma) ? ma : f(ma.right)) 14 | } 15 | 16 | // ------------------------------------ 17 | // tests 18 | // ------------------------------------ 19 | 20 | import * as assert from 'assert' 21 | 22 | assert.deepStrictEqual( 23 | Monad.map(Monad.of(1), (n: number) => n * 2), 24 | E.right(2) 25 | ) 26 | assert.deepStrictEqual( 27 | Monad.chain(Monad.of(1), (n: number) => 28 | n > 0 ? Monad.of(n * 2) : E.left('error') 29 | ), 30 | E.right(2) 31 | ) 32 | assert.deepStrictEqual( 33 | Monad.chain(Monad.of(-1), (n: number) => 34 | n > 0 ? Monad.of(n * 2) : E.left('error') 35 | ), 36 | E.left('error') 37 | ) 38 | -------------------------------------------------------------------------------- /src/solutions/Monad02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire l'istanza di `Monad` per `type TaskEither = Task>` 3 | */ 4 | import * as T from 'fp-ts/Task' 5 | import * as E from 'fp-ts/Either' 6 | import { Monad2 } from 'fp-ts/Monad' 7 | import { URI } from 'fp-ts/TaskEither' 8 | import { pipe } from 'fp-ts/function' 9 | 10 | const Monad: Monad2 = { 11 | URI: URI, 12 | map: (fa, f) => pipe(fa, T.map(E.map(f))), 13 | of: (a) => T.of(E.of(a)), 14 | ap: (fab, fa) => () => 15 | Promise.all([fab(), fa()]).then(([eab, ea]) => pipe(eab, E.ap(ea))), 16 | chain: (ma, f) => pipe(ma, T.chain(E.match((e) => T.of(E.left(e)), f))) 17 | } 18 | 19 | // ------------------------------------ 20 | // tests 21 | // ------------------------------------ 22 | 23 | import * as assert from 'assert' 24 | 25 | async function test() { 26 | assert.deepStrictEqual( 27 | await Monad.map(Monad.of(1), (n: number) => n * 2)(), 28 | E.right(2) 29 | ) 30 | assert.deepStrictEqual( 31 | await Monad.chain(Monad.of(1), (n: number) => 32 | n > 0 ? Monad.of(n * 2) : T.of(E.left('error')) 33 | )(), 34 | E.right(2) 35 | ) 36 | assert.deepStrictEqual( 37 | await Monad.chain(Monad.of(-1), (n: number) => 38 | n > 0 ? Monad.of(n * 2) : T.of(E.left('error')) 39 | )(), 40 | E.left('error') 41 | ) 42 | } 43 | 44 | test() 45 | -------------------------------------------------------------------------------- /src/solutions/Monad03.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/function' 2 | import * as TE from 'fp-ts/TaskEither' 3 | 4 | interface User {} 5 | 6 | // If Page X doesn't have more users it will return empty array ([]) 7 | function getUsersByPage(page: number): Promise> { 8 | return Promise.resolve(page < 3 ? ['a', 'b'] : []) 9 | } 10 | 11 | const getUsers = (page: number): TE.TaskEither> => 12 | TE.tryCatch( 13 | () => getUsersByPage(page), 14 | () => new Error(`Error while fetching page: ${page}`) 15 | ) 16 | 17 | const step = ( 18 | page: number, 19 | users: ReadonlyArray 20 | ): TE.TaskEither> => 21 | pipe( 22 | getUsers(page), 23 | TE.chain((result) => 24 | result.length === 0 ? TE.of(users) : step(page + 1, users.concat(result)) 25 | ) 26 | ) 27 | 28 | export const getAllUsers: TE.TaskEither> = step( 29 | 0, 30 | [] 31 | ) 32 | 33 | // ------------------------------------ 34 | // tests 35 | // ------------------------------------ 36 | 37 | import * as assert from 'assert' 38 | import * as E from 'fp-ts/Either' 39 | 40 | async function test() { 41 | assert.deepStrictEqual( 42 | await getAllUsers(), 43 | E.right(['a', 'b', 'a', 'b', 'a', 'b']) 44 | ) 45 | } 46 | 47 | test() 48 | -------------------------------------------------------------------------------- /src/solutions/Monoid01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supponiamo di avere un `ReadonlyArray` ma di non disporre di una istanza di monoide per `A`, 3 | * possiamo sempre mappare la lista e farla diventare di un tipo per il quale abbiamo una istanza. 4 | * 5 | * Questa operazione è realizzata dalla seguente funzione `foldMap` che dovete implementare. 6 | */ 7 | import { concatAll, Monoid } from 'fp-ts/Monoid' 8 | import * as N from 'fp-ts/number' 9 | import { flow, pipe } from 'fp-ts/function' 10 | import { map } from 'fp-ts/ReadonlyArray' 11 | 12 | const foldMap = ( 13 | M: Monoid 14 | ): ((f: (a: A) => B) => (as: ReadonlyArray) => B) => (f) => 15 | flow(map(f), concatAll(M)) 16 | 17 | // ------------------------------------ 18 | // tests 19 | // ------------------------------------ 20 | 21 | import * as assert from 'assert' 22 | 23 | interface Bonifico { 24 | readonly causale: string 25 | readonly importo: number 26 | } 27 | 28 | const bonifici: ReadonlyArray = [ 29 | { causale: 'causale1', importo: 1000 }, 30 | { causale: 'causale2', importo: 500 }, 31 | { causale: 'causale3', importo: 350 } 32 | ] 33 | 34 | // calcola la somma dei bonifici 35 | assert.deepStrictEqual( 36 | pipe( 37 | bonifici, 38 | foldMap(N.MonoidSum)((bonifico) => bonifico.importo) 39 | ), 40 | 1850 41 | ) 42 | -------------------------------------------------------------------------------- /src/solutions/Ord01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire una istanza di `Ord` per `ReadonlyArray` 3 | */ 4 | import { fromCompare, Ord } from 'fp-ts/Ord' 5 | import * as N from 'fp-ts/number' 6 | import { pipe } from 'fp-ts/function' 7 | 8 | const getOrd = (O: Ord): Ord> => 9 | fromCompare((first, second) => { 10 | const aLen = first.length 11 | const bLen = second.length 12 | const len = Math.min(aLen, bLen) 13 | for (let i = 0; i < len; i++) { 14 | const ordering = O.compare(first[i], second[i]) 15 | if (ordering !== 0) { 16 | return ordering 17 | } 18 | } 19 | return N.Ord.compare(aLen, bLen) 20 | }) 21 | 22 | // ------------------------------------ 23 | // tests 24 | // ------------------------------------ 25 | 26 | import * as assert from 'assert' 27 | 28 | const O = getOrd(N.Ord) 29 | 30 | assert.deepStrictEqual(O.compare([1], [1]), 0) 31 | assert.deepStrictEqual(O.compare([1], [1, 2]), -1) 32 | assert.deepStrictEqual(O.compare([1, 2], [1]), 1) 33 | assert.deepStrictEqual(O.compare([1, 2], [1, 2]), 0) 34 | assert.deepStrictEqual(O.compare([1, 1], [1, 2]), -1) 35 | assert.deepStrictEqual(O.compare([1, 1], [2]), -1) 36 | -------------------------------------------------------------------------------- /src/solutions/Semigroup01.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementare la funzione `concatAll` 3 | */ 4 | import { Semigroup } from 'fp-ts/Semigroup' 5 | import * as N from 'fp-ts/number' 6 | import * as S from 'fp-ts/string' 7 | 8 | const concatAll = (S: Semigroup) => (startWith: A) => ( 9 | as: ReadonlyArray 10 | ): A => as.reduce(S.concat, startWith) 11 | 12 | // ------------------------------------ 13 | // tests 14 | // ------------------------------------ 15 | 16 | import * as assert from 'assert' 17 | 18 | assert.deepStrictEqual(concatAll(N.SemigroupSum)(0)([1, 2, 3, 4]), 10) 19 | assert.deepStrictEqual(concatAll(N.SemigroupProduct)(1)([1, 2, 3, 4]), 24) 20 | assert.deepStrictEqual(concatAll(S.Semigroup)('a')(['b', 'c']), 'abc') 21 | -------------------------------------------------------------------------------- /src/solutions/Semigroup02.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Definire un semigruppo per i predicati su `Point` 3 | */ 4 | import { pipe, Predicate, getSemigroup } from 'fp-ts/function' 5 | import { Semigroup } from 'fp-ts/Semigroup' 6 | import * as B from 'fp-ts/boolean' 7 | 8 | type Point = { 9 | readonly x: number 10 | readonly y: number 11 | } 12 | 13 | const isPositiveX: Predicate = (p) => p.x >= 0 14 | const isPositiveY: Predicate = (p) => p.y >= 0 15 | 16 | const S: Semigroup> = getSemigroup(B.SemigroupAll)() 17 | 18 | // ------------------------------------ 19 | // tests 20 | // ------------------------------------ 21 | 22 | import * as assert from 'assert' 23 | 24 | // restituisce `true` se il punto appartiene al primo quadrante, ovvero se ambedue le sue `x` e `y` sono positive 25 | const isPositiveXY = S.concat(isPositiveX, isPositiveY) 26 | 27 | assert.deepStrictEqual(isPositiveXY({ x: 1, y: 1 }), true) 28 | assert.deepStrictEqual(isPositiveXY({ x: 1, y: -1 }), false) 29 | assert.deepStrictEqual(isPositiveXY({ x: -1, y: 1 }), false) 30 | assert.deepStrictEqual(isPositiveXY({ x: -1, y: -1 }), false) 31 | -------------------------------------------------------------------------------- /src/two-pillar-of-fp/README.md: -------------------------------------------------------------------------------- 1 | # 함수형 프로그래밍의 두 가지 요소 2 | 3 | 함수형 프로그래밍은 다음 두 가지 요소를 기반으로 한다: 4 | 5 | - 참조 투명성 6 | - 합성 (범용적 디자인 패턴으로서) 7 | 8 | 이후 내용은 위 두가지 요소와 직간접적으로 연관되어 있습니다. 9 | -------------------------------------------------------------------------------- /src/two-pillar-of-fp/composition.md: -------------------------------------------------------------------------------- 1 | ## 합성 2 | 3 | 함수형 프로그래밍의 기본 패턴은 합성입니다: 특정한 작업을 수행하는 작은 단위의 코드를 합성해 크고 복잡한 단위로 구성합니다. 4 | 5 | "가장 작은것에서 가장 큰것으로" 합성하는 패턴의 예로 다음과 같은 것이 있습니다: 6 | 7 | - 두 개 이상의 기본타입 값을 합성 (숫자나 문자열) 8 | - 두 개 이상의 함수를 합성 9 | - 전체 프로그램의 합성 10 | 11 | 마지막 예는 _모듈화 프로그래밍_ 이라 할 수 있습니다: 12 | 13 | > 모듈화 프로그래밍이란 더 작은 프로그램을 붙여 큰 프로그램을 만드는 과정을 의미한다 - Simon Peyton Jones 14 | 15 | 이러한 프로그래밍 스타일은 combinator 를 통해 이루어집니다. 16 | 17 | 여기서 combinator 는 [combinator pattern](https://wiki.haskell.org/Combinator) 에서 말하는 용어입니다. 18 | 19 | > 사물들을 조합한다는 개념을 중심으로 하여 라이브러리를 만드는 방법. 보통 어떤 타입 `T`와 `T`의 기본값들, 그리고 `T`의 값들을 다양한 방법으로 조합해 더 복잡한 값을 만든는 "combinator" 가 있습니다. 20 | > 21 | > (원문) A style of organizing libraries centered around the idea of combining things. Usually there is some type `T`, some "primitive" values of type `T`, and some "combinators" which can combine values of type `T` in various ways to build up more complex values of type `T` 22 | 23 | combinator 의 일반적인 개념은 다소 모호하고 다른 형태로 나타날 수 있지만, 가장 간단한 것은 다음과 같다: 24 | 25 | ```typescript 26 | combinator: Thing -> Thing 27 | ``` 28 | 29 | **예제**. `double` 함수는 두 수를 조합합니다. 30 | 31 | combinator 의 목적은 이미 정의된 *어떤 것*으로 부터 새로운 *어떤 것*을 만드는 것입니다. 32 | 33 | combinator 의 출력인 새로운 *어떤 것*은 다른 프로그램이나 combinator 로 전달할 수 있기 때문에, 우리는 조합적 폭발을 얻을 수 있으며 이는 이 패턴이 매우 강력하다는 것을 의미합니다. 34 | > (원문) Since the output of a combinator, the new _Thing_, can be passed around as input to other programs and combinators, we obtain a combinatorial explosion of opportunities, which makes this pattern extremely powerful. 35 | 36 | **예제** 37 | 38 | ```typescript 39 | import { pipe } from 'fp-ts/function' 40 | 41 | const double = (n: number): number => n * 2 42 | 43 | console.log(pipe(2, double, double, double)) // => 16 44 | ``` 45 | 46 | 따라서 함수형 모듈에서 다음과 같은 일반적인 형태를 볼 수 있습니다: 47 | 48 | - 타입 `T`에 대한 model 49 | - 타입 `T`의 "primitives" 50 | - primitives 를 더 큰 구조로 조합하기 위한 combinators 51 | 52 | 이와 같은 모듈을 만들어봅시다. 53 | 54 | **데모** 55 | 56 | [`01_retry.ts`](../01_retry.ts) 57 | 58 | 위 데모를 통해 알 수 있듯이, 3개의 primitive 와 2 개의 combinator 만으로도 꽤 복잡한 정책을 표현할 수 있었습니다. 59 | 60 | 만약 새로운 primitive 나 combinator 를 기존것들과 조합한다면 표현가능한 경우의 수가 기하급수적으로 증가하는 것을 알 수 있습니다. 61 | 62 | `01_retry.ts`에 있는 두 개의 combinator 에서 특히 중요한 함수는 `concat`인데 강력한 함수형 프로그래밍 추상화인 semigroup 과 관련있기 때문입니다. 63 | -------------------------------------------------------------------------------- /src/two-pillar-of-fp/referential-transparency.md: -------------------------------------------------------------------------------- 1 | ## 참조 투명성 2 | 3 | > **정의**. 표현식이 평가되는 결과로 바꿔도 프로그래밍의 동작이 변하지 않는다면 해당 표현식은 참조에 투명하다고 말합니다. 4 | 5 | **예제** (참조 투명성은 순수함수를 사용하는 것을 의미합니다) 6 | 7 | ```typescript 8 | const double = (n: number): number => n * 2 9 | 10 | const x = double(2) 11 | const y = double(2) 12 | ``` 13 | 14 | `double(2)` 표현식은 그 결과인 4로 변경할 수 있기에 참조 투명성을 가지고 있습니다. 15 | 16 | 따라서 코드를 아래와 같이 바꿀 수 있습니다. 17 | 18 | ```typescript 19 | const x = 4 20 | const y = x 21 | ``` 22 | 23 | 모든 표현식이 항상 참조 투명하지는 않습니다. 다음 예제를 봅시다. 24 | 25 | **예제** (참조 투명성은 에러를 던지지 않는것을 의미합니다) 26 | 27 | ```typescript 28 | const inverse = (n: number): number => { 29 | if (n === 0) throw new Error('cannot divide by zero') 30 | return 1 / n 31 | } 32 | 33 | const x = inverse(0) + 1 34 | ``` 35 | 36 | `inverse(0)` 는 참조 투명하지 않기 때문에 결과로 대체할 수 없습니다. 37 | 38 | **예제** (참조 투명성을 위해 불변 자료구조를 사용해야 합니다) 39 | 40 | ```typescript 41 | const xs = [1, 2, 3] 42 | 43 | const append = (xs: Array): void => { 44 | xs.push(4) 45 | } 46 | 47 | append(xs) 48 | 49 | const ys = xs 50 | ``` 51 | 52 | 마지막 라인에서 `xs` 는 초기값인 `[1, 2, 3]` 으로 대체할 수 없습니다. 왜냐하면 `append` 함수를 호출해 값이 변경되었기 때문입니다. 53 | 54 | 왜 참조 투명성이 중요할까요? 다음과 같은 것을 얻을 수 있기 때문입니다: 55 | 56 | - **지역적인 코드분석** 코드를 이해하기 위해 외부 문맥을 알 필요가 없습니다 57 | - **코드 수정** 시스템의 동작을 변경하지 않고 코드를 수정할 수 있습니다 58 | 59 | **문제**. 다음과 같은 프로그램이 있다고 가정합시다: 60 | 61 | ```typescript 62 | // Typescript 에서 `declare` 를 사용하면 함수의 구현부 없이 선언부만 작성할 수 있습니다 63 | declare const question: (message: string) => Promise 64 | 65 | const x = await question('What is your name?') 66 | const y = await question('What is your name?') 67 | ``` 68 | 69 | 다음과 같이 코드를 수정해도 괜찮을까요? 프로그램 동작이 변할까요? 70 | 71 | ```typescript 72 | const x = await question('What is your name?') 73 | const y = x 74 | ``` 75 | 76 | 보시다시피 참조 투명하지 않은 표현식을 수정하는 것은 매우 어렵습니다. 77 | 모든 표현식이 참조 투명한 함수형 프로그램에선 수정에 필요한 인지 부하를 상당히 줄일 수 있습니다. 78 | > (원문) In functional programming, where every expression is referentially transparent, the cognitive load required to make changes is severely reduced. 79 | -------------------------------------------------------------------------------- /src/what-is-fp/README.md: -------------------------------------------------------------------------------- 1 | # 함수형 프로그래밍이란 2 | 3 | > 함수형 프로그래밍은 순수함수, 수학적인 함수를 사용하는 프로그래밍입니다. 4 | 5 | 인터넷 검색을 통해서 아마 다음과 같은 정의를 볼 수 있습니다: 6 | 7 | > (순수) 함수란 같은 입력에 항상 같은 결과를 내는 부작용없는 절차입니다. 8 | 9 | 여기서 "부작용" 이란 용어의 정의를 설명하지 않았지만 (이후 공식적인 정의를 보게될것입이다) 직관적으로 파일을 열거나 데이터베이스의 쓰기같은 것을 생각해볼 수 있습니다. 10 | 11 | 지금 당장은 부작용이란 함수가 값을 반환하는 작업 이외에 하는 모든것이라고 생각하시면 됩니다. 12 | 13 | 순수함수만 사용하는 프로그램은 어떤 구조를 가질까요? 14 | 15 | 보통 함수형 프로그램은 **pipeline** 형태로 이루어져 있습니다. 16 | 17 | ```typescript 18 | const program = pipe( 19 | input, 20 | f1, // 순수 함수 21 | f2, // 순수 함수 22 | f3, // 순수 함수 23 | ... 24 | ) 25 | ``` 26 | 27 | 여기서 `input`은 첫 번째 함수인 `f1`으로 전달되고 그 결과는 두 번째 함수인 `f2`로 전달됩니다. 28 | 29 | 이어서 `f2`가 반환하는 값은 세 번째 함수인 `f3`로 전달되고 이후 같은 방식으로 진행됩니다. 30 | 31 | **데모** 32 | 33 | [`00_pipe_and_flow.ts`](../00_pipe_and_flow.ts) 34 | 35 | 앞으로 함수형 프로그래밍이 위와 같은 구조를 만들어주는 도구를 제공하는지 보게될 것입니다. 36 | 37 | 함수형 프로그래밍이 무엇인지 이해하는 것 외에 이것의 궁극적인 목적을 이해하는 것 또한 중요합니다. 38 | 39 | 함수형 프로그래밍의 목적은 수학적인 _모델_ 을 사용해 **시스템의 복잡성을 조정**하고 **코드의 속성** 과 리팩토링의 편의성에 중점을 두는 것입니다. 40 | > (원문) Functional programming's goal is to **tame a system's complexity** through the use of formal _models_, and to give careful attention to **code's properties** and refactoring ease. 41 | 42 | > 함수형 프로그래밍은 프로그램 구조에 감춰진 수학을 사람들에게 가르치는 것에 도와줍니다: 43 | > 44 | > - 합성 가능한 코드를 작성하는법 45 | > - 부작용을 어떻게 다루는지 46 | > - 일관적이고 범용적이며 체계적인 API 를 만드는 법 47 | 48 | 코드의 속성에 중점을 둔다는 것이 무엇일까요? 예제를 살펴보겠습니다: 49 | 50 | **예제** 51 | 52 | 왜 `for`반복문보다 `Array`의 `map`이 더 함수형이라고 할까요? 53 | 54 | ```typescript 55 | // 입력 56 | const xs: Array = [1, 2, 3] 57 | 58 | // 수정 59 | const double = (n: number): number => n * 2 60 | 61 | // 결과: `xs` 의 각 요소들이 2배가 된 배열을 얻고싶다 62 | const ys: Array = [] 63 | for (let i = 0; i <= xs.length; i++) { 64 | ys.push(double(xs[i])) 65 | } 66 | ``` 67 | 68 | `for`반복문은 많은 유연성을 제공합니다. 즉 다음 값들을 수정할 수 있습니다. 69 | 70 | - 시작 위치, `let i = 0` 71 | - 반복 조건, `i < xs.length` 72 | - 반복 제어, `i++`. 73 | 74 | 이는 **에러**를 만들어 낼 수 있음을 의미하며 따라서 결과물에 대한 확신이 줄어듭니다. 75 | 76 | **문제**. 위 `for 반복문`은 올바른가요? 77 | 78 | 위 예제를 `map`을 활용해 작성해봅시다. 79 | 80 | ```typescript 81 | // 입력 82 | const xs: Array = [1, 2, 3] 83 | 84 | // 수정 85 | const double = (n: number): number => n * 2 86 | 87 | // 결과: `xs` 의 각 요소들이 2배가 된 배열을 얻고싶다 88 | const ys: Array = xs.map(double) 89 | ``` 90 | 91 | `map`은 `for 반복문`에 비해 유연성이 적지만 다음과 같은 확신을 제공합니다. 92 | 93 | - 입력 배열의 모든 요소에 대해 처리될것이다. 94 | - 결과 배열의 크기는 입력 배열의 크기와 동일할 것이다. 95 | 96 | 함수형 프로그래밍에선 구체적인 구현보다 코드의 속성에 더 집중합니다. 97 | 98 | 즉 `map` 연산의 **제약사항**이 오히려 유용하게 해줍니다. 99 | 100 | `for` 반복문 보다 `map` 을 사용한 PR 을 리뷰할 때 얼마나 편한지 생각해보세요. 101 | -------------------------------------------------------------------------------- /task-vs-promise.md: -------------------------------------------------------------------------------- 1 | # `Task` versus `Promise` 2 | 3 | `Task` è una astrazione simile a `Promise`, la differenza chiave è che `Task` rappresenta una computazione asincrona 4 | mentre `Promise` rappresenta solo un risultato (ottenuto in maniera asincrona). 5 | 6 | Se abbiamo un `Task` 7 | 8 | - possiamo far partire la computazione che rappresenta (per esempio una richiesta network) 9 | - possiamo scegliere di non far partire la computazione 10 | - possiamo farlo partire più di una volta (e potenzialmente ottenere risultati diversi) 11 | - mentre la computazione si sta svolgendo, possiamo notificargli che non siamo più interessati al risultato e la computazione può scegliere di terminarsi da sola 12 | - quando la computazione finisce otteniamo il risultato 13 | 14 | Se abbiamo una `Promise` 15 | 16 | - la computazione si sta già svolgendo (o è addirittura già finita) e non abbiamo controllo su questo 17 | - quando è disponible otteniamo il risultato 18 | - due consumatori della stessa `Promise` ottengono lo stesso risultato 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "lib": ["dom"], 9 | "sourceMap": false, 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "stripInternal": true, 18 | "jsx": "react" 19 | }, 20 | "include": ["./src", "./dev"] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint-config-standard", "tslint-immutable", "tslint-etc"], 3 | "rules": { 4 | "space-before-function-paren": false, 5 | "no-use-before-declare": false, 6 | "variable-name": false, 7 | "quotemark": [true, "single", "jsx-double"], 8 | "ter-indent": false, 9 | "strict-boolean-expressions": true, 10 | "forin": true, 11 | "no-console": false, 12 | "array-type": [true, "generic"], 13 | "readonly-keyword": true, 14 | "readonly-array": false, 15 | "no-string-throw": false, 16 | "strict-type-predicates": false, 17 | "expect-type": true, 18 | "no-floating-promises": false 19 | } 20 | } 21 | --------------------------------------------------------------------------------