🎉 Initial commit
This commit is contained in:
commit
63f1f160ab
13 changed files with 4323 additions and 0 deletions
39
.eslintrc.js
Normal file
39
.eslintrc.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:fp/recommended',
|
||||||
|
],
|
||||||
|
plugins: ['fp', 'inato'],
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars-experimental': ['error'],
|
||||||
|
'no-useless-constructor': 'off',
|
||||||
|
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||||
|
'@typescript-eslint/camelcase': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
properties: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react-hooks/rules-of-hooks': 'off',
|
||||||
|
'import/extensions': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ts: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'fp/no-throw':'off',
|
||||||
|
'fp/no-class': 'off',
|
||||||
|
'fp/no-mutation': 'off',
|
||||||
|
'fp/no-nil': 'off',
|
||||||
|
'fp/no-this': 'off',
|
||||||
|
'fp/no-unused-expression': 'off',
|
||||||
|
'inato/no-factories-outside-of-tests': 'error',
|
||||||
|
},
|
||||||
|
};
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
.DS_Store
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
v12.18.0
|
4
.prettierrc
Normal file
4
.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
15
.vscode/settings.json
vendored
Normal file
15
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"search.exclude": {
|
||||||
|
"**/node_modules/": true
|
||||||
|
},
|
||||||
|
"eslint.packageManager": "yarn",
|
||||||
|
"eslint.alwaysShowStatus": true,
|
||||||
|
"javascript.validate.enable": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"prettier.configPath": ".prettierrc",
|
||||||
|
"eslint.format.enable": true
|
||||||
|
}
|
5
jest.config.js
Normal file
5
jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
|
||||||
|
};
|
30
package.json
Normal file
30
package.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "fp-ts-training",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Learning fp-ts while having fun",
|
||||||
|
"author": "Hugo Saracino <hugo@inato.com>",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/inato/fp-ts-training.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/inato/fp-ts-training",
|
||||||
|
"scripts": {
|
||||||
|
"build": "yarn tsc -b",
|
||||||
|
"test": "jest --runInBand"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fp-ts": "^2.6.7",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/jest": "^26.0.3",
|
||||||
|
"@types/node": "^14.0.14",
|
||||||
|
"eslint": "^7.3.1",
|
||||||
|
"eslint-plugin-fp": "^2.3.0",
|
||||||
|
"jest": "^26.1.0",
|
||||||
|
"ts-jest": "^26.1.1",
|
||||||
|
"typescript": "^3.9.6"
|
||||||
|
}
|
||||||
|
}
|
71
src/exo1/exo1.test.ts
Normal file
71
src/exo1/exo1.test.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as Either from 'fp-ts/lib/Either';
|
||||||
|
import * as Option from 'fp-ts/lib/Option';
|
||||||
|
|
||||||
|
import {
|
||||||
|
divide,
|
||||||
|
DivisionByZero,
|
||||||
|
safeDivide,
|
||||||
|
safeDivideWithError,
|
||||||
|
asyncDivide,
|
||||||
|
asyncSafeDivideWithError,
|
||||||
|
} from './exo1';
|
||||||
|
|
||||||
|
describe('exo1', () => {
|
||||||
|
describe('divide', () => {
|
||||||
|
it('should return the result of dividing two numbers', () => {
|
||||||
|
expect(divide(25, 5)).toEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Infinity if the denominator is zero', () => {
|
||||||
|
expect(divide(25, 0)).toBe(Infinity);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('safeDivide', () => {
|
||||||
|
it('should return the result of dividing two numbers', () => {
|
||||||
|
expect(safeDivide(25, 5)).toStrictEqual(Option.some(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Option.none if the denominator is zero', () => {
|
||||||
|
expect(safeDivide(25, 0)).toStrictEqual(Option.none);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('safeDivideWithError', () => {
|
||||||
|
it('should return the result of dividing two numbers', () => {
|
||||||
|
expect(safeDivideWithError(25, 5)).toStrictEqual(Either.right(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Either.left(DivisionByZero) if the denominator is zero', () => {
|
||||||
|
expect(safeDivideWithError(25, 0)).toStrictEqual(
|
||||||
|
Either.left(DivisionByZero),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('asyncDivide', () => {
|
||||||
|
it('should eventually return the result of dividing two numbers', async () => {
|
||||||
|
const result = await asyncDivide(25, 5);
|
||||||
|
|
||||||
|
expect(result).toEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should eventually return Infinity if the denominator is zero', async () => {
|
||||||
|
await expect(asyncDivide(25, 0)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('asyncSafeDivideWithError', () => {
|
||||||
|
it('should eventually return the result of dividing two numbers', async () => {
|
||||||
|
const result = await asyncSafeDivideWithError(25, 5)();
|
||||||
|
|
||||||
|
expect(result).toStrictEqual(Either.right(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should eventually return Either.left(DivisionByZero) if the denominator is zero', async () => {
|
||||||
|
const result = await asyncSafeDivideWithError(25, 0)();
|
||||||
|
|
||||||
|
expect(result).toStrictEqual(Either.left(DivisionByZero));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
83
src/exo1/exo1.ts
Normal file
83
src/exo1/exo1.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// `fp-ts` training Exercice 1
|
||||||
|
// Basic types:
|
||||||
|
// - Option
|
||||||
|
// - Either
|
||||||
|
// - TaskEither
|
||||||
|
|
||||||
|
import * as Option from 'fp-ts/lib/Option';
|
||||||
|
import * as Either from 'fp-ts/lib/Either';
|
||||||
|
import * as TaskEither from 'fp-ts/lib/TaskEither';
|
||||||
|
|
||||||
|
import { unimplemented, sleep, unimplementedAsync } from '../utils';
|
||||||
|
|
||||||
|
export const divide = (a: number, b: number): number => {
|
||||||
|
return a / b;
|
||||||
|
};
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// OPTION //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Write the safe version of `divide` whith signature:
|
||||||
|
// safeDivide : (a: number, b: number) => Option<number>
|
||||||
|
//
|
||||||
|
// HINT: Option has two basic contructors:
|
||||||
|
// - `Option.some(value)`
|
||||||
|
// - `Option.none`
|
||||||
|
|
||||||
|
export const safeDivide: (
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
) => Option.Option<number> = unimplemented;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// EITHER //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Write the safe version of `divide` whith signature:
|
||||||
|
// safeDivideWithError : (a: number, b: number) => Either<DivideByZeroError, number>
|
||||||
|
//
|
||||||
|
// BONUS POINT: Implement `safeDivideWithError` in terms of `safeDivide`.
|
||||||
|
//
|
||||||
|
// HINT : Either has two basic constructors:
|
||||||
|
// - `Either.left(leftValue)`
|
||||||
|
// - `Either.right(rightValue)`
|
||||||
|
// as well as "smarter" constructors like:
|
||||||
|
// - `Either.fromOption(() => leftValue)(option)`
|
||||||
|
|
||||||
|
// Here is an simple error type to help you:
|
||||||
|
export type DivisionByZeroError = 'Error: Division by zero';
|
||||||
|
export const DivisionByZero = 'Error: Division by zero' as const;
|
||||||
|
|
||||||
|
export const safeDivideWithError: (
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
) => Either.Either<DivisionByZeroError, number> = unimplemented;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// TASKEITHER //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Now let's say we have a (pretend) API call that will perform the division for us
|
||||||
|
// (throwing an error when the denominator is 0)
|
||||||
|
export const asyncDivide = async (a: number, b: number) => {
|
||||||
|
await sleep(1000);
|
||||||
|
|
||||||
|
if (b === 0) {
|
||||||
|
throw new Error('BOOM!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return a / b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write the safe version of `divide` whith signature:
|
||||||
|
// asyncSafeDivideWithError : (a: number, b: number) => TaskEither<DivideByZeroError, number>
|
||||||
|
//
|
||||||
|
// HINT: TaskEither has a special constructor to transform a Promise<T> into
|
||||||
|
// a TaskEither<Error, T>:
|
||||||
|
// - `TaskEither.tryCatch(onReject: err => leftValue, onResolve: val => rightValue)(promise)`
|
||||||
|
|
||||||
|
export const asyncSafeDivideWithError: (
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
) => TaskEither.TaskEither<DivisionByZeroError, number> = unimplementedAsync;
|
1
src/exo1/index.ts
Normal file
1
src/exo1/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './exo1';
|
6
src/utils.ts
Normal file
6
src/utils.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export const unimplemented = () => undefined as any;
|
||||||
|
export const unimplementedAsync = () => () => undefined as any;
|
||||||
|
|
||||||
|
export function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "es2019",
|
||||||
|
"lib": ["es2019", "es2020.string", "es2020.symbol.wellknown"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDirs": ["src"],
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"sourceRoot": "/",
|
||||||
|
"noEmitHelpers": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"typeRoots": ["node_modules/@types", "src/types"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
Reference in a new issue