├── .nvmrc ├── .pnpmrc ├── src ├── exo6 - ReaderTaskEither │ ├── domain │ │ ├── index.ts │ │ └── User │ │ │ ├── index.ts │ │ │ ├── Repository │ │ │ ├── index.ts │ │ │ ├── Repository.ts │ │ │ ├── InMemoryUserRepository.ts │ │ │ └── readerMethods.ts │ │ │ ├── User.ts │ │ │ └── type.ts │ ├── application │ │ ├── index.ts │ │ └── services │ │ │ ├── index.ts │ │ │ ├── NodeTimeService.ts │ │ │ └── TimeService.ts │ ├── exo6.test.ts │ ├── exo6.exercise.ts │ └── exo6.solution.ts ├── readerTaskEither.ts ├── testUtils.ts ├── utils.ts ├── exo0 - Composing with pipe and flow │ ├── exo0.test.ts │ ├── exo0.exercise.ts │ └── exo0.solution.ts ├── Failure.ts ├── exo4 - Dependency injection with Reader │ ├── exo4.test.ts │ ├── exo4.exercise.ts │ └── exo4.solution.ts ├── exo1 - Basic types │ ├── exo1.test.ts │ ├── exo1.exercise.ts │ └── exo1.solution.ts ├── exo7 - Collections │ ├── exo7.test.ts │ ├── exo7.exercise.ts │ └── exo7.solution.ts ├── exo3 - Sort with Ord │ ├── exo3.test.ts │ ├── exo3.exercise.ts │ └── exo3.solution.ts ├── exo5 - Nested data with traverse │ ├── exo5.test.ts │ ├── exo5.exercise.ts │ └── exo5.solution.ts ├── exo8 - Own combinators │ ├── exo8.exercise.ts │ ├── exo8.test.ts │ └── exo8.solution.ts └── exo2 - Combinators │ ├── exo2.exercise.ts │ ├── exo2.test.ts │ └── exo2.solution.ts ├── .prettierrc ├── .npmrc ├── .gitignore ├── .github └── dependabot.yml ├── jest.config.js ├── .vscode └── settings.json ├── tsconfig.json ├── .eslintrc.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.16.0 2 | -------------------------------------------------------------------------------- /.pnpmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | resolution-mode=highest -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './User'; 2 | -------------------------------------------------------------------------------- /src/readerTaskEither.ts: -------------------------------------------------------------------------------- 1 | export { readerTaskEither as rte } from 'fp-ts'; 2 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/domain/User/index.ts: -------------------------------------------------------------------------------- 1 | export * as User from './User'; 2 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/application/index.ts: -------------------------------------------------------------------------------- 1 | export * as Application from './services'; 2 | -------------------------------------------------------------------------------- /src/testUtils.ts: -------------------------------------------------------------------------------- 1 | export const isTestingSolution = () => process.env.TEST_SOLUTION === 'true'; 2 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/domain/User/Repository/index.ts: -------------------------------------------------------------------------------- 1 | export * as Repository from './Repository'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/domain/User/User.ts: -------------------------------------------------------------------------------- 1 | export * from './Repository'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | auto-install-peers=true 4 | node-linker=hoisted -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/domain/User/Repository/Repository.ts: -------------------------------------------------------------------------------- 1 | export * from './InMemoryUserRepository'; 2 | export * from './readerMethods'; 3 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/domain/User/type.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | name: string; 4 | bestFriendId: string; 5 | }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # pnpm 2 | pnpm-debug.log* 3 | .pnpm-store/ 4 | .yalc 5 | yalc.lock 6 | node_modules 7 | 8 | dist 9 | 10 | .DS_Store 11 | .idea/* 12 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/application/services/index.ts: -------------------------------------------------------------------------------- 1 | export * as NodeTimeService from './NodeTimeService'; 2 | export * as TimeService from './TimeService'; 3 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/application/services/NodeTimeService.ts: -------------------------------------------------------------------------------- 1 | import { TimeService } from './TimeService'; 2 | 3 | export class NodeTimeService implements TimeService { 4 | public thisYear() { 5 | return new Date().getFullYear(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '06:00' 8 | timezone: Europe/Paris 9 | open-pull-requests-limit: 1 10 | labels: 11 | - dependabot 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['/dist/', '/node_modules/'], 5 | globals: { 6 | transform: { 7 | '*': [ 8 | 'ts-jest', 9 | { 10 | diagnostics: { warnOnly: true }, 11 | }, 12 | ], 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/application/services/TimeService.ts: -------------------------------------------------------------------------------- 1 | import { getReaderMethod } from '../../../utils'; 2 | 3 | export interface TimeService { 4 | thisYear: () => number; 5 | } 6 | 7 | export interface Access { 8 | timeService: TimeService; 9 | } 10 | 11 | export const thisYear = getReaderMethod( 12 | ({ timeService }: Access) => timeService.thisYear, 13 | ); 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules/": true 4 | }, 5 | "eslint.packageManager": "yarn", 6 | "eslint.alwaysShowStatus": true, 7 | "javascript.validate.enable": false, 8 | "editor.formatOnSave": true, 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": "explicit" 11 | }, 12 | "typescript.tsdk": "node_modules/typescript/lib", 13 | "prettier.configPath": ".prettierrc", 14 | "eslint.format.enable": true 15 | } 16 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { reader } from 'fp-ts'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { Reader } from 'fp-ts/lib/Reader'; 4 | 5 | export const unimplemented = (..._args: any) => undefined as any; 6 | export const unimplementedAsync = () => () => undefined as any; 7 | 8 | export function sleep(ms: number) { 9 | return new Promise(resolve => setTimeout(resolve, ms)); 10 | } 11 | 12 | export const getReaderMethod = 13 | , R>( 14 | getMethod: (access: Access) => (...a: A) => R, 15 | ) => 16 | (...a: A): Reader => 17 | pipe( 18 | reader.ask(), 19 | reader.map(access => getMethod(access)(...a)), 20 | ); 21 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/domain/User/Repository/InMemoryUserRepository.ts: -------------------------------------------------------------------------------- 1 | import { taskEither } from 'fp-ts'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { TaskEither } from 'fp-ts/lib/TaskEither'; 4 | import { User } from '../User'; 5 | import { UserNotFoundError } from './readerMethods'; 6 | 7 | export class InMemoryUserRepository { 8 | protected aggregates: Map; 9 | 10 | constructor(aggregates: User[]) { 11 | this.aggregates = new Map(aggregates.map(user => [user.id, user])); 12 | } 13 | 14 | getById(userId: string): TaskEither { 15 | return pipe( 16 | this.aggregates.get(userId), 17 | taskEither.fromNullable(new UserNotFoundError()), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/domain/User/Repository/readerMethods.ts: -------------------------------------------------------------------------------- 1 | import { reader } from 'fp-ts'; 2 | import { pipe } from 'fp-ts/lib/function'; 3 | import { ReaderTaskEither } from 'fp-ts/lib/ReaderTaskEither'; 4 | import { User } from '../User'; 5 | import { InMemoryUserRepository } from './InMemoryUserRepository'; 6 | 7 | export interface Access { 8 | userRepository: InMemoryUserRepository; 9 | } 10 | 11 | export const getById = ( 12 | userId: string, 13 | ): ReaderTaskEither => 14 | pipe( 15 | reader.ask(), 16 | reader.map(({ userRepository }) => userRepository.getById(userId)), 17 | ); 18 | 19 | export class UserNotFoundError extends Error { 20 | constructor() { 21 | super('User not found'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2019", 9 | "lib": ["es2019", "es2020.string", "es2020.symbol.wellknown"], 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "resolveJsonModule": true, 18 | "outDir": "./dist", 19 | "rootDirs": ["src"], 20 | "incremental": true, 21 | "skipLibCheck": true, 22 | "sourceMap": true, 23 | "inlineSources": true, 24 | "sourceRoot": "/", 25 | "noEmitHelpers": true, 26 | "importHelpers": true, 27 | "typeRoots": ["node_modules/@types", "src/types"] 28 | }, 29 | "include": ["src"] 30 | } 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | }, 5 | parserOptions: { 6 | project: './tsconfig.json', 7 | tsconfigRootDir: __dirname, 8 | }, 9 | extends: [ 10 | 'plugin:fp/recommended', 11 | ], 12 | plugins: ['fp', 'inato'], 13 | rules: { 14 | 'no-unused-vars': 'off', 15 | '@typescript-eslint/no-unused-vars-experimental': ['error'], 16 | 'no-useless-constructor': 'off', 17 | '@typescript-eslint/no-useless-constructor': ['error'], 18 | '@typescript-eslint/camelcase': [ 19 | 'error', 20 | { 21 | properties: 'never', 22 | }, 23 | ], 24 | 'react-hooks/rules-of-hooks': 'off', 25 | 'import/extensions': [ 26 | 'error', 27 | { 28 | ts: 'never', 29 | }, 30 | ], 31 | 'fp/no-throw':'off', 32 | 'fp/no-class': 'off', 33 | 'fp/no-mutation': 'off', 34 | 'fp/no-nil': 'off', 35 | 'fp/no-this': 'off', 36 | 'fp/no-unused-expression': 'off', 37 | 'inato/no-factories-outside-of-tests': 'error', 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fp-ts-training", 3 | "version": "1.0.0", 4 | "description": "Learning fp-ts while having fun", 5 | "author": "Inato engineering ", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/inato/fp-ts-training.git" 11 | }, 12 | "homepage": "https://github.com/inato/fp-ts-training", 13 | "scripts": { 14 | "build": "pnpm tsc -b", 15 | "check": "pnpm tsc --noEmit", 16 | "test": "jest --runInBand", 17 | "test:watch": "jest --runInBand --watch", 18 | "test:solution": "TEST_SOLUTION=true jest --runInBand", 19 | "test:solution-watch": "pnpm test:solution --watch" 20 | }, 21 | "dependencies": { 22 | "decod": "^6.1.6", 23 | "fp-ts": "^2.16.10", 24 | "tslib": "^2.5.0" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^29.5.1", 28 | "@types/node": "^18.15.13", 29 | "eslint": "^8.39.0", 30 | "eslint-plugin-fp": "^2.3.0", 31 | "jest": "^29.5.0", 32 | "ts-jest": "^29.1.0", 33 | "typescript": "^5.0.4" 34 | }, 35 | "packageManager": "pnpm@8.15.4" 36 | } 37 | -------------------------------------------------------------------------------- /src/exo0 - Composing with pipe and flow/exo0.test.ts: -------------------------------------------------------------------------------- 1 | import { isTestingSolution } from '../testUtils'; 2 | import * as exercise from './exo0.exercise'; 3 | import * as solution from './exo0.solution'; 4 | 5 | const { isOddF, isOddP, next, next3 } = isTestingSolution() 6 | ? solution 7 | : exercise; 8 | 9 | describe('exo0', () => { 10 | describe('isOddP', () => { 11 | it('should return true if the provided number is odd', () => { 12 | const oddValue = 1; 13 | 14 | expect(isOddP(oddValue)).toBe(true); 15 | }); 16 | 17 | it('should return false if the provided number is even', () => { 18 | const oddValue = 2; 19 | 20 | expect(isOddP(oddValue)).toBe(false); 21 | }); 22 | }); 23 | 24 | describe('isOddF', () => { 25 | it('should return true if the provided number is odd', () => { 26 | const oddValue = 1; 27 | 28 | expect(isOddF(oddValue)).toBe(true); 29 | }); 30 | 31 | it('should return false if the provided number is even', () => { 32 | const oddValue = 2; 33 | 34 | expect(isOddF(oddValue)).toBe(false); 35 | }); 36 | }); 37 | 38 | describe('next', () => { 39 | it('should return the correct next element in the Collatz sequence', () => { 40 | expect(next(4)).toBe(2); 41 | expect(next(2)).toBe(1); 42 | expect(next(1)).toBe(4); 43 | }); 44 | }); 45 | 46 | describe('next3', () => { 47 | it('should return the correct element in the Collatz sequence 3 steps ahead', () => { 48 | expect(next3(12)).toBe(10); 49 | expect(next3(10)).toBe(8); 50 | expect(next3(8)).toBe(1); 51 | expect(next3(1)).toBe(1); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/Failure.ts: -------------------------------------------------------------------------------- 1 | import * as decod from 'decod'; 2 | import { either, task } from 'fp-ts'; 3 | 4 | const decodeErrorMessage = decod.at('message', decod.string); 5 | 6 | export enum FailureType { 7 | Unexpected = 'FailureType_Unexpected', 8 | } 9 | 10 | export type UnexpectedFailure = Failure; 11 | 12 | export class Failure { 13 | constructor({ 14 | reason, 15 | type, 16 | originalError, 17 | }: { 18 | reason: string; 19 | type: T; 20 | originalError?: Error | unknown; 21 | }) { 22 | this.reason = reason; 23 | this.type = type; 24 | if (originalError instanceof Error) { 25 | this.originalError = originalError; 26 | } 27 | } 28 | 29 | reason: string; 30 | 31 | type: T; 32 | 33 | originalError?: Error; 34 | 35 | toError(): Error { 36 | if (this.originalError) return this.originalError; 37 | return new Error(this.reason); 38 | } 39 | 40 | static builder(type: T) { 41 | return (reason: string, originalError?: Error | unknown) => 42 | new Failure({ 43 | reason, 44 | type, 45 | originalError, 46 | }); 47 | } 48 | 49 | static unexpected = Failure.builder(FailureType.Unexpected); 50 | 51 | static fromUnknownError(originalError: unknown | Error): UnexpectedFailure { 52 | return Failure.unexpected(decodeErrorMessage(originalError), originalError); 53 | } 54 | 55 | static toType(type: T): (failure: Failure) => Failure { 56 | return (failure: Failure) => 57 | new Failure({ 58 | reason: failure.reason, 59 | type, 60 | originalError: failure.originalError, 61 | }); 62 | } 63 | 64 | static toUnexpected = Failure.toType(FailureType.Unexpected); 65 | 66 | private static throw = (failure: Failure) => { 67 | throw failure.toError(); 68 | }; 69 | 70 | static eitherUnsafeGet = either.getOrElseW(Failure.throw); 71 | 72 | static taskEitherUnsafeGet = task.map(this.eitherUnsafeGet); 73 | } 74 | 75 | export type FailureTypes> = F[keyof F]; 76 | export interface FailureTypings> { 77 | types: FailureTypes; 78 | failure: Failure>; 79 | } 80 | -------------------------------------------------------------------------------- /src/exo4 - Dependency injection with Reader/exo4.test.ts: -------------------------------------------------------------------------------- 1 | import { isTestingSolution } from '../testUtils'; 2 | import * as exercise from './exo4.exercise'; 3 | import * as solution from './exo4.solution'; 4 | 5 | const { Country, exclamation, greet, excitedlyGreet } = isTestingSolution() 6 | ? solution 7 | : exercise; 8 | 9 | describe('exo4', () => { 10 | describe('greet', () => { 11 | it('should greet Alice in french', () => { 12 | const result = greet('Alice')(Country.France); 13 | 14 | expect(result).toStrictEqual('Bonjour, Alice'); 15 | }); 16 | 17 | it('should greet Bernardo in spanish', () => { 18 | const result = greet('Bernardo')(Country.Spain); 19 | 20 | expect(result).toStrictEqual('Buenos dìas, Bernardo'); 21 | }); 22 | 23 | it('should greet Crystal in english', () => { 24 | const result = greet('Crystal')(Country.USA); 25 | 26 | expect(result).toStrictEqual('Hello, Crystal'); 27 | }); 28 | }); 29 | 30 | describe('exclamation', () => { 31 | it('should add exclamation in french style (with a space before "!")', () => { 32 | const result = exclamation('Youpi')(Country.France); 33 | 34 | expect(result).toStrictEqual('Youpi !'); 35 | }); 36 | 37 | it('should add exclamation in spanish style (between "¡" and "!")', () => { 38 | const result = exclamation('Olé')(Country.Spain); 39 | 40 | expect(result).toStrictEqual('¡Olé!'); 41 | }); 42 | 43 | it('should add exclamation in english style (with no space before "!")', () => { 44 | const result = exclamation('Yeah')(Country.USA); 45 | 46 | expect(result).toStrictEqual('Yeah!'); 47 | }); 48 | }); 49 | 50 | describe('excitedlyGreet', () => { 51 | it('should excitedly greet Alice in french', () => { 52 | const result = excitedlyGreet('Alice')(Country.France); 53 | 54 | expect(result).toStrictEqual('Bonjour, Alice !'); 55 | }); 56 | 57 | it('should excitedly greet Bernardo in spanish', () => { 58 | const result = excitedlyGreet('Bernardo')(Country.Spain); 59 | 60 | expect(result).toStrictEqual('¡Buenos dìas, Bernardo!'); 61 | }); 62 | 63 | it('should excitedly greet Crystal in english', () => { 64 | const result = excitedlyGreet('Crystal')(Country.USA); 65 | 66 | expect(result).toStrictEqual('Hello, Crystal!'); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/exo1 - Basic types/exo1.test.ts: -------------------------------------------------------------------------------- 1 | import { either, option } from 'fp-ts'; 2 | import * as exercise from './exo1.exercise'; 3 | import * as solution from './exo1.solution'; 4 | import { isTestingSolution } from '../testUtils'; 5 | 6 | const { 7 | divide, 8 | DivisionByZero, 9 | safeDivide, 10 | safeDivideWithError, 11 | asyncDivide, 12 | asyncSafeDivideWithError, 13 | } = isTestingSolution() ? solution : exercise; 14 | 15 | describe('exo1', () => { 16 | describe('divide', () => { 17 | it('should return the result of dividing two numbers', () => { 18 | expect(divide(25, 5)).toEqual(5); 19 | }); 20 | 21 | it('should return Infinity or -Infinity if the denominator is zero', () => { 22 | expect(divide(25, 0)).toBe(Infinity); 23 | expect(divide(-25, 0)).toBe(-Infinity); 24 | }); 25 | }); 26 | 27 | describe('safeDivide', () => { 28 | it('should return the result of dividing two numbers', () => { 29 | expect(safeDivide(25, 5)).toStrictEqual(option.some(5)); 30 | }); 31 | 32 | it('should return option.none if the denominator is zero', () => { 33 | expect(safeDivide(25, 0)).toStrictEqual(option.none); 34 | expect(safeDivide(-25, 0)).toStrictEqual(option.none); 35 | }); 36 | }); 37 | 38 | describe('safeDivideWithError', () => { 39 | it('should return the result of dividing two numbers', () => { 40 | expect(safeDivideWithError(25, 5)).toStrictEqual(either.right(5)); 41 | }); 42 | 43 | it('should return either.left(DivisionByZero) if the denominator is zero', () => { 44 | expect(safeDivideWithError(25, 0)).toStrictEqual( 45 | either.left(DivisionByZero), 46 | ); 47 | expect(safeDivideWithError(-25, 0)).toStrictEqual( 48 | either.left(DivisionByZero), 49 | ); 50 | }); 51 | }); 52 | 53 | describe('asyncDivide', () => { 54 | it('should eventually return the result of dividing two numbers', async () => { 55 | const result = await asyncDivide(25, 5); 56 | 57 | expect(result).toEqual(5); 58 | }); 59 | 60 | it('should eventually return Infinity if the denominator is zero', async () => { 61 | await expect(asyncDivide(25, 0)).rejects.toThrow(); 62 | await expect(asyncDivide(-25, 0)).rejects.toThrow(); 63 | }); 64 | }); 65 | 66 | describe('asyncSafeDivideWithError', () => { 67 | it('should eventually return the result of dividing two numbers', async () => { 68 | const result = await asyncSafeDivideWithError(25, 5)(); 69 | 70 | expect(result).toStrictEqual(either.right(5)); 71 | }); 72 | 73 | it('should eventually return either.left(DivisionByZero) if the denominator is zero', async () => { 74 | const resultA = await asyncSafeDivideWithError(25, 0)(); 75 | const resultB = await asyncSafeDivideWithError(-25, 0)(); 76 | 77 | expect(resultA).toStrictEqual(either.left(DivisionByZero)); 78 | expect(resultB).toStrictEqual(either.left(DivisionByZero)); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/exo7 - Collections/exo7.test.ts: -------------------------------------------------------------------------------- 1 | import { isTestingSolution } from '../testUtils'; 2 | import * as exercise from './exo7.exercise'; 3 | import * as solution from './exo7.solution'; 4 | 5 | const { 6 | allPageViews, 7 | intersectionPageViews, 8 | mapWithConcatenatedEntries, 9 | mapWithLastEntry, 10 | nonPrimeOdds, 11 | numberArray, 12 | numberArrayFromSet, 13 | numberSet, 14 | primeOdds, 15 | } = isTestingSolution() ? solution : exercise; 16 | 17 | describe('exo7', () => { 18 | describe('numberSet', () => { 19 | it('should be the set of unique values from `numberArray`', () => { 20 | expect(numberSet).toStrictEqual(new Set(numberArray)); 21 | }); 22 | }); 23 | 24 | describe('numberArrayFromSet', () => { 25 | it('should be the array of unique values from `numberArray`', () => { 26 | expect(numberArrayFromSet).toStrictEqual( 27 | [...new Set(numberArray)].sort((a, b) => a - b), 28 | ); 29 | }); 30 | }); 31 | 32 | describe('mapWithLastEntry', () => { 33 | it('should construct the map from `associativeArray` keeping only the last entry for colliding keys', () => { 34 | expect(mapWithLastEntry).toStrictEqual( 35 | new Map([ 36 | [1, 'Alice'], 37 | [3, 'Clara'], 38 | [4, 'Denise'], 39 | [2, 'Robert'], 40 | ]), 41 | ); 42 | }); 43 | }); 44 | 45 | describe('mapWithConcatenatedEntries', () => { 46 | it('should construct the map from `associativeArray` concatenating values for colliding keys', () => { 47 | expect(mapWithConcatenatedEntries).toStrictEqual( 48 | new Map([ 49 | [1, 'Alice'], 50 | [3, 'Clara'], 51 | [4, 'Denise'], 52 | [2, 'BobRobert'], 53 | ]), 54 | ); 55 | }); 56 | }); 57 | 58 | describe('nonPrimeOdds', () => { 59 | it('should contain only the odd numbers that are not prime', () => { 60 | expect(nonPrimeOdds).toStrictEqual(new Set([1, 9])); 61 | }); 62 | }); 63 | 64 | describe('primeOdds', () => { 65 | it('should contain only the odd numbers that are also prime', () => { 66 | expect(primeOdds).toStrictEqual(new Set([3, 5, 7])); 67 | }); 68 | }); 69 | 70 | describe('allPageViews', () => { 71 | it('should contain the map of aggregated page views from both sources of analytics', () => { 72 | expect(allPageViews).toStrictEqual( 73 | new Map([ 74 | ['home', { page: 'home', views: 15 }], 75 | ['about', { page: 'about', views: 2 }], 76 | ['blog', { page: 'blog', views: 42 }], 77 | ['faq', { page: 'faq', views: 5 }], 78 | ]), 79 | ); 80 | }); 81 | }); 82 | 83 | describe('intersectionPageViews', () => { 84 | it('should contain the map of intersecting page views from both sources of analytics', () => { 85 | expect(intersectionPageViews).toStrictEqual( 86 | new Map([ 87 | ['home', { page: 'home', views: 15 }], 88 | ['blog', { page: 'blog', views: 42 }], 89 | ]), 90 | ); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/exo0 - Composing with pipe and flow/exo0.exercise.ts: -------------------------------------------------------------------------------- 1 | // `fp-ts` training introduction 2 | // Composing computations with `pipe` and `flow` 3 | 4 | // Functional programming is all about composing small functions together like 5 | // Lego bricks to build more and more complex computations. 6 | // 7 | // Strictly speaking, composing two functions `f` and `g` means applying the 8 | // result of the first one to the second one. By applying this composition 9 | // over and over you can chain multiple functions together. 10 | // 11 | // The `fp-ts` library provides helpers to do that: 12 | // - `pipe` which first needs to be fed a value to start the pipe and then 13 | // any number of functions to be applied sequentially. 14 | // - `flow` which is the same thing but where we do not have to provide the 15 | // first value. It will then return a function which will expect that value 16 | // to be provided 17 | // 18 | // ```ts 19 | // flow(f, g, h) === x => pipe(x, f, g, h) 20 | // pipe(x, f, g, h) === flow(f, g, h)(x) 21 | // ``` 22 | // 23 | // NOTES: 24 | // - `flow(f) === f` 25 | // - `pipe(x, f) === f(x)` 26 | 27 | import { unimplemented } from '../utils'; 28 | 29 | export const isEven = (value: number) => value % 2 === 0; 30 | 31 | export const not = (value: boolean) => !value; 32 | 33 | /////////////////////////////////////////////////////////////////////////////// 34 | // PIPE & FLOW // 35 | /////////////////////////////////////////////////////////////////////////////// 36 | 37 | // For this exercise each function needs to be implemented using both forms: 38 | // - a `pipe` implementation (P suffix) 39 | // - a `flow` implementation (F suffix) 40 | 41 | // Using only the two helpers `isEven` and `not` at the top of this file (and 42 | // `pipe` or `flow`), write the function `isOdd` that checks if a number is 43 | // odd. 44 | 45 | export const isOddP: (value: number) => boolean = unimplemented; 46 | 47 | export const isOddF: (value: number) => boolean = unimplemented; 48 | 49 | // We will write a function that for any given number, computes the next 50 | // one according to the following rules: 51 | // - if n is even => divide it by two 52 | // - if n is odd => triple it and add one 53 | // 54 | // This sequence is the object of The Collatz conjecture: https://en.wikipedia.org/wiki/Collatz_conjecture 55 | // 56 | // Below is the functional equivalent of the control flow statement if-else. 57 | 58 | export const ifThenElse = 59 | (onTrue: () => A, onFalse: () => A) => 60 | (condition: boolean) => 61 | condition ? onTrue() : onFalse(); 62 | 63 | // Using `pipe` and `ifThenElse`, write the function that computes the next step in the Collatz 64 | // sequence. 65 | 66 | export const next: (value: number) => number = unimplemented; 67 | 68 | // Using only `flow` and `next`, write the function that for any given number 69 | // a_n from the Collatz sequence, returns the number a_n+3 (i.e. the number 70 | // three steps ahead in the sequence). 71 | 72 | export const next3: (value: number) => number = unimplemented; 73 | -------------------------------------------------------------------------------- /src/exo0 - Composing with pipe and flow/exo0.solution.ts: -------------------------------------------------------------------------------- 1 | // `fp-ts` training introduction 2 | // Composing computations with `pipe` and `flow` 3 | 4 | // Functional programming is all about composing small functions together like 5 | // Lego bricks to build more and more complex computations. 6 | // 7 | // Strictly speaking, composing two functions `f` and `g` means applying the 8 | // result of the first one to the second one. By applying this composition 9 | // over and over you can chain multiple functions together. 10 | // 11 | // The `fp-ts` library provides helpers to do that: 12 | // - `pipe` which first needs to be fed a value to start the pipe and then 13 | // any number of functions to be applied sequentially. 14 | // - `flow` which is the same thing but where we do not have to provide the 15 | // first value. It will then return a function which will expect that value 16 | // to be provided 17 | // 18 | // ```ts 19 | // flow(f, g, h) === x => pipe(x, f, g, h) 20 | // pipe(x, f, g, h) === flow(f, g, h)(x) 21 | // ``` 22 | // 23 | // NOTES: 24 | // - `flow(f) === f` 25 | // - `pipe(x, f) === f(x)` 26 | 27 | import { flow, pipe } from 'fp-ts/function'; 28 | 29 | export const isEven = (value: number) => value % 2 === 0; 30 | 31 | export const not = (value: boolean) => !value; 32 | 33 | /////////////////////////////////////////////////////////////////////////////// 34 | // PIPE & FLOW // 35 | /////////////////////////////////////////////////////////////////////////////// 36 | 37 | // For this exercise each function needs to be implemented using both forms: 38 | // - a `pipe` implementation (P suffix) 39 | // - a `flow` implementation (F suffix) 40 | 41 | // Using only the two helpers `isEven` and `not` at the top of this file (and 42 | // `pipe` or `flow`), write the function `isOdd` that checks if a number is 43 | // odd. 44 | 45 | export const isOddP = (value: number) => pipe(value, isEven, not); 46 | 47 | export const isOddF = flow(isEven, not); 48 | 49 | // We will write a function that for any given number, computes the next 50 | // one according to the following rules: 51 | // - if n is even => divide it by two 52 | // - if n is odd => triple it and add one 53 | // 54 | // This sequence is the object of The Collatz conjecture: https://en.wikipedia.org/wiki/Collatz_conjecture 55 | // 56 | // Below is the functional equivalent of the control flow statement if-else. 57 | 58 | export const ifThenElse = 59 | (onTrue: () => A, onFalse: () => A) => 60 | (condition: boolean) => 61 | condition ? onTrue() : onFalse(); 62 | 63 | // Using `pipe` and `ifThenElse`, write the function that computes the next step in the Collatz 64 | // sequence. 65 | 66 | export const next = (value: number) => 67 | pipe( 68 | value, 69 | isEven, 70 | ifThenElse( 71 | () => value / 2, 72 | () => value * 3 + 1, 73 | ), 74 | ); 75 | 76 | // Using only `flow` and `next`, write the function that for any given number 77 | // a_n from the Collatz sequence, returns the number a_n+3 (ie. the number 78 | // three steps ahead in the sequence). 79 | 80 | export const next3 = flow(next, next, next); 81 | -------------------------------------------------------------------------------- /src/exo6 - ReaderTaskEither/exo6.test.ts: -------------------------------------------------------------------------------- 1 | import { either } from 'fp-ts'; 2 | import { Application } from './application'; 3 | import { User } from './domain'; 4 | import * as exercise from './exo6.exercise'; 5 | import * as solution from './exo6.solution'; 6 | import { isTestingSolution } from '../testUtils'; 7 | 8 | const { 9 | getCapitalizedUserName, 10 | getConcatenationOfTheBestFriendNameAndUserName, 11 | getConcatenationOfTheTwoUserNames, 12 | getConcatenationOfTheTwoUserNamesUsingAp, 13 | getConcatenationOfUserNameAndCurrentYear, 14 | } = isTestingSolution() ? solution : exercise; 15 | 16 | describe('exo6', () => { 17 | it('should return the capitalized user name', async () => { 18 | const usecase = getCapitalizedUserName({ userId: '1' })({ 19 | userRepository: new User.Repository.InMemoryUserRepository([ 20 | { id: '1', name: 'rob', bestFriendId: '' }, 21 | ]), 22 | }); 23 | 24 | const result = await usecase(); 25 | 26 | expect(result).toEqual(either.right('Rob')); 27 | }); 28 | 29 | it('should return the concatenation of the two capitalized user names', async () => { 30 | const usecase = getConcatenationOfTheTwoUserNames({ 31 | userIdOne: '1', 32 | userIdTwo: '2', 33 | })({ 34 | userRepository: new User.Repository.InMemoryUserRepository([ 35 | { id: '1', name: 'rob', bestFriendId: '' }, 36 | { id: '2', name: 'scott', bestFriendId: '' }, 37 | ]), 38 | }); 39 | 40 | const result = await usecase(); 41 | 42 | expect(result).toEqual(either.right('RobScott')); 43 | }); 44 | 45 | it('should return the concatenation of the two capitalized user names using rte.ap', async () => { 46 | const usecase = getConcatenationOfTheTwoUserNamesUsingAp({ 47 | userIdOne: '1', 48 | userIdTwo: '2', 49 | })({ 50 | userRepository: new User.Repository.InMemoryUserRepository([ 51 | { id: '1', name: 'rob', bestFriendId: '' }, 52 | { id: '2', name: 'scott', bestFriendId: '' }, 53 | ]), 54 | }); 55 | 56 | const result = await usecase(); 57 | 58 | expect(result).toEqual(either.right('RobScott')); 59 | }); 60 | 61 | it('should return the concatenation of the two capitalized user names based on the best friend relation', async () => { 62 | const usecase = getConcatenationOfTheBestFriendNameAndUserName({ 63 | userIdOne: '1', 64 | })({ 65 | userRepository: new User.Repository.InMemoryUserRepository([ 66 | { id: '1', name: 'rob', bestFriendId: '2' }, 67 | { id: '2', name: 'scott', bestFriendId: '1' }, 68 | ]), 69 | }); 70 | 71 | const result = await usecase(); 72 | 73 | expect(result).toEqual(either.right('RobScott')); 74 | }); 75 | 76 | it('should return the concatenation of the user name and the current year', async () => { 77 | const timeservice = new Application.NodeTimeService.NodeTimeService(); 78 | 79 | const usecase = getConcatenationOfUserNameAndCurrentYear({ 80 | userIdOne: '1', 81 | })({ 82 | userRepository: new User.Repository.InMemoryUserRepository([ 83 | { id: '1', name: 'rob', bestFriendId: '2' }, 84 | { id: '2', name: 'scott', bestFriendId: '1' }, 85 | ]), 86 | 87 | timeService: timeservice, 88 | }); 89 | 90 | const result = await usecase(); 91 | 92 | expect(result).toEqual(either.right(`rob${timeservice.thisYear()}`)); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /src/exo1 - Basic types/exo1.exercise.ts: -------------------------------------------------------------------------------- 1 | // `fp-ts` training Exercise 1 2 | // Basic types: 3 | // - Option 4 | // - Either 5 | // - TaskEither 6 | 7 | import { Either } from 'fp-ts/Either'; 8 | import { Option } from 'fp-ts/Option'; 9 | import { TaskEither } from 'fp-ts/TaskEither'; 10 | import { unimplemented, sleep, unimplementedAsync } from '../utils'; 11 | 12 | export const divide = (a: number, b: number): number => { 13 | return a / b; 14 | }; 15 | 16 | /////////////////////////////////////////////////////////////////////////////// 17 | // OPTION // 18 | /////////////////////////////////////////////////////////////////////////////// 19 | 20 | // Write the safe version (meaning it handles the case where b is 0) of `divide` with signature: 21 | // safeDivide : (a: number, b: number) => Option 22 | // 23 | // HINT: Option has two basic constructors: 24 | // - `option.some(value)` 25 | // - `option.none` 26 | 27 | export const safeDivide: (a: number, b: number) => Option = 28 | unimplemented; 29 | 30 | // You probably wrote `safeDivide` using `if` statements, and it's perfectly valid! 31 | // There are ways to not use `if` statements. 32 | // Keep in mind that extracting small functions out of pipes and using `if` statements in them 33 | // is perfectly fine and is sometimes more readable than not using `if`. 34 | // 35 | // BONUS: Try now to re-write `safeDivide` without any `if` 36 | // 37 | // HINT: Have a look at `fromPredicate` constructor 38 | 39 | /////////////////////////////////////////////////////////////////////////////// 40 | // EITHER // 41 | /////////////////////////////////////////////////////////////////////////////// 42 | 43 | // Write the safe version of `divide` with signature: 44 | // safeDivideWithError : (a: number, b: number) => Either 45 | // 46 | // BONUS POINT: Implement `safeDivideWithError` in terms of `safeDivide`. 47 | // 48 | // HINT : Either has two basic constructors: 49 | // - `either.left(leftValue)` 50 | // - `either.right(rightValue)` 51 | // as well as "smarter" constructors like: 52 | // - `either.fromOption(() => leftValue)(option)` 53 | 54 | // Here is a simple error type to help you: 55 | export type DivisionByZeroError = 'Error: Division by zero'; 56 | export const DivisionByZero = 'Error: Division by zero' as const; 57 | 58 | export const safeDivideWithError: ( 59 | a: number, 60 | b: number, 61 | ) => Either = unimplemented; 62 | 63 | /////////////////////////////////////////////////////////////////////////////// 64 | // TASKEITHER // 65 | /////////////////////////////////////////////////////////////////////////////// 66 | 67 | // Now let's say we have a (pretend) API call that will perform the division for us 68 | // (throwing an error when the denominator is 0) 69 | export const asyncDivide = async (a: number, b: number) => { 70 | await sleep(1000); 71 | 72 | if (b === 0) { 73 | throw new Error('BOOM!'); 74 | } 75 | 76 | return a / b; 77 | }; 78 | 79 | // Write the safe version of `asyncDivide` with signature: 80 | // asyncSafeDivideWithError : (a: number, b: number) => TaskEither 81 | // 82 | // HINT: TaskEither has a special constructor to transform a Promise into 83 | // a TaskEither: 84 | // - `taskEither.tryCatch(f: () => promise, onReject: reason => leftValue)` 85 | 86 | export const asyncSafeDivideWithError: ( 87 | a: number, 88 | b: number, 89 | ) => TaskEither = unimplementedAsync; 90 | -------------------------------------------------------------------------------- /src/exo4 - Dependency injection with Reader/exo4.exercise.ts: -------------------------------------------------------------------------------- 1 | // `fp-ts` training Exercise 4 2 | // Dependency injection with `Reader` 3 | 4 | import { Reader } from 'fp-ts/Reader'; 5 | 6 | import { unimplemented } from '../utils'; 7 | 8 | // Sometimes, a function can have a huge amount of dependencies (services, 9 | // repositories, ...) and it is often impractical (not to say truly annoying) 10 | // to thread those values through multiple levels of the call stack. 11 | // 12 | // That's precisely what dependency injection is meant to solve, and it's not a 13 | // concept exclusive to OOP! 14 | // In the world of FP, the so-called `Reader` construct (just a fancy word for 15 | // a partially applied function) offers precisely those capabilities. 16 | 17 | /////////////////////////////////////////////////////////////////////////////// 18 | // SETUP // 19 | /////////////////////////////////////////////////////////////////////////////// 20 | 21 | // Let's consider a small range of countries (here, France, Spain and the USA): 22 | 23 | export enum Country { 24 | France = 'France', 25 | Spain = 'Spain', 26 | USA = 'USA', 27 | } 28 | 29 | /////////////////////////////////////////////////////////////////////////////// 30 | // ASK // 31 | /////////////////////////////////////////////////////////////////////////////// 32 | 33 | // These countries have different rules on how to add exclamation to a sentence: 34 | // - French speakers will add an exclamation point at the end preceded by a 35 | // space: "Youpi !" 36 | // - spanish speakers begin the sentence with an inverted exclamation point and 37 | // finish it by a regular one: `¡Olé!` 38 | // - english speakers will have no space before the exclamation point at the end 39 | // of the sentence: `Yeah!` 40 | // 41 | // The following function should take a sentence as input and return a `Reader` 42 | // that will at some point expect a `Country` and return the sentence with the 43 | // proper exclamation style. 44 | // 45 | // HINT: Take a look at `reader.ask` to access the environment value 46 | 47 | export const exclamation: (sentence: string) => Reader = 48 | unimplemented(); 49 | 50 | // Obviously, different countries often mean different languages and so 51 | // different words for saying "Hello": 52 | 53 | export const sayHello = (country: Country): string => { 54 | switch (country) { 55 | case Country.France: 56 | return 'Bonjour'; 57 | case Country.Spain: 58 | return 'Buenos dìas'; 59 | case Country.USA: 60 | return 'Hello'; 61 | } 62 | }; 63 | 64 | // Using the `sayHello` function above, write the `greet` function that 65 | // delivers a personalized greeting to a person based on their name. 66 | // The output should look something like `${hello}, ${name}`. 67 | // 68 | // HINT: Remember that a `Reader` is just an alias for `(r: R) => T` 69 | // 70 | // HINT: You can look into `reader.map` to modify the output of a `Reader` 71 | // action. 72 | 73 | export const greet: (name: string) => Reader = unimplemented(); 74 | 75 | // Finally, we are going to compose multiple `Reader`s together. 76 | // 77 | // Sometimes, a simple greeting is not enough, we want to communicate our 78 | // excitement to see the person. 79 | // Luckily, we already know how to greet them normally (`greet`) and how to 80 | // add excitement to a sentence (`exclamation`). 81 | // 82 | // Compose those two to complete the `excitedlyGreet` function below: 83 | // 84 | // HINT: As with other wrapper types in `fp-ts`, `reader` offers a way of 85 | // composing effects with `reader.flatMap`. 86 | 87 | export const excitedlyGreet: (name: string) => Reader = 88 | unimplemented(); 89 | -------------------------------------------------------------------------------- /src/exo1 - Basic types/exo1.solution.ts: -------------------------------------------------------------------------------- 1 | // `fp-ts` training Exercise 1 2 | // Basic types: 3 | // - Option 4 | // - Either 5 | // - TaskEither 6 | 7 | import { either, option, taskEither } from 'fp-ts'; 8 | import { flow, pipe } from 'fp-ts/function'; 9 | 10 | import { sleep } from '../utils'; 11 | 12 | export const divide = (a: number, b: number): number => { 13 | return a / b; 14 | }; 15 | 16 | /////////////////////////////////////////////////////////////////////////////// 17 | // OPTION // 18 | /////////////////////////////////////////////////////////////////////////////// 19 | 20 | // Write the safe version (meaning it handles the case where b is 0) of `divide` with signature: 21 | // safeDivide : (a: number, b: number) => Option 22 | // 23 | // HINT: Option has two basic constructors: 24 | // - `option.some(value)` 25 | // - `option.none` 26 | 27 | export const safeDivide = (a: number, b: number) => { 28 | if (b === 0) { 29 | return option.none; 30 | } 31 | 32 | return option.some(a / b); 33 | }; 34 | 35 | // You probably wrote `safeDivide` using `if` statements and it's perfectly valid! 36 | // There are ways to not use `if` statements. 37 | // Keep in mind that extracting small functions out of pipes and using `if` statements in them 38 | // is perfectly fine and is sometimes more readable than not using `if`. 39 | // 40 | // BONUS: Try now to re-write `safeDivide` without any `if` 41 | // 42 | // HINT: Have a look at `fromPredicate` constructor 43 | 44 | export const safeDivideBonus = (a: number, b: number) => 45 | pipe( 46 | b, 47 | option.fromPredicate(n => n != 0), 48 | option.map(b => a / b), 49 | ); 50 | 51 | /////////////////////////////////////////////////////////////////////////////// 52 | // EITHER // 53 | /////////////////////////////////////////////////////////////////////////////// 54 | 55 | // Write the safe version of `divide` with signature: 56 | // safeDivideWithError : (a: number, b: number) => Either 57 | // 58 | // BONUS POINT: Implement `safeDivideWithError` in terms of `safeDivide`. 59 | // 60 | // HINT : Either has two basic constructors: 61 | // - `either.left(leftValue)` 62 | // - `either.right(rightValue)` 63 | // as well as "smarter" constructors like: 64 | // - `either.fromOption(() => leftValue)(option)` 65 | 66 | // Here is an simple error type to help you: 67 | export const DivisionByZero = 'Error: Division by zero' as const; 68 | 69 | export const safeDivideWithError = flow( 70 | safeDivide, 71 | either.fromOption(() => DivisionByZero), 72 | ); 73 | 74 | /////////////////////////////////////////////////////////////////////////////// 75 | // TASKEITHER // 76 | /////////////////////////////////////////////////////////////////////////////// 77 | 78 | // Now let's say we have a (pretend) API call that will perform the division for us 79 | // (throwing an error when the denominator is 0) 80 | export const asyncDivide = async (a: number, b: number) => { 81 | await sleep(1000); 82 | 83 | if (b === 0) { 84 | throw new Error('BOOM!'); 85 | } 86 | 87 | return a / b; 88 | }; 89 | 90 | // Write the safe version of `asyncDivide` with signature: 91 | // asyncSafeDivideWithError : (a: number, b: number) => TaskEither 92 | // 93 | // HINT: TaskEither has a special constructor to transform a Promise into 94 | // a TaskEither: 95 | // - `taskEither.tryCatch(f: () => promise, onReject: reason => leftValue)` 96 | 97 | export const asyncSafeDivideWithError = (a: number, b: number) => 98 | taskEither.tryCatch( 99 | () => asyncDivide(a, b), 100 | () => DivisionByZero, 101 | ); 102 | -------------------------------------------------------------------------------- /src/exo3 - Sort with Ord/exo3.test.ts: -------------------------------------------------------------------------------- 1 | import { option } from 'fp-ts'; 2 | import * as exercise from './exo3.exercise'; 3 | import * as solution from './exo3.solution'; 4 | import { isTestingSolution } from '../testUtils'; 5 | 6 | const { 7 | sortStrings, 8 | sortNumbers, 9 | sortNumbersDescending, 10 | sortOptionalNumbers, 11 | sortPersonsByName, 12 | sortPersonsByAge, 13 | sortPersonsByAgeThenByName, 14 | } = isTestingSolution() ? solution : exercise; 15 | 16 | describe('exo3', () => { 17 | describe('sortStrings', () => { 18 | it('should return an alphabetically sorted array of strings', () => { 19 | const strings = ['xyz', 'aba', 'ori', 'aab', 'ghl']; 20 | 21 | const result = sortStrings(strings); 22 | const expected = ['aab', 'aba', 'ghl', 'ori', 'xyz']; 23 | 24 | expect(result).toStrictEqual(expected); 25 | }); 26 | }); 27 | 28 | describe('sortNumbers', () => { 29 | it('should return a sorted array of numbers', () => { 30 | const numbers = [1337, 42, 5701]; 31 | 32 | const result = sortNumbers(numbers); 33 | const expected = [42, 1337, 5701]; 34 | 35 | expect(result).toStrictEqual(expected); 36 | }); 37 | }); 38 | 39 | describe('sortNumbersDescending', () => { 40 | it('should return a sorted array of descending numbers', () => { 41 | const numbers = [1337, 42, 5701]; 42 | 43 | const result = sortNumbersDescending(numbers); 44 | const expected = [5701, 1337, 42]; 45 | 46 | expect(result).toStrictEqual(expected); 47 | }); 48 | }); 49 | 50 | describe('sortOptionalNumbers', () => { 51 | it('should return a sorted array of optional numbers', () => { 52 | const optionalNumbers = [option.some(1337), option.none, option.some(42)]; 53 | 54 | const result = sortOptionalNumbers(optionalNumbers); 55 | const expected = [option.none, option.some(42), option.some(1337)]; 56 | 57 | expect(result).toStrictEqual(expected); 58 | }); 59 | }); 60 | 61 | describe('sortPersonsByName', () => { 62 | it('should return an array of persons alphabetically sorted by their name', () => { 63 | const alice = { name: 'Alice', age: option.none }; 64 | const bob = { name: 'Bob', age: option.none }; 65 | const crystal = { name: 'Crystal', age: option.none }; 66 | 67 | const persons = [crystal, alice, bob]; 68 | 69 | const result = sortPersonsByName(persons); 70 | const expected = [alice, bob, crystal]; 71 | 72 | expect(result).toStrictEqual(expected); 73 | }); 74 | }); 75 | 76 | describe('sortPersonsByName', () => { 77 | it('should return an array of persons sorted by their age', () => { 78 | const alice = { name: 'Alice', age: option.some(42) }; 79 | const bob = { name: 'Bob', age: option.none }; 80 | const crystal = { name: 'Crystal', age: option.some(29) }; 81 | 82 | const persons = [crystal, alice, bob]; 83 | 84 | const result = sortPersonsByAge(persons); 85 | const expected = [bob, crystal, alice]; 86 | 87 | expect(result).toStrictEqual(expected); 88 | }); 89 | }); 90 | 91 | describe('sortPersonsByName', () => { 92 | it('should return an array of persons sorted first by age and then by name', () => { 93 | const alice = { name: 'Alice', age: option.some(42) }; 94 | const bob = { name: 'Bob', age: option.none }; 95 | const crystal = { name: 'Crystal', age: option.some(29) }; 96 | const dorian = { name: 'Dorian', age: option.some(29) }; 97 | const edgar = { name: 'Edgar', age: option.none }; 98 | 99 | const persons = [dorian, alice, edgar, bob, crystal]; 100 | 101 | const result = sortPersonsByAgeThenByName(persons); 102 | const expected = [bob, edgar, crystal, dorian, alice]; 103 | 104 | expect(result).toStrictEqual(expected); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/exo5 - Nested data with traverse/exo5.test.ts: -------------------------------------------------------------------------------- 1 | import { option } from 'fp-ts'; 2 | import * as exercise from './exo5.exercise'; 3 | import * as solution from './exo5.solution'; 4 | import { isTestingSolution } from '../testUtils'; 5 | 6 | const { 7 | getCountryCurrencyOfOptionalCountryCode, 8 | getValidCountryCodeOfCountryNames, 9 | giveCurrencyOfCountryToUser, 10 | performAsyncComputationInParallel, 11 | performAsyncComputationInSequence, 12 | sequenceOptionArray, 13 | sequenceOptionTask, 14 | } = isTestingSolution() ? solution : exercise; 15 | 16 | describe('exo5', () => { 17 | describe('getCountryCurrencyOfOptionalCountryCode', () => { 18 | it('should return a Task if given a None', async () => { 19 | const result = await getCountryCurrencyOfOptionalCountryCode( 20 | option.none, 21 | )(); 22 | 23 | expect(result).toStrictEqual(option.none); 24 | }); 25 | 26 | it('should return a Task` is also necessarily a `Magma`. 118 | // The `string` module may contain what you need ;) 119 | // 120 | // Bonus point: 121 | // Did you find something in the `Semigroup` module that may have been 122 | // helpful in defining `mapWithLastEntry`? 123 | 124 | export const mapWithConcatenatedEntries: ReadonlyMap = 125 | unimplemented(); 126 | 127 | /////////////////////////////////////////////////////////////////////////////// 128 | // DIFFERENCE / UNION / INTERSECTION // 129 | /////////////////////////////////////////////////////////////////////////////// 130 | 131 | export const primes = new Set([2, 3, 5, 7]); 132 | export const odds = new Set([1, 3, 5, 7, 9]); 133 | 134 | // Construct the set `nonPrimeOdds` from the two sets defined above. It should 135 | // only include the odd numbers that are not prime. 136 | // 137 | // HINT: 138 | // - Be mindful of the order of operands for the operator you will choose. 139 | 140 | export const nonPrimeOdds: ReadonlySet = unimplemented(); 141 | 142 | // Construct the set `primeOdds` from the two sets defined above. It should 143 | // only include the odd numbers that are also prime. 144 | 145 | export const primeOdds: ReadonlySet = unimplemented(); 146 | 147 | /////////////////////////////////////////////////////////////////////////////// 148 | 149 | export type Analytics = { 150 | page: string; 151 | views: number; 152 | }; 153 | 154 | // These example Maps are voluntarily written in a non fp-ts way to not give 155 | // away too much obviously ;) 156 | // 157 | // As an exercise for the reader, they may rewrite those with what they've 158 | // learned earlier. 159 | 160 | export const pageViewsA = new Map( 161 | [ 162 | { page: 'home', views: 5 }, 163 | { page: 'about', views: 2 }, 164 | { page: 'blog', views: 7 }, 165 | ].map(entry => [entry.page, entry]), 166 | ); 167 | 168 | export const pageViewsB = new Map( 169 | [ 170 | { page: 'home', views: 10 }, 171 | { page: 'blog', views: 35 }, 172 | { page: 'faq', views: 5 }, 173 | ].map(entry => [entry.page, entry]), 174 | ); 175 | 176 | // Construct the `Map` with the total page views for all the pages in both sources 177 | // of analytics `pageViewsA` and `pageViewsB`. 178 | // 179 | // In case a page appears in both sources, their view count should be summed. 180 | 181 | export const allPageViews: ReadonlyMap = unimplemented(); 182 | 183 | // Construct the `Map` with the total page views but only for the pages that 184 | // appear in both sources of analytics `pageViewsA` and `pageViewsB`. 185 | 186 | export const intersectionPageViews: ReadonlyMap = 187 | unimplemented(); 188 | -------------------------------------------------------------------------------- /src/exo2 - Combinators/exo2.solution.ts: -------------------------------------------------------------------------------- 1 | // `fp-ts` training Exercise 2 2 | // Let's have fun with combinators! 3 | 4 | import { Option } from 'fp-ts/Option'; 5 | import { Failure } from '../Failure'; 6 | import { either, option, readonlyArray } from 'fp-ts'; 7 | import { flow, pipe } from 'fp-ts/lib/function'; 8 | 9 | /////////////////////////////////////////////////////////////////////////////// 10 | // SETUP // 11 | /////////////////////////////////////////////////////////////////////////////// 12 | 13 | // We are developing a small game, and the player can control either one of 14 | // three types of characters, mainly differentiated by the type of damage they 15 | // can put out. 16 | 17 | // Our main `Character` type is a simple union of all the concrete character 18 | // types. 19 | export type Character = Warrior | Wizard | Archer; 20 | 21 | // We have three types of `Damage`, each corresponding to a character type. 22 | export enum Damage { 23 | Physical = 'Physical damage', 24 | Magical = 'Magical damage', 25 | Ranged = 'Ranged damage', 26 | } 27 | 28 | // A `Warrior` only can output physical damage. 29 | export class Warrior { 30 | smash() { 31 | return Damage.Physical; 32 | } 33 | 34 | toString() { 35 | return 'Warrior'; 36 | } 37 | } 38 | 39 | // A `Wizard` only can output magical damage. 40 | export class Wizard { 41 | burn() { 42 | return Damage.Magical; 43 | } 44 | 45 | toString() { 46 | return 'Wizard'; 47 | } 48 | } 49 | 50 | // An `Archer` only can output ranged damage. 51 | export class Archer { 52 | shoot() { 53 | return Damage.Ranged; 54 | } 55 | 56 | toString() { 57 | return 'Archer'; 58 | } 59 | } 60 | 61 | // We also have convenient type guards to help us differentiate between 62 | // character types when given a `Character`. 63 | 64 | export const isWarrior = (character: Character): character is Warrior => { 65 | return (character as Warrior).smash !== undefined; 66 | }; 67 | 68 | export const isWizard = (character: Character): character is Wizard => { 69 | return (character as Wizard).burn !== undefined; 70 | }; 71 | 72 | export const isArcher = (character: Character): character is Archer => { 73 | return (character as Archer).shoot !== undefined; 74 | }; 75 | 76 | // Finally, we have convenient and expressive error types, defining what can 77 | // go wrong in our game: 78 | // - the player can try to perform an action without first choosing a character as the attacker 79 | // - the player can try to perform the wrong action for a character class 80 | 81 | export enum Exo2FailureType { 82 | NoAttacker = 'Exo2FailureType_NoAttacker', 83 | InvalidAttacker = 'Exo2FailureType_InvalidAttacker', 84 | } 85 | 86 | export type NoAttackerFailure = Failure; 87 | export const noAttackerFailure = Failure.builder(Exo2FailureType.NoAttacker); 88 | 89 | export type InvalidAttackerFailure = Failure; 90 | export const invalidAttackerFailure = Failure.builder( 91 | Exo2FailureType.InvalidAttacker, 92 | ); 93 | 94 | /////////////////////////////////////////////////////////////////////////////// 95 | // EITHER // 96 | /////////////////////////////////////////////////////////////////////////////// 97 | 98 | // The next three functions take the character currently selected by the player as the attacker 99 | // and return the expected damage type if appropriate. 100 | // 101 | // If no attacker is selected, it should return 102 | // `either.left(noAttackerFailure('No attacker currently selected'))` 103 | // 104 | // If an attacker of the wrong type is selected, it should return 105 | // `either.left(invalidAttackerFailure(' cannot perform '))` 106 | // 107 | // Otherwise, it should return `either.right()` 108 | // 109 | // HINT: These functions represent the public API. But it is heavily 110 | // recommended to break those down into smaller private functions that can be 111 | // reused instead of doing one big `pipe` for each. 112 | // 113 | // HINT: `Either` has a special constructor `fromPredicate` that can accept 114 | // a type guard such as `isWarrior` to help with type inference. 115 | // 116 | // HINT: Sequentially check for various possible errors is one of the most 117 | // common operations done with the `Either` type and it is available through 118 | // the `flatMap` operator. 119 | 120 | const checkSelected = either.fromOption(() => 121 | noAttackerFailure('No attacker currently selected'), 122 | ); 123 | 124 | const checkWarrior = either.fromPredicate(isWarrior, character => 125 | invalidAttackerFailure(`${character.toString()} cannot perform smash`), 126 | ); 127 | 128 | const checkWizard = either.fromPredicate(isWizard, character => 129 | invalidAttackerFailure(`${character.toString()} cannot perform burn`), 130 | ); 131 | 132 | const checkArcher = either.fromPredicate(isArcher, character => 133 | invalidAttackerFailure(`${character.toString()} cannot perform shoot`), 134 | ); 135 | 136 | const smash = flow( 137 | checkWarrior, 138 | either.map(warrior => warrior.smash()), 139 | ); 140 | 141 | const burn = flow( 142 | checkWizard, 143 | either.map(wizard => wizard.burn()), 144 | ); 145 | 146 | const shoot = flow( 147 | checkArcher, 148 | either.map(archer => archer.shoot()), 149 | ); 150 | 151 | export const checkAttackerAndSmash = (attacker: Option) => 152 | pipe(attacker, checkSelected, either.flatMap(smash)); 153 | 154 | export const checkAttackerAndBurn = (attacker: Option) => 155 | pipe(attacker, checkSelected, either.flatMap(burn)); 156 | 157 | export const checkAttackerAndShoot = (attacker: Option) => 158 | pipe(attacker, checkSelected, either.flatMap(shoot)); 159 | 160 | /////////////////////////////////////////////////////////////////////////////// 161 | // OPTION // 162 | /////////////////////////////////////////////////////////////////////////////// 163 | 164 | // The next three functions take a `Character` and optionally return the 165 | // expected damage type if the attacker matches the expected character type. 166 | // 167 | // HINT: These functions represent the public API. But it is heavily 168 | // recommended to break those down into smaller private functions that can be 169 | // reused instead of doing one big `pipe` for each. 170 | // 171 | // HINT: `Option` has a special constructor `fromEither` that discards the 172 | // error type. 173 | // 174 | // BONUS POINTS: If you properly defined small private helpers in the previous 175 | // section, they should be easily reused for those use-cases. 176 | 177 | export const smashOption = flow(smash, option.fromEither); 178 | 179 | export const burnOption = flow(burn, option.fromEither); 180 | 181 | export const shootOption = flow(shoot, option.fromEither); 182 | 183 | /////////////////////////////////////////////////////////////////////////////// 184 | // ARRAY // 185 | /////////////////////////////////////////////////////////////////////////////// 186 | 187 | // We now want to aggregate all the attacks of a selection of arbitrarily many 188 | // attackers and know how many are Physical, Magical or Ranged. 189 | // 190 | // HINT: You should be able to reuse the attackOption variants defined earlier 191 | // 192 | // HINT: `ReadonlyArray` from `fp-ts` has a neat `filterMap` function that 193 | // perform mapping and filtering at the same time by applying a function 194 | // of type `A => Option` over the collection. 195 | 196 | export interface TotalDamage { 197 | [Damage.Physical]: number; 198 | [Damage.Magical]: number; 199 | [Damage.Ranged]: number; 200 | } 201 | 202 | export const attack = (army: ReadonlyArray) => ({ 203 | [Damage.Physical]: pipe( 204 | army, 205 | readonlyArray.filterMap(smashOption), 206 | readonlyArray.size, 207 | ), 208 | [Damage.Magical]: pipe( 209 | army, 210 | readonlyArray.filterMap(burnOption), 211 | readonlyArray.size, 212 | ), 213 | [Damage.Ranged]: pipe( 214 | army, 215 | readonlyArray.filterMap(shootOption), 216 | readonlyArray.size, 217 | ), 218 | }); 219 | -------------------------------------------------------------------------------- /src/exo7 - Collections/exo7.solution.ts: -------------------------------------------------------------------------------- 1 | // `fp-ts` training Exercise 7 2 | // Manipulate collections with type-classes 3 | 4 | import { 5 | number, 6 | readonlyArray, 7 | readonlyMap, 8 | readonlySet, 9 | semigroup, 10 | string, 11 | } from 'fp-ts'; 12 | import { pipe } from 'fp-ts/function'; 13 | 14 | // In this exercise, we will learn how to manipulate essential collections 15 | // such as `Set` and `Map`. 16 | // 17 | // These collections have very important properties that make them slightly 18 | // more complex than the venerable `Array`: 19 | // - `Set` requires each element to be unique 20 | // - `Map` associates unique keys to values 21 | // 22 | // In fact, it can sometimes be helpful to think of `Set` as a special case 23 | // of `Map` where `Set` is strictly equivalent to `Map`. 24 | // 25 | // To manipulate these collections, we often need to inform `fp-ts` on 26 | // how to uphold the properties outlined above (e.g. how to determine whether 27 | // two elements or keys have the same value, how to combine values together 28 | // in case of key collision or how to order the values when converting back 29 | // to an array). 30 | // 31 | // And the way to describe those properties for the specific inner types of a 32 | // given `Set` or `Map` is... TYPECLASSES! 33 | 34 | /////////////////////////////////////////////////////////////////////////////// 35 | // SET // 36 | /////////////////////////////////////////////////////////////////////////////// 37 | 38 | // A `Set` is pretty straightforward, it stores values but doesn't care at all 39 | // about the ordering of those values. Furthermore, it ignores duplicates. 40 | 41 | export const numberArray: ReadonlyArray = [7, 42, 1337, 1, 0, 1337, 42]; 42 | 43 | // Construct `numberSet` from the provided `numberArray`. 44 | // You need to use the `ReadonlySet` module from `fp-ts` instead of the 45 | // JavaScript standard constructor. 46 | // 47 | // HINTS: 48 | // - You can look into `readonlySet.fromReadonlyArray` 49 | // - `fp-ts` doesn't know how you want to define equality for the inner type 50 | // and requires you to provide an `Eq` instance 51 | 52 | export const numberSet: ReadonlySet = pipe( 53 | numberArray, 54 | readonlySet.fromReadonlyArray(number.Eq), 55 | ); 56 | 57 | // Convert `numberSet` back to an array in `numberArrayFromSet`. 58 | // You need to use the `ReadonlySet` module from `fp-ts` instead of the 59 | // JavaScript standard constructor. 60 | // 61 | // HINTS: 62 | // - You can look into `readonlySet.toReadonlyArray` 63 | // - The elements in `numberSet` have no guarantees whatsoever regarding 64 | // their ordering. This ordering could be totally random. But remember that 65 | // functional programming is all about purity. Converting a set to an array 66 | // is a pure operation and as such, should return the same value for the 67 | // same input. This means you **need** to instruct `fp-ts` on how you wish 68 | // the values to be ordered in the output array, by providing an `Ord` 69 | // instance. 70 | 71 | export const numberArrayFromSet: ReadonlyArray = pipe( 72 | numberSet, 73 | readonlySet.toReadonlyArray(number.Ord), 74 | ); 75 | 76 | /////////////////////////////////////////////////////////////////////////////// 77 | // MAP // 78 | /////////////////////////////////////////////////////////////////////////////// 79 | 80 | // A `Map` associates a set of unique keys to arbitrary values. Values 81 | // themselves can be duplicated across various keys but keys have to be unique. 82 | // 83 | // This means than when constructing a `Map` from an `Array`, you need to be 84 | // explicit on how you wish to combine values in case of key collision (maybe 85 | // you want to only insert the last value provided, maybe the first, maybe you 86 | // want to combine both values in a specific way eg. concatenate strings, add 87 | // numbers, etc...) 88 | 89 | export const associativeArray: ReadonlyArray<[number, string]> = [ 90 | [1, 'Alice'], 91 | [2, 'Bob'], 92 | [3, 'Clara'], 93 | [4, 'Denise'], 94 | [2, 'Robert'], 95 | ]; 96 | 97 | // Construct `mapWithLastEntry` from the provided `associativeArray`. 98 | // You need to use the `ReadonlyMap` module from `fp-ts` instead of the 99 | // JavaScript standard constructor. 100 | // 101 | // The resulting `Map` should have the following shape: 102 | // 1 => 'Alice' 103 | // 2 => 'Robert' 104 | // 3 => 'Clara' 105 | // 4 => 'Denise' 106 | // 107 | // HINTS: 108 | // - You can look into `readonlyMap.fromFoldable` 109 | // - You need to provide an `Eq` instance for the key type 110 | // - You need to provide a `Magma` instance for the value type. In this case, 111 | // the `Magma` instance should ignore the first value and return the second. 112 | // (You can define your own, or look into the `Magma` or `Semigroup` module) 113 | // - You need to provide the `Foldable` instance for the input container type. 114 | // Just know that you can construct a `Map` from other types than `Array` as 115 | // long as they implement `Foldable`. Here, you can simply pass the standard 116 | // `readonlyArray.Foldable` instance. 117 | 118 | export const mapWithLastEntry: ReadonlyMap = pipe( 119 | associativeArray, 120 | readonlyMap.fromFoldable(number.Eq, semigroup.last(), readonlyArray.Foldable), 121 | ); 122 | 123 | // Same thing as above, except that upon key collision we don't want to simply 124 | // select the newest entry value but append it to the previous one. 125 | // 126 | // Basically, the resulting `Map` here should have the following shape: 127 | // 1 => 'Alice' 128 | // 2 => 'BobRobert' 129 | // 3 => 'Clara' 130 | // 4 => 'Denise' 131 | // 132 | // HINT: 133 | // - You can look into the `Semigroup` typeclass as it is a super-class of 134 | // `Magma`, meaning that a `Semigroup` is also necessarily a `Magma`. 135 | // The `string` module may contain what you need ;) 136 | // 137 | // Bonus point: 138 | // Did you find something in the `Semigroup` module that may have been 139 | // helpful in defining `mapWithLastEntry`? 140 | 141 | export const mapWithConcatenatedEntries: ReadonlyMap = pipe( 142 | associativeArray, 143 | readonlyMap.fromFoldable(number.Eq, string.Semigroup, readonlyArray.Foldable), 144 | ); 145 | 146 | /////////////////////////////////////////////////////////////////////////////// 147 | // DIFFERENCE / UNION / INTERSECTION // 148 | /////////////////////////////////////////////////////////////////////////////// 149 | 150 | export const primes = new Set([2, 3, 5, 7]); 151 | export const odds = new Set([1, 3, 5, 7, 9]); 152 | 153 | // Construct the set `nonPrimeOdds` from the two sets defined above. It should 154 | // only include the odd numbers that are not prime. 155 | // 156 | // HINT: 157 | // - Be mindful of the order of operands for the operator you will choose. 158 | 159 | export const nonPrimeOdds: ReadonlySet = readonlySet.difference( 160 | number.Eq, 161 | )(odds, primes); 162 | 163 | // Construct the set `primeOdds` from the two sets defined above. It should 164 | // only include the odd numbers that are also prime. 165 | 166 | export const primeOdds: ReadonlySet = readonlySet.intersection( 167 | number.Eq, 168 | )(odds, primes); 169 | 170 | /////////////////////////////////////////////////////////////////////////////// 171 | 172 | export type Analytics = { 173 | page: string; 174 | views: number; 175 | }; 176 | 177 | // These example Maps are voluntarily written in a non fp-ts way to not give 178 | // away too much obviously ;) 179 | // 180 | // As an exercise for the reader, they may rewrite those with what they've 181 | // learned earlier. 182 | 183 | export const pageViewsA = new Map( 184 | [ 185 | { page: 'home', views: 5 }, 186 | { page: 'about', views: 2 }, 187 | { page: 'blog', views: 7 }, 188 | ].map(entry => [entry.page, entry]), 189 | ); 190 | 191 | export const pageViewsB = new Map( 192 | [ 193 | { page: 'home', views: 10 }, 194 | { page: 'blog', views: 35 }, 195 | { page: 'faq', views: 5 }, 196 | ].map(entry => [entry.page, entry]), 197 | ); 198 | 199 | // Construct the `Map` with the total page views for all the pages in both sources 200 | // of analytics `pageViewsA` and `pageViewsB`. 201 | // 202 | // In case a page appears in both sources, their view count should be summed. 203 | 204 | const S = semigroup.struct({ 205 | page: semigroup.first(), 206 | views: number.SemigroupSum, 207 | }); 208 | 209 | export const allPageViews: ReadonlyMap = pipe( 210 | pageViewsA, 211 | readonlyMap.union(string.Eq, S)(pageViewsB), 212 | ); 213 | 214 | // Construct the `Map` with the total page views but only for the pages that 215 | // appear in both sources of analytics `pageViewsA` and `pageViewsB`. 216 | 217 | export const intersectionPageViews: ReadonlyMap = pipe( 218 | pageViewsA, 219 | readonlyMap.intersection(string.Eq, S)(pageViewsB), 220 | ); 221 | -------------------------------------------------------------------------------- /src/exo5 - Nested data with traverse/exo5.exercise.ts: -------------------------------------------------------------------------------- 1 | // `fp-ts` training Exercise 5 2 | // Managing nested effectful data with `traverse` 3 | 4 | import { option, readonlyRecord, task } from 'fp-ts'; 5 | import { pipe } from 'fp-ts/lib/function'; 6 | import { Option } from 'fp-ts/lib/Option'; 7 | import { ReadonlyRecord } from 'fp-ts/lib/ReadonlyRecord'; 8 | import { Task } from 'fp-ts/lib/Task'; 9 | import { sleep, unimplemented, unimplementedAsync } from '../utils'; 10 | 11 | // When using many different Functors in a complex application, we can easily 12 | // get to a point when we have many nested types that we would like to 'merge', 13 | // like `Task>>` or `Either>>` 14 | // It would be nice to have a way to 'move up' the similar types in order to 15 | // merge them, like merging the `Task` to have a `Task>` or the 16 | // `Either` to have a `Either>` 17 | // 18 | // That's precisely the concept of `traverse`. It will allow us to transform 19 | // a `Option>` to a `Task>` so we can flatMap it with another 20 | // `Task` for example, or to transform a `ReadonlyArray>` to a 21 | // `Either>` 22 | 23 | /////////////////////////////////////////////////////////////////////////////// 24 | // SETUP // 25 | /////////////////////////////////////////////////////////////////////////////// 26 | 27 | // Let's consider a small range of countries (here, France, Spain and the USA) 28 | // with a mapping from their name to their code: 29 | type CountryCode = 'FR' | 'SP' | 'US'; 30 | export const countryNameToCountryCode: ReadonlyRecord = { 31 | France: 'FR', 32 | Spain: 'SP', 33 | USA: 'US', 34 | }; 35 | 36 | // Let's simulate the call to an api which would return the currency when 37 | // providing a country code. For the sake of simplicity, let's consider that it 38 | // cannot fail. 39 | type Currency = 'EUR' | 'DOLLAR'; 40 | export const getCountryCurrency: (countryCode: CountryCode) => Task = 41 | (countryCode: CountryCode): Task => 42 | async () => { 43 | if (countryCode === 'US') { 44 | return 'DOLLAR'; 45 | } 46 | return 'EUR'; 47 | }; 48 | 49 | // Let's simulate a way for the user to provide a country name. 50 | // Let's consider that it cannot fail and let's add the possibility to set 51 | // the user's response as a parameter for easier testing. 52 | export const getCountryNameFromUser: (countryName: string) => Task = ( 53 | countryName: string, 54 | ) => task.of(countryName); 55 | 56 | // Here's a function to retrieve the countryCode from a country name if it is 57 | // matching a country we support. This method returns an `Option` as we cannot 58 | // return anything if the given string is not matching a country name we know 59 | export const getCountryCode: (countryName: string) => Option = ( 60 | countryName: string, 61 | ) => readonlyRecord.lookup(countryName)(countryNameToCountryCode); 62 | 63 | /////////////////////////////////////////////////////////////////////////////// 64 | // TRAVERSING OPTIONS // 65 | /////////////////////////////////////////////////////////////////////////////// 66 | 67 | // With all these functions, we can simulate a program that would ask for a 68 | // country name and return its currency if it knows the country. 69 | // A naive implementation would be mapping on each `Task` and `Option` to call 70 | // the correct method: 71 | export const naiveGiveCurrencyOfCountryToUser = ( 72 | countryNameFromUserMock: string, 73 | ) => 74 | pipe( 75 | getCountryNameFromUser(countryNameFromUserMock), 76 | task.map(getCountryCode), 77 | task.map(option.map(getCountryCurrency)), 78 | ); 79 | 80 | // The result type of this method is: `Task>>` 81 | // Not ideal, right? We would need to await the first `Task`, then check if it's 82 | // `Some` to get the `Task` inside and finally await the `Task` to retrieve the 83 | // currency. 84 | // Let's do better than that! 85 | 86 | // First we need a way to transform our `Option>` to 87 | // `Task>` 88 | // That's precisely what traverse is about. 89 | // Use `option.traverse` to implement `getCountryCurrencyOfOptionalCountryCode` 90 | // below. This function takes an `Option`, should apply 91 | // `getCountryCurrency` to the `CountryCode` and make it so that the result 92 | // is `Task>` 93 | // 94 | // HINT: `option.traverse` asks for an Applicative as the first parameter. You 95 | // can find it for `Task` in `task.ApplicativePar` 96 | 97 | export const getCountryCurrencyOfOptionalCountryCode: ( 98 | optionalCountryCode: Option, 99 | ) => Task> = unimplementedAsync; 100 | 101 | // Let's now use this function in our naive implementation's pipe to see how it 102 | // improves it. 103 | // Implement `giveCurrencyOfCountryToUser` below so that it returns a 104 | // `Task>` 105 | // 106 | // HINT: You should be able to copy the pipe from naiveGiveCurrencyOfCountryToUser 107 | // and make only few updates of it. The `task.flatMap` helper may be useful. 108 | 109 | export const giveCurrencyOfCountryToUser: ( 110 | countryNameFromUserMock: string, 111 | ) => Task> = unimplementedAsync; 112 | 113 | // BONUS: We don't necessarily need `traverse` to do this. Try implementing 114 | // `giveCurrencyOfCountryToUser` by lifting some of the functions' results to 115 | // `TaskOption` 116 | 117 | /////////////////////////////////////////////////////////////////////////////// 118 | // TRAVERSING ARRAYS // 119 | /////////////////////////////////////////////////////////////////////////////// 120 | 121 | // Let's say we want to ask the user to provide multiple countries. We'll have an array 122 | // of country names as `string` and we want to retrieve the country code of each. 123 | // Looks pretty easy: 124 | export const getCountryCodeOfCountryNames = ( 125 | countryNames: ReadonlyArray, 126 | ) => countryNames.map(getCountryCode); 127 | 128 | // As expected, we end up with a `ReadonlyArray>`. We know for 129 | // each item of the array if we have been able to find the corresponding country 130 | // code or not. 131 | // While this can be useful, you need to handle the option anytime you want to 132 | // perform any operation on each country code (let's say you want to get the 133 | // currency of each) 134 | // It would be easier to 'merge' all the options into one and have a `Some` only if 135 | // all the country codes are `Some` and a `None` if at least one is `None`. 136 | // Doing this allows you to stop the process if you have a `None` to tell the user 137 | // that some countries are not valid or move on with a `ReadonlyArray>` 138 | // if all are valid. 139 | // Type-wise, it means going from `ReadonlyArray>` to 140 | // `Option>` 141 | // This is what traversing array is about. 142 | 143 | // Let's write a method that gets the country code for each element of an array 144 | // of country names and returns an option of an array of country codes. 145 | // 146 | // HINT: while `readonlyArray.traverse` exists, you have a shortcut in the `option` 147 | // module: `option.traverseArray` 148 | 149 | export const getValidCountryCodeOfCountryNames: ( 150 | countryNames: ReadonlyArray, 151 | ) => Option> = unimplemented; 152 | 153 | /////////////////////////////////////////////////////////////////////////////// 154 | // TRAVERSING ARRAYS ASYNCHRONOUSLY // 155 | /////////////////////////////////////////////////////////////////////////////// 156 | 157 | // We've seen how to traverse an `array` of `option`s but this is not something 158 | // specific to `option`. We can traverse an `array` of any applicative functor, 159 | // like `either` or `task` for example. 160 | // When dealing with functors that perform asynchronous side effects, like 161 | //`task`, comes the question of parallelization. Do we want to run the 162 | // computation on each item of the array in parallel or one after the other? 163 | // Both are equally feasible with fp-ts, let's discover it! 164 | 165 | // Let's simulate a method that reads a number in a database, does some async 166 | // computation with it, replaces this number in the database by the result of 167 | // the computation and returns it 168 | const createSimulatedAsyncMethod = (): ((toAdd: number) => Task) => { 169 | let number = 0; 170 | 171 | return (toAdd: number) => async () => { 172 | const currentValue = number; 173 | await sleep(100); 174 | number = currentValue + toAdd; 175 | return number; 176 | }; 177 | }; 178 | 179 | // Write a method to traverse an array by running the method 180 | // `simulatedAsyncMethodForParallel: (toAdd: number) => Task` 181 | // defined below on each item in parallel. 182 | // 183 | // HINT: as was the case for `option`, you have a few helpers in the `task` 184 | // module to traverse arrays 185 | 186 | export const simulatedAsyncMethodForParallel = createSimulatedAsyncMethod(); 187 | export const performAsyncComputationInParallel: ( 188 | numbers: ReadonlyArray, 189 | ) => Task> = unimplementedAsync; 190 | 191 | // Write a method to traverse an array by running the method 192 | // `simulatedAsyncMethodForSequence: (toAdd: number) => Task` 193 | // defined below on each item in sequence. 194 | // 195 | // HINT: as was the case for `option`, you have a few helpers in the `task` 196 | // module to traverse arrays 197 | 198 | export const simulatedAsyncMethodForSequence = createSimulatedAsyncMethod(); 199 | export const performAsyncComputationInSequence: ( 200 | numbers: ReadonlyArray, 201 | ) => Task> = unimplementedAsync; 202 | 203 | /////////////////////////////////////////////////////////////////////////////// 204 | // SEQUENCE // 205 | /////////////////////////////////////////////////////////////////////////////// 206 | 207 | // `traverse` is nice when you need to get the value inside a container (let's 208 | // say `Option`), apply a method to it that return another container type (let's 209 | // say `Task`) and 'invert' the container (to get a `Task