├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── 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 ├── program6.ts ├── 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Giulio Canti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /file.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /images/adt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/adt.png -------------------------------------------------------------------------------- /images/associativity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/associativity.png -------------------------------------------------------------------------------- /images/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/category.png -------------------------------------------------------------------------------- /images/chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/chain.png -------------------------------------------------------------------------------- /images/composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/composition.png -------------------------------------------------------------------------------- /images/contramap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/contramap.png -------------------------------------------------------------------------------- /images/eilenberg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/eilenberg.jpg -------------------------------------------------------------------------------- /images/flatMap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/flatMap.png -------------------------------------------------------------------------------- /images/functor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/functor.png -------------------------------------------------------------------------------- /images/identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/identity.png -------------------------------------------------------------------------------- /images/kleisli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/kleisli.jpg -------------------------------------------------------------------------------- /images/kleisli_arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/kleisli_arrows.png -------------------------------------------------------------------------------- /images/kleisli_category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/kleisli_category.png -------------------------------------------------------------------------------- /images/kleisli_composition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/kleisli_composition.png -------------------------------------------------------------------------------- /images/liftA2-first-step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/liftA2-first-step.png -------------------------------------------------------------------------------- /images/liftA2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/liftA2.png -------------------------------------------------------------------------------- /images/maclane.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/maclane.jpg -------------------------------------------------------------------------------- /images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/map.png -------------------------------------------------------------------------------- /images/moggi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/moggi.jpg -------------------------------------------------------------------------------- /images/monoid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/monoid.png -------------------------------------------------------------------------------- /images/morphism.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/morphism.png -------------------------------------------------------------------------------- /images/mutable-immutable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/mutable-immutable.jpg -------------------------------------------------------------------------------- /images/objects-morphisms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/objects-morphisms.png -------------------------------------------------------------------------------- /images/of.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/of.png -------------------------------------------------------------------------------- /images/semigroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/semigroup.png -------------------------------------------------------------------------------- /images/semigroupVector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/semigroupVector.png -------------------------------------------------------------------------------- /images/spoiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/spoiler.png -------------------------------------------------------------------------------- /images/wadler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/images/wadler.jpg -------------------------------------------------------------------------------- /mind-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/mind-map.png -------------------------------------------------------------------------------- /monad-transformers.md: -------------------------------------------------------------------------------- 1 | # Monad transformers 2 | 3 | **Scenario 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.11.8", 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 operator: 11 | 12 | def program1 (n) do 13 | n 14 | |> increment 15 | |> double 16 | |> decrement 17 | end 18 | 19 | method chaining: 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 | Abstraction for a mechanism to perform actions repetitively until successful. 4 | 5 | Questo modulo è diviso in 3 sezioni 6 | 7 | - modello 8 | - primitive 9 | - combinatori 10 | 11 | */ 12 | 13 | // ------------------------------------------------------------------------------------- 14 | // model 15 | // ------------------------------------------------------------------------------------- 16 | 17 | export interface RetryStatus { 18 | /** Iteration number, where `0` is the first try */ 19 | readonly iterNumber: number 20 | 21 | /** Latest attempt's delay. Will always be `undefined` on first run. */ 22 | readonly previousDelay: number | undefined 23 | } 24 | 25 | export const startStatus: RetryStatus = { 26 | iterNumber: 0, 27 | previousDelay: undefined 28 | } 29 | 30 | /** 31 | * A `RetryPolicy` is a function that takes an `RetryStatus` and 32 | * possibly returns a delay in milliseconds. Iteration numbers start 33 | * at zero and increase by one on each retry. A *undefined* return value from 34 | * the function implies we have reached the retry limit. 35 | */ 36 | export interface RetryPolicy { 37 | (status: RetryStatus): number | undefined 38 | } 39 | 40 | // ------------------------------------------------------------------------------------- 41 | // primitives 42 | // ------------------------------------------------------------------------------------- 43 | 44 | /** 45 | * Constant delay with unlimited retries. 46 | */ 47 | export const constantDelay = (delay: number): RetryPolicy => () => delay 48 | 49 | /** 50 | * Retry immediately, but only up to `i` times. 51 | */ 52 | export const limitRetries = (i: number): RetryPolicy => (status) => 53 | status.iterNumber >= i ? undefined : 0 54 | 55 | /** 56 | * Grow delay exponentially each iteration. 57 | * Each delay will increase by a factor of two. 58 | */ 59 | export const exponentialBackoff = (delay: number): RetryPolicy => (status) => 60 | delay * Math.pow(2, status.iterNumber) 61 | 62 | // ------------------------------------------------------------------------------------- 63 | // combinators 64 | // ------------------------------------------------------------------------------------- 65 | 66 | /** 67 | * Set a time-upperbound for any delays that may be directed by the 68 | * given policy. 69 | */ 70 | export const capDelay = (maxDelay: number) => ( 71 | policy: RetryPolicy 72 | ): RetryPolicy => (status) => { 73 | const delay = policy(status) 74 | return delay === undefined ? undefined : Math.min(maxDelay, delay) 75 | } 76 | 77 | /** 78 | * Merges two policies. **Quiz**: cosa vuol dire fare merge di due policy? 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 | // tests 93 | // ------------------------------------------------------------------------------------- 94 | 95 | /** 96 | * Apply policy on status to see what the decision would be. 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 | * Apply a policy keeping all intermediate results. 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 | **Quiz**. Dato un tipo `A` è possibile definire una istanza di semigruppo 4 | per `Ord`. Cosa potrebbe rappresentare? 5 | 6 | */ 7 | 8 | import { pipe } from 'fp-ts/function' 9 | import * as O from 'fp-ts/Ord' 10 | import { sort } from 'fp-ts/ReadonlyArray' 11 | import { concatAll, Semigroup } from 'fp-ts/Semigroup' 12 | import * as S from 'fp-ts/string' 13 | import * as N from 'fp-ts/number' 14 | import * as B from 'fp-ts/boolean' 15 | 16 | /* 17 | 18 | prima di tutto definiamo una istanza di semigrouppo per `Ord` 19 | 20 | */ 21 | 22 | const getSemigroup = (): Semigroup> => ({ 23 | concat: (first, second) => 24 | O.fromCompare((a1, a2) => { 25 | const ordering = first.compare(a1, a2) 26 | return ordering !== 0 ? ordering : second.compare(a1, a2) 27 | }) 28 | }) 29 | 30 | /* 31 | 32 | adesso vediamola applicata ad un esempio pratico 33 | 34 | */ 35 | 36 | interface User { 37 | readonly id: number 38 | readonly name: string 39 | readonly age: number 40 | readonly rememberMe: boolean 41 | } 42 | 43 | const byName = pipe( 44 | S.Ord, 45 | O.contramap((_: User) => _.name) 46 | ) 47 | 48 | const byAge = pipe( 49 | N.Ord, 50 | O.contramap((_: User) => _.age) 51 | ) 52 | 53 | const byRememberMe = pipe( 54 | B.Ord, 55 | O.contramap((_: User) => _.rememberMe) 56 | ) 57 | 58 | const SemigroupOrdUser = getSemigroup() 59 | 60 | // rappresenta una tabella da ordinare 61 | const users: ReadonlyArray = [ 62 | { id: 1, name: 'Guido', age: 47, rememberMe: false }, 63 | { id: 2, name: 'Guido', age: 46, rememberMe: true }, 64 | { id: 3, name: 'Giulio', age: 44, rememberMe: false }, 65 | { id: 4, name: 'Giulio', age: 44, rememberMe: true } 66 | ] 67 | 68 | // un ordinamento classico: 69 | // prima per nome, poi per età, poi per `rememberMe` 70 | 71 | const byNameAgeRememberMe = concatAll(SemigroupOrdUser)(byName)([ 72 | byAge, 73 | byRememberMe 74 | ]) 75 | pipe(users, sort(byNameAgeRememberMe), console.log) 76 | /* 77 | [ { id: 3, name: 'Giulio', age: 44, rememberMe: false }, 78 | { id: 4, name: 'Giulio', age: 44, rememberMe: true }, 79 | { id: 2, name: 'Guido', age: 46, rememberMe: true }, 80 | { id: 1, name: 'Guido', age: 47, rememberMe: false } ] 81 | */ 82 | 83 | // adesso invece voglio tutti gli utenti con 84 | // `rememberMe = true` per primi 85 | 86 | const byRememberMeNameAge = concatAll(SemigroupOrdUser)( 87 | O.reverse(byRememberMe) 88 | )([byName, byAge]) 89 | pipe(users, sort(byRememberMeNameAge), console.log) 90 | /* 91 | [ { id: 4, name: 'Giulio', age: 44, rememberMe: true }, 92 | { id: 2, name: 'Guido', age: 46, rememberMe: true }, 93 | { id: 3, name: 'Giulio', age: 44, rememberMe: false }, 94 | { id: 1, name: 'Guido', age: 47, rememberMe: false } ] 95 | */ 96 | -------------------------------------------------------------------------------- /src/03_shapes.ts: -------------------------------------------------------------------------------- 1 | // run `npm run shapes` to execute 2 | /* 3 | PROBLEMA: implementare un sistema per disegnare forme geometriche sul canvas. 4 | */ 5 | import { pipe } from 'fp-ts/function' 6 | import { Monoid, concatAll } from 'fp-ts/Monoid' 7 | 8 | // ------------------------------------------------------------------------------------- 9 | // model 10 | // ------------------------------------------------------------------------------------- 11 | 12 | export interface Point { 13 | readonly x: number 14 | readonly y: number 15 | } 16 | 17 | /** 18 | * Una forma è una funzione che dato un punto 19 | * restituisce `true` se il punto appartiene alla forma e `false` altrimenti 20 | * 21 | * > Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function. 22 | * > - John Carmack 23 | */ 24 | export type Shape = (point: Point) => boolean 25 | 26 | /* 27 | 28 | FFFFFFFFFFFFFFFFFFFFFF 29 | FFFFFFFFFFFFFFFFFFFFFF 30 | FFFFFFFTTTTTTTTFFFFFFF 31 | FFFFFFFTTTTTTTTFFFFFFF 32 | FFFFFFFTTTTTTTTFFFFFFF 33 | FFFFFFFTTTTTTTTFFFFFFF 34 | FFFFFFFFFFFFFFFFFFFFFF 35 | FFFFFFFFFFFFFFFFFFFFFF 36 | 37 | ▧▧▧▧▧▧▧▧ 38 | ▧▧▧▧▧▧▧▧ 39 | ▧▧▧▧▧▧▧▧ 40 | ▧▧▧▧▧▧▧▧ 41 | 42 | */ 43 | 44 | // ------------------------------------------------------------------------------------- 45 | // primitives 46 | // ------------------------------------------------------------------------------------- 47 | 48 | /** 49 | * Crea una forma che rappresenta un cerchio 50 | */ 51 | export const disk = (center: Point, radius: number): Shape => (point) => 52 | distance(point, center) <= radius 53 | 54 | // distanza euclidea 55 | const distance = (p1: Point, p2: Point) => 56 | Math.sqrt( 57 | Math.pow(Math.abs(p1.x - p2.x), 2) + Math.pow(Math.abs(p1.y - p2.y), 2) 58 | ) 59 | 60 | // pipe(disk({ x: 200, y: 200 }, 100), draw) 61 | 62 | // ------------------------------------------------------------------------------------- 63 | // combinators 64 | // ------------------------------------------------------------------------------------- 65 | 66 | /** 67 | * Possiamo definire un primo combinatore che data una forma 68 | * restituisce la sua forma complementare (il negativo) 69 | */ 70 | export const outside = (s: Shape): Shape => (point) => !s(point) 71 | 72 | // pipe(disk({ x: 200, y: 200 }, 100), outside, draw) 73 | 74 | // ------------------------------------------------------------------------------------- 75 | // instances 76 | // ------------------------------------------------------------------------------------- 77 | 78 | /** 79 | * Un monoide in cui `concat` rappresenta l'unione di due forme 80 | */ 81 | export const MonoidUnion: Monoid = { 82 | concat: (first, second) => (point) => first(point) || second(point), 83 | empty: () => false 84 | } 85 | 86 | // pipe( 87 | // MonoidUnion.concat( 88 | // disk({ x: 150, y: 200 }, 100), 89 | // disk({ x: 250, y: 200 }, 100) 90 | // ), 91 | // draw 92 | // ) 93 | 94 | /** 95 | * Un monoide in cui `concat` rappresenta l'intersezione di due forme 96 | */ 97 | const MonoidIntersection: Monoid = { 98 | concat: (first, second) => (point) => first(point) && second(point), 99 | empty: () => true 100 | } 101 | 102 | // pipe( 103 | // MonoidIntersection.concat( 104 | // disk({ x: 150, y: 200 }, 100), 105 | // disk({ x: 250, y: 200 }, 100) 106 | // ), 107 | // draw 108 | // ) 109 | 110 | /** 111 | * Usando il combinatore `outside` e `MonoidIntersection` possiamo 112 | * creare una forma che rappresenta un anello 113 | */ 114 | export const ring = ( 115 | point: Point, 116 | bigRadius: number, 117 | smallRadius: number 118 | ): Shape => 119 | MonoidIntersection.concat( 120 | disk(point, bigRadius), 121 | outside(disk(point, smallRadius)) 122 | ) 123 | 124 | // pipe(ring({ x: 200, y: 200 }, 100, 50), draw) 125 | 126 | export const mickeymouse: ReadonlyArray = [ 127 | disk({ x: 200, y: 200 }, 100), 128 | disk({ x: 130, y: 100 }, 60), 129 | disk({ x: 280, y: 100 }, 60) 130 | ] 131 | 132 | // pipe(concatAll(MonoidUnion)(mickeymouse), draw) 133 | 134 | // ------------------------------------------------------------------------------------- 135 | // utils 136 | // ------------------------------------------------------------------------------------- 137 | 138 | export function draw(shape: Shape): void { 139 | const canvas: HTMLCanvasElement = document.getElementById('canvas') as any 140 | const ctx: CanvasRenderingContext2D = canvas.getContext('2d') as any 141 | const width = canvas.width 142 | const height = canvas.height 143 | const imagedata = ctx.createImageData(width, height) 144 | for (let x = 0; x < width; x++) { 145 | for (let y = 0; y < height; y++) { 146 | const point: Point = { x, y } 147 | if (shape(point)) { 148 | const pixelIndex = (point.y * width + point.x) * 4 149 | imagedata.data[pixelIndex] = 0 150 | imagedata.data[pixelIndex + 1] = 0 151 | imagedata.data[pixelIndex + 2] = 0 152 | imagedata.data[pixelIndex + 3] = 255 153 | } 154 | } 155 | } 156 | ctx.putImageData(imagedata, 0, 0) 157 | } 158 | -------------------------------------------------------------------------------- /src/04_functor.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://adrian-salajan.github.io/blog/2021/01/25/images-functor 2 | // run `npm run functor` to execute 3 | 4 | import { Endomorphism } from 'fp-ts/function' 5 | import * as R from 'fp-ts/Reader' 6 | 7 | // ------------------------------------------------------------------------------------- 8 | // model 9 | // ------------------------------------------------------------------------------------- 10 | 11 | type Color = { 12 | readonly red: number 13 | readonly green: number 14 | readonly blue: number 15 | } 16 | 17 | type Point = { 18 | readonly x: number 19 | readonly y: number 20 | } 21 | 22 | type Image = R.Reader 23 | 24 | // ------------------------------------------------------------------------------------- 25 | // constructors 26 | // ------------------------------------------------------------------------------------- 27 | 28 | const color = (red: number, green: number, blue: number): Color => ({ 29 | red, 30 | green, 31 | blue 32 | }) 33 | 34 | const BLACK: Color = color(0, 0, 0) 35 | 36 | const WHITE: Color = color(255, 255, 255) 37 | 38 | // ------------------------------------------------------------------------------------- 39 | // combinators 40 | // ------------------------------------------------------------------------------------- 41 | 42 | const brightness = (color: Color): number => 43 | (color.red + color.green + color.blue) / 3 44 | 45 | export const grayscale = (c: Color): Color => { 46 | const n = brightness(c) 47 | return color(n, n, n) 48 | } 49 | 50 | export const invert = (c: Color): Color => 51 | color(255 - c.red, 255 - c.green, 255 - c.red) 52 | 53 | // if brightness over some value V then put White else put Black 54 | export const threshold = (c: Color): Color => 55 | brightness(c) < 100 ? BLACK : WHITE 56 | 57 | // ------------------------------------------------------------------------------------- 58 | // main 59 | // ------------------------------------------------------------------------------------- 60 | 61 | // `main` deve essere chiamata passando una funzione di trasformazione, cioè un `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 | Modellare il lancio di dadi di un gioco di ruolo. 4 | 5 | */ 6 | import { pipe } from 'fp-ts/function' 7 | import * as IO from 'fp-ts/IO' 8 | import * as R from 'fp-ts/Random' 9 | 10 | // ------------------------------------ 11 | // model 12 | // ------------------------------------ 13 | 14 | export interface Die extends IO.IO {} 15 | 16 | // ------------------------------------ 17 | // constructors 18 | // ------------------------------------ 19 | 20 | export const die = (faces: number): Die => R.randomInt(1, faces) 21 | 22 | // ------------------------------------ 23 | // combinators 24 | // ------------------------------------ 25 | 26 | export const modifier = (n: number) => (die: Die): Die => 27 | pipe( 28 | die, 29 | IO.map((m) => m + n) 30 | ) 31 | 32 | const liftA2 = (f: (a: A) => (b: B) => C) => (fa: IO.IO) => ( 33 | fb: IO.IO 34 | ): IO.IO => pipe(fa, IO.map(f), IO.ap(fb)) 35 | 36 | export const add: ( 37 | second: Die 38 | ) => (first: Die) => Die = liftA2((a: number) => (b: number) => a + b) 39 | 40 | export const multiply = (n: number) => (die: Die): Die => 41 | pipe( 42 | die, 43 | IO.map((m) => m * n) 44 | ) 45 | 46 | // ------------------------------------ 47 | // tests 48 | // ------------------------------------ 49 | 50 | const d6 = die(6) 51 | const d8 = die(8) 52 | 53 | // 2d6 + 1d8 + 2 54 | const _2d6_1d8_2 = pipe(d6, multiply(2), add(d8), modifier(2)) 55 | 56 | console.log(_2d6_1d8_2()) 57 | -------------------------------------------------------------------------------- /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 | // il numero da indovinare 14 | export const secret: T.Task = T.fromIO(randomInt(1, 100)) 15 | 16 | // combinatore: stampa un messaggio prima di una azione 17 | const withMessage = (message: string) => (next: T.Task): T.Task => 18 | pipe( 19 | putStrLn(message), 20 | T.chain(() => next) 21 | ) 22 | 23 | // l'input è una stringa perciò dobbiamo validarlo 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 = pipe( 31 | getLine, 32 | withMessage('Indovina il numero') 33 | ) 34 | 35 | const answer: T.Task = pipe( 36 | question, 37 | T.chain((s) => 38 | pipe( 39 | s, 40 | parseGuess, 41 | O.match( 42 | () => pipe(answer, withMessage('Devi inserire un intero da 1 a 100')), 43 | (n) => T.of(n) 44 | ) 45 | ) 46 | ) 47 | ) 48 | 49 | const check = ( 50 | secret: number, // il numero segreto da indovinare 51 | guess: number, // tentativo dell'utente 52 | ok: T.Task, // cosa fare se l'utente ha indovinato 53 | ko: T.Task // cosa fare se l'utente NON ha indovinato 54 | ): T.Task => { 55 | if (guess > secret) { 56 | return pipe(ko, withMessage('Troppo alto')) 57 | } else if (guess < secret) { 58 | return pipe(ko, withMessage('Troppo basso')) 59 | } else { 60 | return ok 61 | } 62 | } 63 | 64 | const end: T.Task = putStrLn('Hai indovinato!') 65 | 66 | // mantengo lo stato (secret) come argomento della funzione (alla Erlang) 67 | const loop = (secret: number): T.Task => 68 | pipe( 69 | answer, 70 | T.chain((guess) => check(secret, guess, end, loop(secret))) 71 | ) 72 | 73 | const program: T.Task = pipe(secret, T.chain(loop)) 74 | 75 | program() 76 | -------------------------------------------------------------------------------- /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 | /** legge dallo 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 | /** scrive dallo 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/gcanti/functional-programming/8ad21c39072f14a3c65a7a3c057546c7c89c8b29/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/program6.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/function' 2 | import * as TE from 'fp-ts/TaskEither' 3 | import * as RTE from 'fp-ts/ReaderTaskEither' 4 | 5 | // ----------------------------------------- 6 | // effetto del nostro programma 7 | // ----------------------------------------- 8 | 9 | interface Effect extends RTE.ReaderTaskEither {} 10 | 11 | // ----------------------------------------- 12 | // helpers 13 | // ----------------------------------------- 14 | 15 | const chainW: ( 16 | f: (a: A) => Effect 17 | ) => (ma: Effect) => Effect = RTE.chainW 18 | 19 | const asksEffectW = ( 20 | f: (r1: R1) => Effect 21 | ): Effect => (r) => f(r)(r) 22 | 23 | // ----------------------------------------- 24 | // APIs 25 | // ----------------------------------------- 26 | 27 | interface ReadFile { 28 | readonly readFile: (filename: string) => Effect 29 | } 30 | 31 | const readFile = (filename: string) => 32 | asksEffectW((r: ReadFile) => r.readFile(filename)) 33 | 34 | interface WriteFile { 35 | readonly writeFile: (filename: string, data: string) => Effect 36 | } 37 | 38 | const writeFile = (filename: string, data: string) => 39 | asksEffectW((r: WriteFile) => r.writeFile(filename, data)) 40 | 41 | interface Logger { 42 | readonly log: (a: A) => Effect 43 | } 44 | 45 | const log = (a: A) => asksEffectW((r: Logger) => r.log(a)) 46 | 47 | // ----------------------------------------- 48 | // programma 49 | // ----------------------------------------- 50 | 51 | const modifyFile = (filename: string, f: (s: string) => string) => 52 | pipe( 53 | readFile(filename), 54 | chainW((s) => writeFile(filename, f(s))) 55 | ) 56 | 57 | const program6 = pipe( 58 | readFile('-.txt'), 59 | chainW(log), 60 | chainW(() => modifyFile('file.txt', (s) => s + '\n// eof')), 61 | chainW(() => readFile('file.txt')), 62 | chainW(log) 63 | ) 64 | 65 | // ----------------------------------------- 66 | // istanze per ReadFile, Console, WriteFile 67 | // ----------------------------------------- 68 | 69 | import * as fs from 'fs' 70 | import * as E from 'fp-ts/Either' 71 | import * as C from 'fp-ts/Console' 72 | 73 | const ReadFile: ReadFile = { 74 | readFile: (filename) => () => () => 75 | new Promise((resolve) => 76 | fs.readFile(filename, { encoding: 'utf-8' }, (err, s) => { 77 | if (err !== null) { 78 | resolve(E.left(err)) 79 | } else { 80 | resolve(E.right(s)) 81 | } 82 | }) 83 | ) 84 | } 85 | 86 | const WriteFile: WriteFile = { 87 | writeFile: (filename: string, data: string) => () => () => 88 | new Promise((resolve) => 89 | fs.writeFile(filename, data, (err) => { 90 | if (err !== null) { 91 | resolve(E.left(err)) 92 | } else { 93 | resolve(E.right(undefined)) 94 | } 95 | }) 96 | ) 97 | } 98 | 99 | const Logger: Logger = { 100 | log: (a) => () => TE.fromIO(C.log(a)) 101 | } 102 | 103 | // dependency injection 104 | program6({ ...ReadFile, ...WriteFile, ...Logger })().then(console.log) 105 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------