├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── file.txt ├── images ├── adt.png ├── associativity.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 ├── 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 ├── 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 ├── functor.html ├── hangman.ts ├── images │ └── bird.png ├── laws.ts ├── mutable-is-unsafe-in-typescript.ts ├── onion-architecture │ ├── 1.ts │ ├── 2.ts │ ├── 3.ts │ ├── 4.ts │ ├── 5.ts │ ├── 6.ts │ └── employee_data.txt ├── program5.ts ├── quiz-answers │ ├── for-loop.md │ ├── javascript-includes.md │ ├── magma-concat-closed.md │ ├── option-semigroup-monoid-second.md │ ├── pattern-matching.md │ ├── semigroup-commutative.md │ ├── semigroup-concatAll-initial-value.md │ ├── semigroup-demo-concat.md │ ├── semigroup-first.md │ └── semigroup-second.md ├── reader.ts ├── 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 ├── task-vs-promise.md ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | dist 4 | dev 5 | coverage 6 | .cache 7 | -------------------------------------------------------------------------------- /.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 | # "Introduction to Functional Programming"에 기여하기 2 | 3 | 기여는 일반적으로 원본 콘텐츠를 더 이해하기 쉽게 만들거나 오타/언어를 수정하는 것을 목표로 해야 합니다. 4 | 5 | Pull Request는 앞서 언급한 목표를 준수하는 경우 환영합니다. 6 | 7 | 이러한 정책은 변경될 수 있습니다. 8 | 9 | 각 섹션별 할당자를 정하여 번역을 진행합니다. 10 | 11 | 섹션은 [Github 이슈](https://github.com/alstn2468/functional-programming/issues)를 확인해주세요. 12 | 13 | 번역은 README.md에 연결되어 있는 모든 파일을 번역을 진행합니다. 14 | 15 | 이슈 하나 또는 이슈의 세부 항목에 대하여 할당자를 지정합니다. 16 | 17 | 기여하시고 싶은 부분이 있다면 코멘트를 남겨주세요. -------------------------------------------------------------------------------- /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 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /file.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /images/adt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/adt.png -------------------------------------------------------------------------------- /images/associativity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/associativity.png -------------------------------------------------------------------------------- /images/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/category.png -------------------------------------------------------------------------------- /images/chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/chain.png -------------------------------------------------------------------------------- /images/composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/composition.png -------------------------------------------------------------------------------- /images/contramap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/contramap.png -------------------------------------------------------------------------------- /images/eilenberg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/eilenberg.jpg -------------------------------------------------------------------------------- /images/flatMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/flatMap.png -------------------------------------------------------------------------------- /images/functor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/functor.png -------------------------------------------------------------------------------- /images/identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/identity.png -------------------------------------------------------------------------------- /images/kleisli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/kleisli.jpg -------------------------------------------------------------------------------- /images/kleisli_arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/kleisli_arrows.png -------------------------------------------------------------------------------- /images/kleisli_category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/kleisli_category.png -------------------------------------------------------------------------------- /images/kleisli_composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/kleisli_composition.png -------------------------------------------------------------------------------- /images/liftA2-first-step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/liftA2-first-step.png -------------------------------------------------------------------------------- /images/liftA2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/liftA2.png -------------------------------------------------------------------------------- /images/maclane.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/maclane.jpg -------------------------------------------------------------------------------- /images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/map.png -------------------------------------------------------------------------------- /images/moggi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/moggi.jpg -------------------------------------------------------------------------------- /images/monoid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/monoid.png -------------------------------------------------------------------------------- /images/morphism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/morphism.png -------------------------------------------------------------------------------- /images/mutable-immutable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/mutable-immutable.jpg -------------------------------------------------------------------------------- /images/objects-morphisms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/objects-morphisms.png -------------------------------------------------------------------------------- /images/of.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/of.png -------------------------------------------------------------------------------- /images/semigroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/semigroup.png -------------------------------------------------------------------------------- /images/semigroupVector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/semigroupVector.png -------------------------------------------------------------------------------- /images/spoiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/spoiler.png -------------------------------------------------------------------------------- /images/wadler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/images/wadler.jpg -------------------------------------------------------------------------------- /mind-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/mind-map.png -------------------------------------------------------------------------------- /monad-transformers.md: -------------------------------------------------------------------------------- 1 | # Monad transformers 2 | 3 | **Senario 1** 4 | 5 | Supponiamo di avere definto le seguenti API: 6 | 7 | ```ts 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 | ```ts 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 | "doctoc": "doctoc README.md", 10 | "shapes": "npx parcel src/shapes.html", 11 | "functor": "npx parcel src/functor.html" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/gcanti/functional-programming.git" 16 | }, 17 | "author": "Giulio Canti ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/gcanti/functional-programming/issues" 21 | }, 22 | "homepage": "https://github.com/gcanti/functional-programming", 23 | "dependencies": { 24 | "@types/react": "^17.0.3", 25 | "fast-check": "^2.12.1", 26 | "fp-ts": "^2.10.4", 27 | "react": "^17.0.2" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^12.6.8", 31 | "doctoc": "^1.4.0", 32 | "parcel-bundler": "^1.12.4", 33 | "prettier": "^2.2.1", 34 | "tslint": "^5.11.0", 35 | "tslint-config-standard": "^8.0.1", 36 | "tslint-etc": "^1.13.9", 37 | "tslint-immutable": "^6.0.1", 38 | "typescript": "^4.2.4" 39 | }, 40 | "tags": [], 41 | "keywords": [] 42 | } 43 | -------------------------------------------------------------------------------- /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 | 이 모듈은 세 부분으로 나뉩니다: 6 | 7 | - 모델 8 | - 원시 요소 9 | - 결합자 10 | 11 | */ 12 | 13 | // ------------------------------------------------------------------------------------- 14 | // 모델 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 | * 지연 시간(밀리초)을 반환할 수 있습니다. 반복 횟수는 33 | * 0에서 시작하여 재시도할 때마다 1씩 증가합니다. 34 | * 함수가 *undefined*를 반환하는 것은 재시도 제한에 도달했음을 의미합니다. 35 | */ 36 | export interface RetryPolicy { 37 | (status: RetryStatus): number | undefined 38 | } 39 | 40 | // ------------------------------------------------------------------------------------- 41 | // 원시 요소 42 | // ------------------------------------------------------------------------------------- 43 | 44 | /** 45 | * 무제한 재시도와 지속적인 지연 46 | */ 47 | export const constantDelay = (delay: number): RetryPolicy => () => delay 48 | 49 | /** 50 | * 최대 'i'회까지만 즉시 다시 시도 51 | */ 52 | export const limitRetries = (i: number): RetryPolicy => (status) => 53 | status.iterNumber >= i ? undefined : 0 54 | 55 | /** 56 | * 반복할 때마다 지연 시간이 기하급수적으로 증가합니다. 57 | * 각 지연 시간은 2배씩 증가합니다. 58 | */ 59 | export const exponentialBackoff = (delay: number): RetryPolicy => (status) => 60 | delay * Math.pow(2, status.iterNumber) 61 | 62 | // ------------------------------------------------------------------------------------- 63 | // 결합자 64 | // ------------------------------------------------------------------------------------- 65 | 66 | /** 67 | * 지정된 정책에 의해 지시될 수 있는 지연에 대한 시간 상한을 설정합니다. 68 | */ 69 | export const capDelay = (maxDelay: number) => ( 70 | policy: RetryPolicy 71 | ): RetryPolicy => (status) => { 72 | const delay = policy(status) 73 | return delay === undefined ? undefined : Math.min(maxDelay, delay) 74 | } 75 | 76 | /** 77 | * 두 정책을 병합합니다. 78 | * **퀴즈**: 두 정책을 병합한다는 것은 무엇을 의미하나요? 79 | */ 80 | export const concat = (second: RetryPolicy) => ( 81 | first: RetryPolicy 82 | ): RetryPolicy => (status) => { 83 | const delay1 = first(status) 84 | const delay2 = second(status) 85 | if (delay1 !== undefined && delay2 !== undefined) { 86 | return Math.max(delay1, delay2) 87 | } 88 | return undefined 89 | } 90 | 91 | // ------------------------------------------------------------------------------------- 92 | // 테스트 93 | // ------------------------------------------------------------------------------------- 94 | 95 | /** 96 | * 상태에 대한 정책을 적용하여 결정 사항을 확인합니다. 97 | */ 98 | export const applyPolicy = (policy: RetryPolicy) => ( 99 | status: RetryStatus 100 | ): RetryStatus => ({ 101 | iterNumber: status.iterNumber + 1, 102 | previousDelay: policy(status) 103 | }) 104 | 105 | /** 106 | * 모든 중간 결과를 유지하는 정책을 적용합니다. 107 | */ 108 | export const dryRun = (policy: RetryPolicy): ReadonlyArray => { 109 | const apply = applyPolicy(policy) 110 | let status: RetryStatus = apply(startStatus) 111 | const out: Array = [status] 112 | while (status.previousDelay !== undefined) { 113 | out.push((status = apply(out[out.length - 1]))) 114 | } 115 | return out 116 | } 117 | 118 | import { pipe } from 'fp-ts/function' 119 | 120 | /* 121 | constantDelay(300) 122 | |> concat(exponentialBackoff(200)) 123 | |> concat(limitRetries(5)) 124 | |> capDelay(2000) 125 | */ 126 | const myPolicy = pipe( 127 | constantDelay(300), 128 | concat(exponentialBackoff(200)), 129 | concat(limitRetries(5)), 130 | capDelay(2000) 131 | ) 132 | 133 | console.log(dryRun(myPolicy)) 134 | /* 135 | [ 136 | { iterNumber: 1, previousDelay: 300 }, <= constantDelay 137 | { iterNumber: 2, previousDelay: 400 }, <= exponentialBackoff 138 | { iterNumber: 3, previousDelay: 800 }, <= exponentialBackoff 139 | { iterNumber: 4, previousDelay: 1600 }, <= exponentialBackoff 140 | { iterNumber: 5, previousDelay: 2000 }, <= capDelay 141 | { iterNumber: 6, previousDelay: undefined } <= limitRetries 142 | ] 143 | */ 144 | -------------------------------------------------------------------------------- /src/02_ord.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | **퀴즈**: 타입 `A` 주어지면 `Ord`에 대한 세미그룹 인스턴스를 정의할 수 있나요? 4 | 그것은 무엇을 표현할 수 있나요? 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 | 우선 `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 | 이제 실제 사례에 적용해 보겠습니다. 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 | // 정렬할 테이블을 표현합니다. 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 | // 일반적인 정렬: 68 | // 처음에는 이름으로, 그다음에는 나이로, 그다음에는 `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 | // 이제 `rememberMe = true`인 사용자가 앞 순서이길 원합니다. 83 | 84 | const byRememberMeNameAge = concatAll(SemigroupOrdUser)( 85 | O.reverse(byRememberMe) 86 | )([byName, byAge]) 87 | pipe(users, sort(byRememberMeNameAge), console.log) 88 | /* 89 | [ { id: 4, name: 'Giulio', age: 44, rememberMe: true }, 90 | { id: 2, name: 'Guido', age: 46, rememberMe: true }, 91 | { id: 3, name: 'Giulio', age: 44, rememberMe: false }, 92 | { id: 1, name: 'Guido', age: 47, rememberMe: false } ] 93 | */ 94 | -------------------------------------------------------------------------------- /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 | // 모델 10 | // ------------------------------------------------------------------------------------- 11 | 12 | export interface Point { 13 | readonly x: number 14 | readonly y: number 15 | } 16 | 17 | /** 18 | * Shape는 주어진 점이 해당 Shape에 속하면 19 | * `true`를 반환하고 그렇지 않으면 `false`를 반환하는 함수입니다. 20 | */ 21 | export type Shape = (point: Point) => boolean 22 | 23 | /* 24 | 25 | FFFFFFFFFFFFFFFFFFFFFF 26 | FFFFFFFFFFFFFFFFFFFFFF 27 | FFFFFFFTTTTTTTTFFFFFFF 28 | FFFFFFFTTTTTTTTFFFFFFF 29 | FFFFFFFTTTTTTTTFFFFFFF 30 | FFFFFFFTTTTTTTTFFFFFFF 31 | FFFFFFFFFFFFFFFFFFFFFF 32 | FFFFFFFFFFFFFFFFFFFFFF 33 | 34 | ▧▧▧▧▧▧▧▧ 35 | ▧▧▧▧▧▧▧▧ 36 | ▧▧▧▧▧▧▧▧ 37 | ▧▧▧▧▧▧▧▧ 38 | 39 | */ 40 | 41 | // ------------------------------------------------------------------------------------- 42 | // 원시 요소 43 | // ------------------------------------------------------------------------------------- 44 | 45 | /** 46 | * 원을 표현하는 도형 만들기 47 | */ 48 | export const disk = (center: Point, radius: number): Shape => (point) => 49 | distance(point, center) <= radius 50 | 51 | // 유클리드 거리 52 | const distance = (p1: Point, p2: Point) => 53 | Math.sqrt( 54 | Math.pow(Math.abs(p1.x - p2.x), 2) + Math.pow(Math.abs(p1.y - p2.y), 2) 55 | ) 56 | 57 | // pipe(disk({ x: 200, y: 200 }, 100), draw) 58 | 59 | // ------------------------------------------------------------------------------------- 60 | // 결합자 61 | // ------------------------------------------------------------------------------------- 62 | 63 | /** 64 | * 주어진 형태의 반대되는 형태을 65 | * 반환하는 첫 번째 결합자를 정의할 수 있습니다. 66 | */ 67 | export const outside = (s: Shape): Shape => (point) => !s(point) 68 | 69 | // pipe(disk({ x: 200, y: 200 }, 100), outside, draw) 70 | 71 | // ------------------------------------------------------------------------------------- 72 | // 인스턴스 73 | // ------------------------------------------------------------------------------------- 74 | 75 | /** 76 | * `concat`이 두 `Shape`의 합집합을 나타내는 모노이드 77 | */ 78 | export const MonoidUnion: Monoid = { 79 | concat: (first, second) => (point) => first(point) || second(point), 80 | empty: () => false 81 | } 82 | 83 | // pipe( 84 | // MonoidUnion.concat( 85 | // disk({ x: 150, y: 200 }, 100), 86 | // disk({ x: 250, y: 200 }, 100) 87 | // ), 88 | // draw 89 | // ) 90 | 91 | /** 92 | * `concat`이 두 `Shape`의 교집합을 나타내는 모노이드 93 | */ 94 | const MonoidIntersection: Monoid = { 95 | concat: (first, second) => (point) => first(point) && second(point), 96 | empty: () => true 97 | } 98 | 99 | // pipe( 100 | // MonoidIntersection.concat( 101 | // disk({ x: 150, y: 200 }, 100), 102 | // disk({ x: 250, y: 200 }, 100) 103 | // ), 104 | // draw 105 | // ) 106 | 107 | /** 108 | * 결합자 `outside`와 `MonoidIntersection`을 사용하여 109 | * 링을 표현하는 `Shape`를 만들 수 있습니다. 110 | */ 111 | export const ring = ( 112 | point: Point, 113 | bigRadius: number, 114 | smallRadius: number 115 | ): Shape => 116 | MonoidIntersection.concat( 117 | disk(point, bigRadius), 118 | outside(disk(point, smallRadius)) 119 | ) 120 | 121 | // pipe(ring({ x: 200, y: 200 }, 100, 50), draw) 122 | 123 | export const mickeymouse: ReadonlyArray = [ 124 | disk({ x: 200, y: 200 }, 100), 125 | disk({ x: 130, y: 100 }, 60), 126 | disk({ x: 280, y: 100 }, 60) 127 | ] 128 | 129 | // pipe(concatAll(MonoidUnion)(mickeymouse), draw) 130 | 131 | // ------------------------------------------------------------------------------------- 132 | // 유틸 133 | // ------------------------------------------------------------------------------------- 134 | 135 | export function draw(shape: Shape): void { 136 | const canvas: HTMLCanvasElement = document.getElementById('canvas') as any 137 | const ctx: CanvasRenderingContext2D = canvas.getContext('2d') as any 138 | const width = canvas.width 139 | const height = canvas.height 140 | const imagedata = ctx.createImageData(width, height) 141 | for (let x = 0; x < width; x++) { 142 | for (let y = 0; y < height; y++) { 143 | const point: Point = { x, y } 144 | if (shape(point)) { 145 | const pixelIndex = (point.y * width + point.x) * 4 146 | imagedata.data[pixelIndex] = 0 147 | imagedata.data[pixelIndex + 1] = 0 148 | imagedata.data[pixelIndex + 2] = 0 149 | imagedata.data[pixelIndex + 3] = 255 150 | } 151 | } 152 | } 153 | ctx.putImageData(imagedata, 0, 0) 154 | } 155 | -------------------------------------------------------------------------------- /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 | // 모델 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 | // 생성자 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 | // 결합자 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 | // 메인 59 | // ------------------------------------------------------------------------------------- 60 | 61 | // `main`은 변환 함수, 즉 `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 | Models the dice roll of a role-playing game. 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 | // instances 49 | // ------------------------------------ 50 | 51 | export const monoidDie: Monoid = { 52 | concat: (first, second) => pipe(first, add(second)), 53 | empty: () => 0 // <= un dado con zero facce 54 | } 55 | 56 | // ------------------------------------ 57 | // tests 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 | // GUESS THE NUMBER 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 | // the number to guess 14 | export const secret: T.Task = T.fromIO(randomInt(1, 100)) 15 | 16 | // combinator: print a message before an action 17 | const withMessage = (message: string, next: T.Task): T.Task => 18 | pipe( 19 | putStrLn(message), 20 | T.chain(() => next) 21 | ) 22 | 23 | // the input is a string so we have to validate it 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('Indovina il numero', 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('Devi inserire un intero da 1 a 100', answer), 40 | (a) => T.of(a) 41 | ) 42 | ) 43 | ) 44 | ) 45 | 46 | const check = ( 47 | secret: number, // the secret number to guess 48 | guess: number, // user attempt 49 | ok: T.Task, // what to do if the user guessed 50 | ko: T.Task // what to do if the user did NOT guess 51 | ): T.Task => { 52 | if (guess > secret) { 53 | return withMessage('Troppo alto', ko) 54 | } else if (guess < secret) { 55 | return withMessage('Troppo basso', ko) 56 | } else { 57 | return ok 58 | } 59 | } 60 | 61 | const end: T.Task = putStrLn('Hai indovinato!') 62 | 63 | // keep the state (secret) as the argument of the function (alla Erlang) 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/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/functor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/hangman.ts: -------------------------------------------------------------------------------- 1 | import { 2 | constFalse, 3 | constTrue, 4 | constUndefined, 5 | flow, 6 | pipe 7 | } from 'fp-ts/function' 8 | import * as IO from 'fp-ts/IO' 9 | import * as S from 'fp-ts/string' 10 | import * as O from 'fp-ts/Option' 11 | import { randomInt } from 'fp-ts/Random' 12 | import * as RA from 'fp-ts/ReadonlyArray' 13 | import * as T from 'fp-ts/Task' 14 | import { getLine, putStrLn } from './Console' 15 | 16 | interface State { 17 | readonly name: string 18 | readonly guesses: ReadonlyArray 19 | readonly word: ReadonlyArray 20 | } 21 | 22 | const newState = (name: string, word: string): State => ({ 23 | name, 24 | word: word.split(''), 25 | guesses: [] 26 | }) 27 | 28 | const addGuess = (state: State, guess: string): State => ({ 29 | ...state, 30 | guesses: pipe(state.guesses, RA.append(guess)) 31 | }) 32 | 33 | const difference = RA.difference(S.Eq) 34 | 35 | const failures = (state: State): number => 36 | pipe(state.guesses, difference(state.word)).length 37 | 38 | const playerLost = (state: State): boolean => failures(state) > 10 39 | 40 | const playerWon = (state: State): boolean => 41 | pipe(state.word, difference(state.guesses)).length === 0 42 | 43 | const question = (question: string): T.Task => 44 | pipe( 45 | putStrLn(question), 46 | T.chain(() => getLine) 47 | ) 48 | 49 | const getName: T.Task = question('What is your name:') 50 | 51 | const parseGuess = (s: string): O.Option => { 52 | const out = s.toLowerCase().trim() 53 | return out.length === 1 ? O.some(out) : O.none 54 | } 55 | 56 | const getGuess: T.Task = pipe( 57 | question('Please enter a letter:'), 58 | T.chain( 59 | flow( 60 | parseGuess, 61 | O.match(() => getGuess, T.of) 62 | ) 63 | ) 64 | ) 65 | 66 | const dictionary: ReadonlyArray = getDictionary() 67 | 68 | const randomElem = (as: ReadonlyArray): IO.IO => 69 | pipe( 70 | randomInt(0, as.length - 1), 71 | IO.map((i) => as[i]) 72 | ) 73 | 74 | const chooseWord: T.Task = pipe(dictionary, randomElem, T.fromIO) 75 | 76 | const render = (state: State) => { 77 | const word = state.word 78 | .map((c) => (state.guesses.includes(c) ? ` ${c} ` : ' ')) 79 | .join('') 80 | const line = state.word.map(() => ' - ').join('') 81 | const guesses = ` Guesses: ${state.guesses.join(', ')}` 82 | return putStrLn(word + '\n' + line + '\n\n' + guesses + '\n') 83 | } 84 | 85 | const doLoop = (state: State, guess: string): T.Task => { 86 | if (playerWon(state)) { 87 | return pipe( 88 | putStrLn(`Congratulations ${state.name} you won the game!`), 89 | T.map(constFalse) 90 | ) 91 | } else if (playerLost(state)) { 92 | return pipe( 93 | putStrLn( 94 | `Sorry ${state.name} you lost the game. The word was ${state.word.join( 95 | '' 96 | )}` 97 | ), 98 | T.map(constFalse) 99 | ) 100 | } else if (state.word.includes(guess)) { 101 | return pipe(putStrLn('You guessed correctly!'), T.map(constTrue)) 102 | } else { 103 | return pipe( 104 | // tslint:disable-next-line: quotemark 105 | putStrLn("That's wrong. but keep trying!"), 106 | T.map(constTrue) 107 | ) 108 | } 109 | } 110 | 111 | const loop = (oldState: State): T.Task => 112 | pipe( 113 | T.Do, 114 | T.bind('guess', () => getGuess), 115 | T.bind('state', ({ guess }) => T.of(addGuess(oldState, guess))), 116 | T.chainFirst(({ state }) => render(state)), 117 | T.bind('doLoop', ({ state, guess }) => doLoop(state, guess)), 118 | T.chain(({ doLoop, state }) => (doLoop ? loop(state) : T.of(state))) 119 | ) 120 | 121 | const hangman: T.Task = pipe( 122 | T.Do, 123 | T.chainFirst(() => putStrLn('Welcome to purely functional hangman')), 124 | T.bind('name', () => getName), 125 | T.chainFirst(({ name }) => putStrLn(`Welcome ${name}. Let's begin!`)), 126 | T.bind('word', () => chooseWord), 127 | T.bind('state', ({ name, word }) => T.of(newState(name, word))), 128 | T.chainFirst(({ state }) => render(state)), 129 | T.chain(({ state }) => loop(state)), 130 | T.map(constUndefined) 131 | ) 132 | 133 | hangman() 134 | 135 | function getDictionary(): ReadonlyArray { 136 | return `ability 137 | able 138 | about 139 | above 140 | accept 141 | according 142 | account 143 | across 144 | act 145 | action 146 | activity 147 | actually 148 | add 149 | address 150 | administration 151 | admit 152 | adult 153 | affect 154 | after 155 | again 156 | against 157 | age 158 | agency 159 | agent 160 | ago 161 | agree 162 | agreement 163 | ahead 164 | air 165 | all 166 | allow 167 | almost 168 | alone 169 | along 170 | already 171 | also 172 | although 173 | always 174 | American 175 | among 176 | amount 177 | analysis 178 | and 179 | animal 180 | another 181 | answer 182 | any 183 | anyone 184 | anything 185 | appear 186 | apply 187 | approach 188 | area 189 | argue 190 | arm 191 | around 192 | arrive 193 | art 194 | article 195 | artist 196 | as 197 | ask 198 | assume 199 | at 200 | attack 201 | attention 202 | attorney 203 | audience 204 | author 205 | authority 206 | available 207 | avoid 208 | away 209 | baby 210 | back 211 | bad 212 | bag 213 | ball 214 | bank 215 | bar 216 | base 217 | be 218 | beat 219 | beautiful 220 | because 221 | become 222 | bed 223 | before 224 | begin 225 | behavior 226 | behind 227 | believe 228 | benefit 229 | best 230 | better 231 | between 232 | beyond 233 | big 234 | bill 235 | billion 236 | bit 237 | black 238 | blood 239 | blue 240 | board 241 | body 242 | book 243 | born 244 | both 245 | box 246 | boy 247 | break 248 | bring 249 | brother 250 | budget 251 | build 252 | building 253 | business 254 | but 255 | buy 256 | by 257 | call 258 | camera 259 | campaign 260 | can 261 | cancer 262 | candidate 263 | capital 264 | car 265 | card 266 | care 267 | career 268 | carry 269 | case 270 | catch 271 | cause 272 | cell 273 | center 274 | central 275 | century 276 | certain 277 | certainly 278 | chair 279 | challenge 280 | chance 281 | change 282 | character 283 | charge 284 | check 285 | child 286 | choice 287 | choose 288 | church 289 | citizen 290 | city 291 | civil 292 | claim 293 | class 294 | clear 295 | clearly 296 | close 297 | coach 298 | cold 299 | collection 300 | college 301 | color 302 | come 303 | commercial 304 | common 305 | community 306 | company 307 | compare 308 | computer 309 | concern 310 | condition 311 | conference 312 | Congress 313 | consider 314 | consumer 315 | contain 316 | continue 317 | control 318 | cost 319 | could 320 | country 321 | couple 322 | course 323 | court 324 | cover 325 | create 326 | crime 327 | cultural 328 | culture 329 | cup 330 | current 331 | customer 332 | cut 333 | dark 334 | data 335 | daughter 336 | day 337 | dead 338 | deal 339 | death 340 | debate 341 | decade 342 | decide 343 | decision 344 | deep 345 | defense 346 | degree 347 | Democrat 348 | democratic 349 | describe 350 | design 351 | despite 352 | detail 353 | determine 354 | develop 355 | development 356 | die 357 | difference 358 | different 359 | difficult 360 | dinner 361 | direction 362 | director 363 | discover 364 | discuss 365 | discussion 366 | disease 367 | do 368 | doctor 369 | dog 370 | door 371 | down 372 | draw 373 | dream 374 | drive 375 | drop 376 | drug 377 | during 378 | each 379 | early 380 | east 381 | easy 382 | eat 383 | economic 384 | economy 385 | edge 386 | education 387 | effect 388 | effort 389 | eight 390 | either 391 | election 392 | else 393 | employee 394 | end 395 | energy 396 | enjoy 397 | enough 398 | enter 399 | entire 400 | environment 401 | environmental 402 | especially 403 | establish 404 | even 405 | evening 406 | event 407 | ever 408 | every 409 | everybody 410 | everyone 411 | everything 412 | evidence 413 | exactly 414 | example 415 | executive 416 | exist 417 | expect 418 | experience 419 | expert 420 | explain 421 | eye 422 | face 423 | fact 424 | factor 425 | fail 426 | fall 427 | family 428 | far 429 | fast 430 | father 431 | fear 432 | federal 433 | feel 434 | feeling 435 | few 436 | field 437 | fight 438 | figure 439 | fill 440 | film 441 | final 442 | finally 443 | financial 444 | find 445 | fine 446 | finger 447 | finish 448 | fire 449 | firm 450 | first 451 | fish 452 | five 453 | floor 454 | fly 455 | focus 456 | follow 457 | food 458 | foot 459 | for 460 | force 461 | foreign 462 | forget 463 | form 464 | former 465 | forward 466 | four 467 | free 468 | friend 469 | from 470 | front 471 | full 472 | fund 473 | future 474 | game 475 | garden 476 | gas 477 | general 478 | generation 479 | get 480 | girl 481 | give 482 | glass 483 | go 484 | goal 485 | good 486 | government 487 | great 488 | green 489 | ground 490 | group 491 | grow 492 | growth 493 | guess 494 | gun 495 | guy 496 | hair 497 | half 498 | hand 499 | hang 500 | happen 501 | happy 502 | hard 503 | have 504 | he 505 | head 506 | health 507 | hear 508 | heart 509 | heat 510 | heavy 511 | help 512 | her 513 | here 514 | herself 515 | high 516 | him 517 | himself 518 | his 519 | history 520 | hit 521 | hold 522 | home 523 | hope 524 | hospital 525 | hot 526 | hotel 527 | hour 528 | house 529 | how 530 | however 531 | huge 532 | human 533 | hundred 534 | husband 535 | I 536 | idea 537 | identify 538 | if 539 | image 540 | imagine 541 | impact 542 | important 543 | improve 544 | in 545 | include 546 | including 547 | increase 548 | indeed 549 | indicate 550 | individual 551 | industry 552 | information 553 | inside 554 | instead 555 | institution 556 | interest 557 | interesting 558 | international 559 | interview 560 | into 561 | investment 562 | involve 563 | issue 564 | it 565 | item 566 | its 567 | itself 568 | job 569 | join 570 | just 571 | keep 572 | key 573 | kid 574 | kill 575 | kind 576 | kitchen 577 | know 578 | knowledge 579 | land 580 | language 581 | large 582 | last 583 | late 584 | later 585 | laugh 586 | law 587 | lawyer 588 | lay 589 | lead 590 | leader 591 | learn 592 | least 593 | leave 594 | left 595 | leg 596 | legal 597 | less 598 | let 599 | letter 600 | level 601 | lie 602 | life 603 | light 604 | like 605 | likely 606 | line 607 | list 608 | listen 609 | little 610 | live 611 | local 612 | long 613 | look 614 | lose 615 | loss 616 | lot 617 | love 618 | low 619 | machine 620 | magazine 621 | main 622 | maintain 623 | major 624 | majority 625 | make 626 | man 627 | manage 628 | management 629 | manager 630 | many 631 | market 632 | marriage 633 | material 634 | matter 635 | may 636 | maybe 637 | me 638 | mean 639 | measure 640 | media 641 | medical 642 | meet 643 | meeting 644 | member 645 | memory 646 | mention 647 | message 648 | method 649 | middle 650 | might 651 | military 652 | million 653 | mind 654 | minute 655 | miss 656 | mission 657 | model 658 | modern 659 | moment 660 | money 661 | month 662 | more 663 | morning 664 | most 665 | mother 666 | mouth 667 | move 668 | movement 669 | movie 670 | Mr 671 | Mrs 672 | much 673 | music 674 | must 675 | my 676 | myself 677 | name 678 | nation 679 | national 680 | natural 681 | nature 682 | near 683 | nearly 684 | necessary 685 | need 686 | network 687 | never 688 | new 689 | news 690 | newspaper 691 | next 692 | nice 693 | night 694 | no 695 | none 696 | nor 697 | north 698 | not 699 | note 700 | nothing 701 | notice 702 | now 703 | n't 704 | number 705 | occur 706 | of 707 | off 708 | offer 709 | office 710 | officer 711 | official 712 | often 713 | oh 714 | oil 715 | ok 716 | old 717 | on 718 | once 719 | one 720 | only 721 | onto 722 | open 723 | operation 724 | opportunity 725 | option 726 | or 727 | order 728 | organization 729 | other 730 | others 731 | our 732 | out 733 | outside 734 | over 735 | own 736 | owner 737 | page 738 | pain 739 | painting 740 | paper 741 | parent 742 | part 743 | participant 744 | particular 745 | particularly 746 | partner 747 | party 748 | pass 749 | past 750 | patient 751 | pattern 752 | pay 753 | peace 754 | people 755 | per 756 | perform 757 | performance 758 | perhaps 759 | period 760 | person 761 | personal 762 | phone 763 | physical 764 | pick 765 | picture 766 | piece 767 | place 768 | plan 769 | plant 770 | play 771 | player 772 | PM 773 | point 774 | police 775 | policy 776 | political 777 | politics 778 | poor 779 | popular 780 | population 781 | position 782 | positive 783 | possible 784 | power 785 | practice 786 | prepare 787 | present 788 | president 789 | pressure 790 | pretty 791 | prevent 792 | price 793 | private 794 | probably 795 | problem 796 | process 797 | produce 798 | product 799 | production 800 | professional 801 | professor 802 | program 803 | project 804 | property 805 | protect 806 | prove 807 | provide 808 | public 809 | pull 810 | purpose 811 | push 812 | put 813 | quality 814 | question 815 | quickly 816 | quite 817 | race 818 | radio 819 | raise 820 | range 821 | rate 822 | rather 823 | reach 824 | read 825 | ready 826 | real 827 | reality 828 | realize 829 | really 830 | reason 831 | receive 832 | recent 833 | recently 834 | recognize 835 | record 836 | red 837 | reduce 838 | reflect 839 | region 840 | relate 841 | relationship 842 | religious 843 | remain 844 | remember 845 | remove 846 | report 847 | represent 848 | Republican 849 | require 850 | research 851 | resource 852 | respond 853 | response 854 | responsibility 855 | rest 856 | result 857 | return 858 | reveal 859 | rich 860 | right 861 | rise 862 | risk 863 | road 864 | rock 865 | role 866 | room 867 | rule 868 | run 869 | safe 870 | same 871 | save 872 | say 873 | scene 874 | school 875 | science 876 | scientist 877 | score 878 | sea 879 | season 880 | seat 881 | second 882 | section 883 | security 884 | see 885 | seek 886 | seem 887 | sell 888 | send 889 | senior 890 | sense 891 | series 892 | serious 893 | serve 894 | service 895 | set 896 | seven 897 | several 898 | sex 899 | sexual 900 | shake 901 | share 902 | she 903 | shoot 904 | short 905 | shot 906 | should 907 | shoulder 908 | show 909 | side 910 | sign 911 | significant 912 | similar 913 | simple 914 | simply 915 | since 916 | sing 917 | single 918 | sister 919 | sit 920 | site 921 | situation 922 | six 923 | size 924 | skill 925 | skin 926 | small 927 | smile 928 | so 929 | social 930 | society 931 | soldier 932 | some 933 | somebody 934 | someone 935 | something 936 | sometimes 937 | son 938 | song 939 | soon 940 | sort 941 | sound 942 | source 943 | south 944 | southern 945 | space 946 | speak 947 | special 948 | specific 949 | speech 950 | spend 951 | sport 952 | spring 953 | staff 954 | stage 955 | stand 956 | standard 957 | star 958 | start 959 | state 960 | statement 961 | station 962 | stay 963 | step 964 | still 965 | stock 966 | stop 967 | store 968 | story 969 | strategy 970 | street 971 | strong 972 | structure 973 | student 974 | study 975 | stuff 976 | style 977 | subject 978 | success 979 | successful 980 | such 981 | suddenly 982 | suffer 983 | suggest 984 | summer 985 | support 986 | sure 987 | surface 988 | system 989 | table 990 | take 991 | talk 992 | task 993 | tax 994 | teach 995 | teacher 996 | team 997 | technology 998 | television 999 | tell 1000 | ten 1001 | tend 1002 | term 1003 | test 1004 | than 1005 | thank 1006 | that 1007 | the 1008 | their 1009 | them 1010 | themselves 1011 | then 1012 | theory 1013 | there 1014 | these 1015 | they 1016 | thing 1017 | think 1018 | third 1019 | this 1020 | those 1021 | though 1022 | thought 1023 | thousand 1024 | threat 1025 | three 1026 | through 1027 | throughout 1028 | throw 1029 | thus 1030 | time 1031 | to 1032 | today 1033 | together 1034 | tonight 1035 | too 1036 | top 1037 | total 1038 | tough 1039 | toward 1040 | town 1041 | trade 1042 | traditional 1043 | training 1044 | travel 1045 | treat 1046 | treatment 1047 | tree 1048 | trial 1049 | trip 1050 | trouble 1051 | true 1052 | truth 1053 | try 1054 | turn 1055 | TV 1056 | two 1057 | type 1058 | under 1059 | understand 1060 | unit 1061 | until 1062 | up 1063 | upon 1064 | us 1065 | use 1066 | usually 1067 | value 1068 | various 1069 | very 1070 | victim 1071 | view 1072 | violence 1073 | visit 1074 | voice 1075 | vote 1076 | wait 1077 | walk 1078 | wall 1079 | want 1080 | war 1081 | watch 1082 | water 1083 | way 1084 | we 1085 | weapon 1086 | wear 1087 | week 1088 | weight 1089 | well 1090 | west 1091 | western 1092 | what 1093 | whatever 1094 | when 1095 | where 1096 | whether 1097 | which 1098 | while 1099 | white 1100 | who 1101 | whole 1102 | whom 1103 | whose 1104 | why 1105 | wide 1106 | wife 1107 | will 1108 | win 1109 | wind 1110 | window 1111 | wish 1112 | with 1113 | within 1114 | without 1115 | woman 1116 | wonder 1117 | word 1118 | work 1119 | worker 1120 | world 1121 | worry 1122 | would 1123 | write 1124 | writer 1125 | wrong 1126 | yard 1127 | yeah 1128 | year 1129 | yes 1130 | yet 1131 | you 1132 | young 1133 | your 1134 | yourself`.split('\n') 1135 | } 1136 | -------------------------------------------------------------------------------- /src/images/bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alstn2468/functional-programming/67b6ebfab0f1ae512e6de95920e867b91df824c5/src/images/bird.png -------------------------------------------------------------------------------- /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/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/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/quiz-answers/for-loop.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | ```ts 4 | // 입력 5 | const xs: Array = [1, 2, 3] 6 | 7 | // 변환 8 | const double = (n: number): number => n * 2 9 | 10 | // 결과: `xs`의 각 요소가 2배가 되는 배열을 원합니다. 11 | const ys: Array = [] 12 | for (let i = 0; i <= xs.length; i++) { 13 | ys.push(double(xs[i])) 14 | } 15 | ``` 16 | 17 | 위의 `for 루프`가 올바르게 작성되었나요? 18 | 19 | ## 정답 20 | 21 | 틀렸습니다. `i <= xs.length` 조건은 `i < xs.length`여야 합니다. 22 | 23 | 코드가 작성된 대로 `ys`의 값은 `[ 2, 4, 6 ]`가 아닌 `[ 2, 4, 6, NaN ]`입니다. 24 | -------------------------------------------------------------------------------- /src/quiz-answers/javascript-includes.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | ```ts 4 | import { Eq } from 'fp-ts/Eq' 5 | 6 | type Point = { 7 | readonly x: number 8 | readonly y: number 9 | } 10 | 11 | const EqPoint: Eq = { 12 | equals: (first, second) => first.x === second.x && first.y === second.y 13 | } 14 | 15 | const points: ReadonlyArray = [ 16 | { x: 0, y: 0 }, 17 | { x: 1, y: 1 }, 18 | { x: 2, y: 2 } 19 | ] 20 | 21 | const search: Point = { x: 1, y: 1 } 22 | 23 | console.log(points.includes(search)) // => false :( 24 | console.log(pipe(points, elem(EqPoint)(search))) // => true :) 25 | ``` 26 | 27 | 왜 `includes` 메소드가 `false`를 반환할까요? 28 | 29 | ## 정답 30 | 31 | `includes` 메소드는 원시 값의 경우 값으로 비교하고 다른 경우에는 참조로 비교합니다. 32 | 33 | 자세한 [설명](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes)은 여기를 확인하세요. includes()는 `sameValueZero` 알고리즘을 사용해 전달된 요소가 있는지 결정합니다. 34 | 35 | `sameValueZero` 알고리즘은 `===`를 사용하는 것과 매우 비슷하며 객체는 값 대신 참조를 비교합니다. (자세한 내용은 [여기](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#same-value-zero_equality)를 확인하세요.) 36 | 37 | 38 | ```ts 39 | console.log({ foo: 'bar' } === { foo: 'bar' }) // => false 40 | 41 | const foo = { foo: 'bar' } 42 | console.log(foo === foo) // => true 43 | ``` 44 | -------------------------------------------------------------------------------- /src/quiz-answers/magma-concat-closed.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | ```ts 4 | import { Magma } from 'fp-ts/Magma' 5 | 6 | const MagmaSub: Magma = { 7 | concat: (first, second) => first - second 8 | } 9 | ``` 10 | 11 | `concat`이 *닫혀있는* 작업이라는 사실은 사소한 내용이 아닙니다. 만약 `A`가 JavaScript의 숫자 타입(양수 및 음수 부동 집합)이 아닌 자연수 집합(양의 정수)인 우리가 구현한 `MagmaSub`와 같이 `concat`을 사용해 `Magma`을 정의할 수 있을까요? `closure` 속성이 유효하지 않은 자연수에 대한 다른 `concat` 작업을 생각할 수 있을까요? 12 | 13 | ## 정답 14 | 15 | 자연수를 사용하면 빼기 연산으로 `Magma`를 정의할 수 없습니다. `b`가 `a`보다 큰 `a - b`는 자연수가 아닌 음수가 됩니다. 16 | 17 | 다음은 `closure` 속성이 유효하지 않은 자연수에 대한 `concat` 연산의 다른 예시입니다. 18 | 19 | - `concat: (first, second) => first / second` 20 | - `concat: (first, second) => (first + second) / 2` 21 | -------------------------------------------------------------------------------- /src/quiz-answers/option-semigroup-monoid-second.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | 다음과 같이 동작하는 `Option`에 대한 모노이드 인스턴스를 정의할 수 있습니다. 4 | 5 | | x | y | concat(x, y) | 6 | | -------- | -------- | ---------------------- | 7 | | none | none | none | 8 | | some(a1) | none | some(a1) | 9 | | none | some(a2) | some(a2) | 10 | | some(a1) | some(a2) | some(S.concat(a1, a2)) | 11 | 12 | ```ts 13 | // 구현은 독자의 연습 문제로 남겨둡니다. 14 | declare const getMonoid: (S: Semigroup) => Monoid> 15 | ``` 16 | 17 | 위 모노이드의 `empty` 요소는 무엇인가요? 18 | 19 | ## 정답 20 | 21 | `none`은 모든 모노이드의 법칙이 참이기 때문에 모노이드의 empty 요소입니다. 새로운 모노이드에 대한 모노이드 법칙을 확인해 봅시다. 22 | 23 | **결합법칙** 24 | ```ts 25 | concat(none, concat(none, concat(none))) === concat(concat(none, none), none) 26 | concat(none, concat(none, concat(some(z)))) === concat(concat(none, none), some(z)) 27 | concat(none, concat(some(y), concat(none))) === concat(concat(none, some(y)), none) 28 | concat(none, concat(some(y), concat(some(z)))) === concat(concat(none, some(y)), some(z)) 29 | concat(some(x), concat(none, concat(none))) === concat(concat(some(x), none), none) 30 | ... 31 | concat(some(x), concat(some(y), concat(some(z)))) === concat(concat(some(x), some(y)), some(z)) 32 | ``` 33 | 34 | **우항등** 35 | ```ts 36 | concat(some(x), none) === some(x) 37 | ``` 38 | 39 | **좌항등** 40 | ```ts 41 | concat(none, some(x)) === some(x) 42 | ``` -------------------------------------------------------------------------------- /src/quiz-answers/pattern-matching.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | ```ts 4 | interface Nil { 5 | readonly _tag: 'Nil' 6 | } 7 | 8 | interface Cons { 9 | readonly _tag: 'Cons' 10 | readonly head: A 11 | readonly tail: List 12 | } 13 | 14 | export type List = Nil | Cons 15 | 16 | export const match = ( 17 | onNil: () => R, 18 | onCons: (head: A, tail: List) => R 19 | ) => (fa: List): R => { 20 | switch (fa._tag) { 21 | case 'Nil': 22 | return onNil() 23 | case 'Cons': 24 | return onCons(fa.head, fa.tail) 25 | } 26 | } 27 | 28 | export const head = match( 29 | () => undefined, 30 | (head, _tail) => head 31 | ) 32 | ``` 33 | 34 | `head` API가 완벽하지 않은 이유는 무엇일까요? 35 | 36 | ## 정답 37 | 38 | 여기서 `head`의 문제는 공역(반환 유형)이 `A`(`List`에서) 또는 `undefined`의 유형일 수 있다는 것입니다. 이 반환 타입으로 작업하는 것은 어려울 수 있으며 버그가 발생할 가능성이 높아집니다. 항상 같은 타입을 반환할 수 있다면 head 함수에서 가능한 두 가지 반환 타입을 처리하기 위해 두 개의 개별 코드를 작성할 필요가 없습니다. 39 | 40 | 사실, 우리는 이 예제와 다르게 항상 `match` 함수를 구현하여 동일한 타입을 반환합니다. 이 튜토리얼의 뒷부분에서 `A`(`List`에서)와 `undefined`를 하나의 타입으로 모델링하는 방법을 배우게 됩니다. -------------------------------------------------------------------------------- /src/quiz-answers/semigroup-commutative.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | `concat`이 [**가환적**](https://en.wikipedia.org/wiki/Commutative_property)인 세미그룹 예시와 그렇지 않은 예시를 찾을 수 있나요? 4 | 5 | ## 정답 6 | 7 | ### 가환적인 예시 8 | 9 | ```ts 10 | import { Semigroup } from 'fp-ts/Semigroup' 11 | 12 | const SemigroupSum: Semigroup = { 13 | concat: (first, second) => first + second 14 | } 15 | ``` 16 | 17 | 덧셈이 가환적이므로 `concat(a, b) = a + b = b + a = concat(b, a)`는 가환적이다. 18 | 19 | ### 가환적이지 않은 예시 20 | 21 | ```ts 22 | import { Semigroup } from 'fp-ts/Semigroup' 23 | 24 | const first = (): Semigroup => ({ 25 | concat: (first, _second) => first 26 | }) 27 | ``` 28 | 29 | `concat(a, b) = a != concat(b, a)` 30 | -------------------------------------------------------------------------------- /src/quiz-answers/semigroup-concatAll-initial-value.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | 정의에 따라 `concat`은 매번 `A`의 두 요소만 결합합니다. 여러 개를 결합하는 것이 가능할까요? 4 | 5 | `concatAll` 함수는 다음을 받습니다. 6 | 7 | - 세미그룹의 인스턴스 8 | - 초기값 9 | - 요소 배열 10 | 11 | ```ts 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 | 20 | 초기 값을 제공해야 하는 이유는 무엇인가요? 21 | 22 | ## 정답 23 | 24 | `concatAll` 메서드는 `A` 타입의 요소를 반환해야 합니다. 제공된 요소 배열이 비어 있으면 반환할 `A` 유형의 요소가 없습니다. 25 | 초기 값의 필요성을 강제하면 배열이 비어 있는 경우 이 초기 값을 반환할 수 있습니다. 26 | 27 | 또한 `NonEmptyArray`를 사용하고 초기 값을 사용하지 않는 `concatAll` 메서드를 정의할 수도 있습니다. 실제로 구현하기가 매우 쉽습니다. 28 | 29 | ```ts 30 | import * as Semigroup from 'fp-ts/Semigroup' 31 | import * as NEA from 'fp-ts/NonEmptyArray' 32 | 33 | const concatAll = (S: Semigroup) => (as: NEA) => 34 | Semigroup.concatAll(S)(NEA.tail(as))(NEA.head(as)) 35 | ``` 36 | -------------------------------------------------------------------------------- /src/quiz-answers/semigroup-demo-concat.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | 데모 [`01_retry.ts`](src/01_retry.ts)에 정의된 `concat` 결합자를 사용해 `RetryPolicy` 타입에 대한 `Semigroup` 인스턴스를 정의할 수 있을까요? 4 | 5 | ## 정답 6 | 7 | 네, 가능합니다. 세미그룹을 다음과 같이 정의해 보겠습니다. 8 | 9 | ```ts 10 | import { Semigroup } from 'fp-ts/Semigroup' 11 | 12 | const SemigroupRetryPolicy: Semigroup = { 13 | concat: (first, second) => concat(first)(second) 14 | } 15 | ``` 16 | 17 | 모든 Semigroup 규칙을 준수합니다. 18 | 19 | - `first`, `second`와 `concat`의 결과는 모두 동일한 타입인 `RetryPolicy`입니다. 20 | - `concat`은 결합법칙을 만족합니다. 21 | 22 | 3개의 `RetryPolicy` `first`, `second`, `third`와 `status`가 주어진다고 가정해봅시다. 23 | 24 | - `RetryPolicy` 중 하나라도 `undefined`를 반환하면 `concat(concat(first, second), third)(status)`와 `concat(first, concat(second, third))(status)` 모두 `undefined`가 됩니다. 25 | - 모든 `RetryPolicy`가 숫자를 반환하면 `concat(concat(first, second), third)(status)`는 `Math.max(Math.max(delay1, delay2), delay3)` 및 `concat(first, concat(second, third))(status)`는 `Math.max(delay1, Math.max(delay2, delay3))`입니다. `Math.max`는 결합법칙을 만족하므로 결과는 `delay1`, `delay2`, `delay3`의 최대값이 됩니다. 26 | -------------------------------------------------------------------------------- /src/quiz-answers/semigroup-first.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | 다음 세미그룹 예시는 법칙을 만족하나요? 4 | 5 | ```ts 6 | import { Semigroup } from 'fp-ts/Semigroup' 7 | 8 | /** 항상 첫 번째 인자를 반환 */ 9 | const first = (): Semigroup => ({ 10 | concat: (first, _second) => first 11 | }) 12 | ``` 13 | 14 | ## 정답 15 | 16 | 만족합니다. 17 | 18 | - `first`, `second`와 `concat`의 결과(`first`)는 모두 동일한 `A` 타입입니다. 19 | - `concat`은 결합법칙을 만족합니다. 20 | - `concat(concat(first, second), third)`는 `concat(first, third)`로 평가된 다음 `first`로 평가됩니다. 21 | - `concat(first, concat(second, third))`는 `concat(first, second)`로 평가된 다음 `first`로 평가됩니다. 22 | -------------------------------------------------------------------------------- /src/quiz-answers/semigroup-second.md: -------------------------------------------------------------------------------- 1 | ## 문제 2 | 3 | 다음 세미그룹 예시는 법칙을 만족하나요? 4 | 5 | ```ts 6 | import { Semigroup } from 'fp-ts/Semigroup' 7 | 8 | /** 항상 두 번째 인자를 반환 */ 9 | const last = (): Semigroup => ({ 10 | concat: (_first, second) => second 11 | }) 12 | ``` 13 | 14 | ## 정답 15 | 16 | 만족합니다. 17 | 18 | - `first`, `second`와 `concat`의 결과(`second`)는 모두 동일한 `A` 타입입니다. 19 | - `concat`은 결합법칙을 만족합니다. 20 | - `concat(concat(first, second), third)`는 `concat(second, third)`로 평가된 다음 `third`로 평가됩니다. 21 | - `concat(first, concat(second, third))`는 `concat(first, third)`로 평가된 다음 `third`로 평가됩니다. 22 | -------------------------------------------------------------------------------- /src/reader.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | # Summary 4 | 5 | Supponiamo di avere una web app con utenti, 6 | ogni utente ha un suo profilo, e di dover implementare 7 | la feature modifica profilo (updateCustomerProfile). 8 | 9 | I requisiti sono i seguenti 10 | 11 | - la richiesta di modifica del profilo è rappresentata 12 | da un modello di dominio (UpdateProfileRequest) 13 | - occorre aggiornare le informazioni nel database (updateProfile) 14 | - se l'email dell'utente è cambiata occorre mandare una email al vecchio 15 | indirizzo per notificare l'avvenuto cambiamento (sendEmailChangedNotification) 16 | 17 | Incominciamo col modellare il problema 18 | 19 | */ 20 | 21 | import * as T from 'fp-ts/Task' 22 | 23 | export interface MonadDB { 24 | readonly getEmail: (userId: number) => T.Task 25 | readonly updateProfile: ( 26 | userId: number, 27 | name: string, 28 | email: string 29 | ) => T.Task 30 | } 31 | 32 | export interface MonadEmail { 33 | readonly sendEmailChangedNotification: ( 34 | newEmail: string, 35 | oldEmail: string 36 | ) => T.Task 37 | } 38 | 39 | /* 40 | 41 | `MonadDB` e `MonadEmail` rappresentano le 42 | "capabilities" di cui ha bisogno l'applicazione. 43 | 44 | Il prefisso `Monad` sta ad inidicare che il codominio 45 | di ogni operazione ha un effetto a cui corrisponde una 46 | istanza di monade (in questo caso specifico `Task`) 47 | 48 | */ 49 | 50 | export interface UpdateProfileRequest { 51 | readonly userId: number 52 | readonly name: string 53 | readonly email: string 54 | } 55 | 56 | declare const monadDB: MonadDB 57 | declare const monadEmail: MonadEmail 58 | 59 | /* 60 | 61 | In questa prima versione le istanze `monadDB` e `monadEmail` 62 | sono cablate nel codice. Si faccia conto che siano importate 63 | staticamente come moduli. Lo scopo ora è cercare di scablarle. 64 | 65 | */ 66 | 67 | import { pipe } from 'fp-ts/function' 68 | 69 | /** 70 | * Restituisce `true` se è stata inviata una notifica 71 | * 72 | * In questa versione le dipendenze sono cablate nel codice 73 | */ 74 | export const updateCustomerProfile1 = ( 75 | request: UpdateProfileRequest 76 | ): T.Task => 77 | pipe( 78 | monadDB.getEmail(request.userId), 79 | T.chain((oldEmail) => 80 | pipe( 81 | monadDB.updateProfile(request.userId, request.name, request.email), 82 | T.chain(() => { 83 | if (oldEmail !== request.email) { 84 | return pipe( 85 | monadEmail.sendEmailChangedNotification(request.email, oldEmail), 86 | T.map(() => true) 87 | ) 88 | } else { 89 | return T.of(false) 90 | } 91 | }) 92 | ) 93 | ) 94 | ) 95 | 96 | /* 97 | 98 | La prima operazione da fare è chiaramente quella di passare le dipendenze come argomenti 99 | e usare il currying. In questo modo abbiamo la possibilità di iniettare le dipendenze 100 | ed ottenere una API dalla firma pulita. 101 | 102 | */ 103 | 104 | export declare const updateCustomerProfile2: ( 105 | monadDB: MonadDB, 106 | monadEmail: MonadEmail 107 | ) => (request: UpdateProfileRequest) => T.Task 108 | 109 | /* 110 | 111 | Il "problema" di questa soluzione è che 112 | ogni consumer di updateCustomerProfile2 deve 113 | preoccuparsi di passare gli argomenti 114 | `MonadDB` e `MonadEmail`. 115 | 116 | Ora, forte dell'adagio che in programmazione funzionale 117 | essere pigri è una qualità invece di un difetto, 118 | faccio una operazione a prima vista bizzarra: 119 | scambio l'ordine dei due gruppi di argomenti nella funzione curried, 120 | postponendo la necessità di avere a disposizione le dipendenze. 121 | 122 | Aggiungo anche un nuovo type alias per avere un solo 123 | parametro contenente tutte le dipendenze. 124 | 125 | */ 126 | 127 | export interface Deps { 128 | readonly db: MonadDB 129 | readonly email: MonadEmail 130 | } 131 | 132 | export declare const updateCustomerProfile3: ( 133 | request: UpdateProfileRequest 134 | ) => (dependencies: Deps) => T.Task 135 | 136 | /* 137 | 138 | In pratica sto ritardando il più possibile il binding 139 | delle dipendenze invece di farlo il prima possibile. 140 | Adesso dovrebbe essere chiaro come si ottiene `Reader`, 141 | guardate l'ultima parte della firma 142 | 143 | (dependencies: Deps) => Task 144 | 145 | Non è altro che Reader> 146 | 147 | */ 148 | 149 | import { Reader } from 'fp-ts/Reader' 150 | 151 | export declare const updateCustomerProfile4: ( 152 | request: UpdateProfileRequest 153 | ) => Reader> 154 | 155 | /* 156 | 157 | Avendo due monadi innestate (Reader e Task) conviene importare una terza 158 | monade che le comprende. 159 | 160 | */ 161 | 162 | import * as RT from 'fp-ts/ReaderTask' 163 | 164 | /* 165 | 166 | Addesso ridefinisco le capabilities 167 | in funzione di `ReaderTask` 168 | 169 | */ 170 | 171 | const getEmail = (userId: number): RT.ReaderTask => (e) => 172 | e.db.getEmail(userId) 173 | 174 | const updateProfile = ( 175 | request: UpdateProfileRequest 176 | ): RT.ReaderTask => (e) => 177 | e.db.updateProfile(request.userId, request.name, request.email) 178 | 179 | const sendEmailChangedNotification = ( 180 | newEmail: string, 181 | oldEmail: string 182 | ): RT.ReaderTask => { 183 | return (e) => e.email.sendEmailChangedNotification(newEmail, oldEmail) 184 | } 185 | 186 | /* 187 | 188 | ...e le uso per ridefinire updateCustomerProfile 189 | 190 | */ 191 | 192 | const updateCustomerProfile5 = ( 193 | request: UpdateProfileRequest 194 | ): RT.ReaderTask => 195 | pipe( 196 | getEmail(request.userId), 197 | RT.chain((oldEmail) => 198 | pipe( 199 | updateProfile(request), 200 | RT.chain(() => { 201 | if (request.email !== oldEmail) { 202 | return pipe( 203 | sendEmailChangedNotification(request.email, oldEmail), 204 | RT.map(() => true) 205 | ) 206 | } else { 207 | return RT.of(false) 208 | } 209 | }) 210 | ) 211 | ) 212 | ) 213 | 214 | /* 215 | 216 | Ora come è possibile testare il nostro programma? 217 | 218 | Semplicemente defininendo delle istanze di test 219 | per per `MonadDB` e `MonadEmail` 220 | 221 | */ 222 | 223 | /** scrive dallo standard output */ 224 | export const putStrLn = (message: string): T.Task => () => 225 | new Promise((res) => { 226 | res(console.log(message)) 227 | }) 228 | 229 | let _email = 'a@gmail.com' 230 | 231 | const withMessage = (message: string, fa: T.Task): T.Task => { 232 | return pipe( 233 | putStrLn(message), 234 | T.chain(() => fa) 235 | ) 236 | } 237 | 238 | const setEmail = (email: string): T.Task => () => { 239 | _email = email 240 | return Promise.resolve(undefined) 241 | } 242 | 243 | const db: MonadDB = { 244 | getEmail: (userId: number) => 245 | withMessage(`getting email for ${userId}: ${_email}`, T.of(_email)), 246 | updateProfile: (_userId: number, _name: string, email: string) => 247 | withMessage( 248 | `updating profile` + 249 | (_email !== email 250 | ? ` and changing email from ${_email} to ${email}` 251 | : ''), 252 | setEmail(email) 253 | ) 254 | } 255 | 256 | const email: MonadEmail = { 257 | sendEmailChangedNotification: (newEmail: string, _oldEmail: string) => 258 | putStrLn(`sending change notification to ${newEmail}`) 259 | } 260 | 261 | const testDeps: Deps = { 262 | db, 263 | email 264 | } 265 | 266 | // program: ReaderTask 267 | const program = updateCustomerProfile5({ 268 | userId: 1, 269 | name: 'foo', 270 | email: 'b@gmail.com' 271 | }) 272 | 273 | program(testDeps)().then(console.log) 274 | /* 275 | getting email for 1: a@gmail.com 276 | updating profile and changing email from a@gmail.com to b@gmail.com 277 | sending change notification to b@gmail.com 278 | true 279 | */ 280 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------