do all tasks

This commit is contained in:
Dmitriy Pleshevskiy 2023-05-29 22:27:14 +03:00
parent f867fef92d
commit 250e2397f1
Signed by: pleshevskiy
GPG key ID: 79C4487B44403985
14 changed files with 7651 additions and 4833 deletions

3
.gitignore vendored
View file

@ -14,3 +14,6 @@ node_modules
dist dist
.DS_Store .DS_Store
# direnv
/.envrc
/.direnv/

61
flake.lock Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1685012353,
"narHash": "sha256-U3oOge4cHnav8OLGdRVhL45xoRj4Ppd+It6nPC9nNIU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "aeb75dba965e790de427b73315d5addf91a54955",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

21
flake.nix Normal file
View file

@ -0,0 +1,21 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nodejs-18_x
nodePackages.typescript-language-server # typescript
nodePackages.vscode-langservers-extracted # html, css, json, eslint
];
};
});
}

4476
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,7 @@
// - `flow(f) === f` // - `flow(f) === f`
// - `pipe(x, f) === f(x)` // - `pipe(x, f) === f(x)`
import { unimplemented } from '../utils'; import { flow, pipe } from "fp-ts/lib/function";
export const isEven = (value: number) => value % 2 === 0; export const isEven = (value: number) => value % 2 === 0;
@ -42,9 +42,10 @@ export const not = (value: boolean) => !value;
// `pipe` or `flow`), write the function `isOdd` that checks if a number is // `pipe` or `flow`), write the function `isOdd` that checks if a number is
// odd. // odd.
export const isOddP: (value: number) => boolean = unimplemented; export const isOddP: (value: number) => boolean = (value: number) =>
pipe(value, isEven, not);
export const isOddF: (value: number) => boolean = unimplemented; export const isOddF: (value: number) => boolean = flow(isEven, not);
// We will write a function that for any given number, computes the next // We will write a function that for any given number, computes the next
// one according to the following rules: // one according to the following rules:
@ -56,17 +57,27 @@ export const isOddF: (value: number) => boolean = unimplemented;
// Below is the functional equivalent of the control flow statement if-else. // Below is the functional equivalent of the control flow statement if-else.
export const ifThenElse = export const ifThenElse =
<A>(onTrue: () => A, onFalse: () => A) => <A>(onTrue: () => A, onFalse: () => A) => (condition: boolean) =>
(condition: boolean) =>
condition ? onTrue() : onFalse(); condition ? onTrue() : onFalse();
// Using `pipe` and `ifThenElse`, write the function that computes the next step in the Collatz // Using `pipe` and `ifThenElse`, write the function that computes the next step in the Collatz
// sequence. // sequence.
export const next: (value: number) => number = unimplemented; const divideByTwo = (a: number): number => a / 2;
const triple = (a: number): number => a * 3;
const addOne = (a: number): number => a + 1;
const tripleAndAddOne = flow(triple, addOne);
export const next: (value: number) => number = (value) =>
ifThenElse(
() => divideByTwo(value),
() => tripleAndAddOne(value),
)(isEven(value));
// Using only `flow` and `next`, write the function that for any given number // Using only `flow` and `next`, write the function that for any given number
// a_n from the Collatz sequence, returns the number a_n+3 (ie. the number // a_n from the Collatz sequence, returns the number a_n+3 (ie. the number
// three steps ahead in the sequence). // three steps ahead in the sequence).
export const next3: (value: number) => number = unimplemented; export const next3: (value: number) => number = flow(next, next, next);

View file

@ -4,10 +4,12 @@
// - Either // - Either
// - TaskEither // - TaskEither
import { Either } from 'fp-ts/Either'; import { Either } from "fp-ts/Either";
import { Option } from 'fp-ts/Option'; import { Option } from "fp-ts/Option";
import { TaskEither } from 'fp-ts/TaskEither'; import { TaskEither } from "fp-ts/TaskEither";
import { unimplemented, sleep, unimplementedAsync } from '../utils'; import { sleep } from "../utils";
import { either, option, taskEither } from "fp-ts";
import { flow, pipe } from "fp-ts/lib/function";
export const divide = (a: number, b: number): number => { export const divide = (a: number, b: number): number => {
return a / b; return a / b;
@ -24,8 +26,12 @@ export const divide = (a: number, b: number): number => {
// - `option.some(value)` // - `option.some(value)`
// - `option.none` // - `option.none`
export const safeDivide: (a: number, b: number) => Option<number> = export const safeDivide: (a: number, b: number) => Option<number> = (a, b) =>
unimplemented; pipe(
b,
option.fromPredicate((b) => b !== 0),
option.map((b) => divide(a, b)),
);
// You probably wrote `safeDivide` using `if` statements and it's perfectly valid! // You probably wrote `safeDivide` using `if` statements and it's perfectly valid!
// There are ways to not use `if` statements. // There are ways to not use `if` statements.
@ -52,13 +58,16 @@ export const safeDivide: (a: number, b: number) => Option<number> =
// - `either.fromOption(() => leftValue)(option)` // - `either.fromOption(() => leftValue)(option)`
// Here is an simple error type to help you: // Here is an simple error type to help you:
export type DivisionByZeroError = 'Error: Division by zero'; export type DivisionByZeroError = "Error: Division by zero";
export const DivisionByZero = 'Error: Division by zero' as const; export const DivisionByZero = "Error: Division by zero" as const;
export const safeDivideWithError: ( export const safeDivideWithError: (
a: number, a: number,
b: number, b: number,
) => Either<DivisionByZeroError, number> = unimplemented; ) => Either<DivisionByZeroError, number> = flow(
safeDivide,
either.fromOption(() => DivisionByZero),
);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// TASKEITHER // // TASKEITHER //
@ -70,7 +79,7 @@ export const asyncDivide = async (a: number, b: number) => {
await sleep(1000); await sleep(1000);
if (b === 0) { if (b === 0) {
throw new Error('BOOM!'); throw new Error("BOOM!");
} }
return a / b; return a / b;
@ -86,4 +95,5 @@ export const asyncDivide = async (a: number, b: number) => {
export const asyncSafeDivideWithError: ( export const asyncSafeDivideWithError: (
a: number, a: number,
b: number, b: number,
) => TaskEither<DivisionByZeroError, number> = unimplementedAsync; ) => TaskEither<DivisionByZeroError, number> = (a, b) =>
taskEither.tryCatch(() => asyncDivide(a, b), () => DivisionByZero);

View file

@ -1,10 +1,12 @@
// `fp-ts` training Exercise 2 // `fp-ts` training Exercise 2
// Let's have fun with combinators! // Let's have fun with combinators!
import { Either } from 'fp-ts/Either'; import { Either } from "fp-ts/Either";
import { Option } from 'fp-ts/Option'; import { Option } from "fp-ts/Option";
import { Failure } from '../Failure'; import { Failure } from "../Failure";
import { unimplemented } from '../utils'; import { either, option, readonlyArray } from "fp-ts";
import { absurd, flow } from "fp-ts/lib/function";
import { Refinement } from "fp-ts/lib/Refinement";
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// SETUP // // SETUP //
@ -20,9 +22,9 @@ export type Character = Warrior | Wizard | Archer;
// We have three types of `Damage`, each corresponding to a character type. // We have three types of `Damage`, each corresponding to a character type.
export enum Damage { export enum Damage {
Physical = 'Physical damage', Physical = "Physical damage",
Magical = 'Magical damage', Magical = "Magical damage",
Ranged = 'Ranged damage', Ranged = "Ranged damage",
} }
// A `Warrior` only can output physical damage. // A `Warrior` only can output physical damage.
@ -32,7 +34,7 @@ export class Warrior {
} }
toString() { toString() {
return 'Warrior'; return "Warrior";
} }
} }
@ -43,7 +45,7 @@ export class Wizard {
} }
toString() { toString() {
return 'Wizard'; return "Wizard";
} }
} }
@ -54,7 +56,7 @@ export class Archer {
} }
toString() { toString() {
return 'Archer'; return "Archer";
} }
} }
@ -79,8 +81,8 @@ export const isArcher = (character: Character): character is Archer => {
// - the player can try to perform the wrong action for a character class // - the player can try to perform the wrong action for a character class
export enum Exo2FailureType { export enum Exo2FailureType {
NoTarget = 'Exo2FailureType_NoTarget', NoTarget = "Exo2FailureType_NoTarget",
InvalidTarget = 'Exo2FailureType_InvalidTarget', InvalidTarget = "Exo2FailureType_InvalidTarget",
} }
export type NoTargetFailure = Failure<Exo2FailureType.NoTarget>; export type NoTargetFailure = Failure<Exo2FailureType.NoTarget>;
@ -117,17 +119,67 @@ export const invalidTargetFailure = Failure.builder(
// common operations done with the `Either` type and it is available through // common operations done with the `Either` type and it is available through
// the `chain` operator and its slightly relaxed variant `chainW`. // the `chain` operator and its slightly relaxed variant `chainW`.
const checkTargetIsSelected: (
target: Option<Character>,
) => Either<NoTargetFailure, Character> = either.fromOption(() =>
noTargetFailure("No unit currently selected")
);
const tryPerformAction: <A extends Character, B extends A>(
predicate: Refinement<A, B>,
onAction: (target: B) => Damage,
onFailure: (target: A) => string,
) => (target: A) => Either<InvalidTargetFailure, Damage> = (
predicate,
onAction,
onFailure,
) =>
flow(
either.fromPredicate(
predicate,
flow(onFailure, invalidTargetFailure),
),
either.map(onAction),
);
const trySmash = tryPerformAction(
isWarrior,
(t) => t.smash(),
(t) => `${t.toString()} cannot perform smash`,
);
const tryBurn = tryPerformAction(
isWizard,
(t) => t.burn(),
(t) => `${t.toString()} cannot perform burn`,
);
const tryShoot = tryPerformAction(
isArcher,
(t) => t.shoot(),
(t) => `${t.toString()} cannot perform shoot`,
);
export const checkTargetAndSmash: ( export const checkTargetAndSmash: (
target: Option<Character>, target: Option<Character>,
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = unimplemented; ) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = flow(
checkTargetIsSelected,
either.flatMap(trySmash),
);
export const checkTargetAndBurn: ( export const checkTargetAndBurn: (
target: Option<Character>, target: Option<Character>,
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = unimplemented; ) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = flow(
checkTargetIsSelected,
either.flatMap(tryBurn),
);
export const checkTargetAndShoot: ( export const checkTargetAndShoot: (
target: Option<Character>, target: Option<Character>,
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = unimplemented; ) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = flow(
checkTargetIsSelected,
either.flatMap(tryShoot),
);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// OPTION // // OPTION //
@ -146,14 +198,20 @@ export const checkTargetAndShoot: (
// BONUS POINTS: If you properly defined small private helpers in the previous // BONUS POINTS: If you properly defined small private helpers in the previous
// section, they should be easily reused for those use-cases. // section, they should be easily reused for those use-cases.
export const smashOption: (character: Character) => Option<Damage> = export const smashOption: (character: Character) => Option<Damage> = flow(
unimplemented; trySmash,
option.fromEither,
);
export const burnOption: (character: Character) => Option<Damage> = export const burnOption: (character: Character) => Option<Damage> = flow(
unimplemented; tryBurn,
option.fromEither,
);
export const shootOption: (character: Character) => Option<Damage> = export const shootOption: (character: Character) => Option<Damage> = flow(
unimplemented; tryShoot,
option.fromEither,
);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// ARRAY // // ARRAY //
@ -174,5 +232,24 @@ export interface TotalDamage {
[Damage.Ranged]: number; [Damage.Ranged]: number;
} }
export const attack: (army: ReadonlyArray<Character>) => TotalDamage = const emptyTotalDamage: () => TotalDamage = () => ({
unimplemented; [Damage.Physical]: 0,
[Damage.Magical]: 0,
[Damage.Ranged]: 0,
});
export const attack: (army: ReadonlyArray<Character>) => TotalDamage = flow(
readonlyArray.filterMap((c) =>
isWarrior(c)
? smashOption(c)
: isWizard(c)
? burnOption(c)
: isArcher(c)
? shootOption(c)
: absurd(c) as Option<Damage>
),
readonlyArray.reduceRight(emptyTotalDamage(), (dmg, res) => {
res[dmg] += 1;
return res;
}),
);

View file

@ -1,8 +1,10 @@
// `fp-ts` training Exercise 3 // `fp-ts` training Exercise 3
// Sort things out with `Ord` // Sort things out with `Ord`
import { Option } from 'fp-ts/Option'; import { Option } from "fp-ts/Option";
import { unimplemented } from '../utils'; import { number, option, ord, readonlyArray, string } from "fp-ts";
import { pipe } from "fp-ts/lib/function";
import { Ord } from "fp-ts/lib/Ord";
// Have you ever looked at the methods provided by `fp-ts` own `Array` and // Have you ever looked at the methods provided by `fp-ts` own `Array` and
// `ReadonlyArray` modules? They expose a load of functions to manipulate // `ReadonlyArray` modules? They expose a load of functions to manipulate
@ -36,11 +38,11 @@ import { unimplemented } from '../utils';
export const sortStrings: ( export const sortStrings: (
strings: ReadonlyArray<string>, strings: ReadonlyArray<string>,
) => ReadonlyArray<string> = unimplemented; ) => ReadonlyArray<string> = readonlyArray.sort(string.Ord);
export const sortNumbers: ( export const sortNumbers: (
numbers: ReadonlyArray<number>, numbers: ReadonlyArray<number>,
) => ReadonlyArray<number> = unimplemented; ) => ReadonlyArray<number> = readonlyArray.sort(number.Ord);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// REVERSE SORT // // REVERSE SORT //
@ -57,7 +59,7 @@ export const sortNumbers: (
export const sortNumbersDescending: ( export const sortNumbersDescending: (
numbers: ReadonlyArray<number>, numbers: ReadonlyArray<number>,
) => ReadonlyArray<number> = unimplemented; ) => ReadonlyArray<number> = readonlyArray.sort(ord.reverse(number.Ord));
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// SORT OPTIONAL VALUES // // SORT OPTIONAL VALUES //
@ -75,7 +77,9 @@ export const sortNumbersDescending: (
export const sortOptionalNumbers: ( export const sortOptionalNumbers: (
optionalNumbers: ReadonlyArray<Option<number>>, optionalNumbers: ReadonlyArray<Option<number>>,
) => ReadonlyArray<Option<number>> = unimplemented; ) => ReadonlyArray<Option<number>> = readonlyArray.sort(
option.getOrd(number.Ord),
);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// SORT COMPLEX OBJECTS // // SORT COMPLEX OBJECTS //
@ -99,13 +103,25 @@ export interface Person {
readonly age: Option<number>; readonly age: Option<number>;
} }
const ordPersonName = string.Ord;
const ordPersonByName: Ord<Person> = pipe(
ordPersonName,
ord.contramap((p) => p.name),
);
const ordPersonAge = option.getOrd(number.Ord);
const ordPersonByAge: Ord<Person> = pipe(
ordPersonAge,
ord.contramap((p) => p.age),
);
export const sortPersonsByName: ( export const sortPersonsByName: (
persons: ReadonlyArray<Person>, persons: ReadonlyArray<Person>,
) => ReadonlyArray<Person> = unimplemented; ) => ReadonlyArray<Person> = readonlyArray.sort(ordPersonByName);
export const sortPersonsByAge: ( export const sortPersonsByAge: (
persons: ReadonlyArray<Person>, persons: ReadonlyArray<Person>,
) => ReadonlyArray<Person> = unimplemented; ) => ReadonlyArray<Person> = readonlyArray.sort(ordPersonByAge);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// COMBINE SORTING SCHEMES // // COMBINE SORTING SCHEMES //
@ -118,4 +134,7 @@ export const sortPersonsByAge: (
export const sortPersonsByAgeThenByName: ( export const sortPersonsByAgeThenByName: (
persons: ReadonlyArray<Person>, persons: ReadonlyArray<Person>,
) => ReadonlyArray<Person> = unimplemented; ) => ReadonlyArray<Person> = readonlyArray.sortBy([
ordPersonByAge,
ordPersonByName,
]);

View file

@ -1,9 +1,10 @@
// `fp-ts` training Exercise 4 // `fp-ts` training Exercise 4
// Dependency injection with `Reader` // Dependency injection with `Reader`
import { Reader } from 'fp-ts/Reader'; import { Reader } from "fp-ts/Reader";
import { unimplemented } from '../utils'; import { reader } from "fp-ts";
import { absurd, flow, pipe } from "fp-ts/lib/function";
// Sometimes, a function can have a huge amount of dependencies (services, // Sometimes, a function can have a huge amount of dependencies (services,
// repositories, ...) and it is often impractical (not to say truly annoying) // repositories, ...) and it is often impractical (not to say truly annoying)
@ -21,9 +22,9 @@ import { unimplemented } from '../utils';
// Let's consider a small range of countries (here, France, Spain and the USA): // Let's consider a small range of countries (here, France, Spain and the USA):
export enum Country { export enum Country {
France = 'France', France = "France",
Spain = 'Spain', Spain = "Spain",
USA = 'USA', USA = "USA",
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -44,8 +45,21 @@ export enum Country {
// //
// HINT: Take a look at `reader.ask` to access the environment value // HINT: Take a look at `reader.ask` to access the environment value
export const exclamation: (sentence: string) => Reader<Country, string> = export const exclamation: (sentence: string) => Reader<Country, string> = (
unimplemented(); sentence,
) =>
pipe(
reader.ask<Country>(),
reader.map((c) =>
c === Country.USA
? `${sentence}!`
: c === Country.France
? `${sentence} !`
: c === Country.Spain
? `¡${sentence}!`
: absurd(c)
),
);
// Obviously, different countries often mean different languages and so // Obviously, different countries often mean different languages and so
// different words for saying "Hello": // different words for saying "Hello":
@ -53,11 +67,13 @@ export const exclamation: (sentence: string) => Reader<Country, string> =
export const sayHello = (country: Country): string => { export const sayHello = (country: Country): string => {
switch (country) { switch (country) {
case Country.France: case Country.France:
return 'Bonjour'; return "Bonjour";
case Country.Spain: case Country.Spain:
return 'Buenos dìas'; return "Buenos dìas";
case Country.USA: case Country.USA:
return 'Hello'; return "Hello";
default:
return absurd(country);
} }
}; };
@ -70,7 +86,8 @@ export const sayHello = (country: Country): string => {
// HINT: You can look into `reader.map` to modify the output of a `Reader` // HINT: You can look into `reader.map` to modify the output of a `Reader`
// action. // action.
export const greet: (name: string) => Reader<Country, string> = unimplemented(); export const greet: (name: string) => Reader<Country, string> = (name) =>
pipe(reader.ask<Country>(), reader.map((c) => `${sayHello(c)}, ${name}`));
// Finally, we are going to compose multiple `Reader`s together. // Finally, we are going to compose multiple `Reader`s together.
// //
@ -84,5 +101,7 @@ export const greet: (name: string) => Reader<Country, string> = unimplemented();
// HINT: As with other wrapper types in `fp-ts`, `reader` offers a way of // HINT: As with other wrapper types in `fp-ts`, `reader` offers a way of
// composing effects with `reader.chain`. // composing effects with `reader.chain`.
export const excitedlyGreet: (name: string) => Reader<Country, string> = export const excitedlyGreet: (name: string) => Reader<Country, string> = flow(
unimplemented(); greet,
reader.flatMap(exclamation),
);

View file

@ -1,12 +1,12 @@
// `fp-ts` training Exercise 5 // `fp-ts` training Exercise 5
// Managing nested effectful data with `traverse` // Managing nested effectful data with `traverse`
import { option, readonlyRecord, task } from 'fp-ts'; import { option, readonlyRecord, task, taskOption } from "fp-ts";
import { pipe } from 'fp-ts/lib/function'; import { flow, pipe } from "fp-ts/lib/function";
import { Option } from 'fp-ts/lib/Option'; import { Option } from "fp-ts/lib/Option";
import { ReadonlyRecord } from 'fp-ts/lib/ReadonlyRecord'; import { ReadonlyRecord } from "fp-ts/lib/ReadonlyRecord";
import { Task } from 'fp-ts/lib/Task'; import { Task } from "fp-ts/lib/Task";
import { sleep, unimplemented, unimplementedAsync } from '../utils'; import { sleep } from "../utils";
// When using many different Functors in a complex application, we can easily // When using many different Functors in a complex application, we can easily
// get to a point when we have many nested types that we would like to 'merge', // get to a point when we have many nested types that we would like to 'merge',
@ -26,24 +26,23 @@ import { sleep, unimplemented, unimplementedAsync } from '../utils';
// Let's consider a small range of countries (here, France, Spain and the USA) // Let's consider a small range of countries (here, France, Spain and the USA)
// with a mapping from their name to their code: // with a mapping from their name to their code:
type CountryCode = 'FR' | 'SP' | 'US'; type CountryCode = "FR" | "SP" | "US";
export const countryNameToCountryCode: ReadonlyRecord<string, CountryCode> = { export const countryNameToCountryCode: ReadonlyRecord<string, CountryCode> = {
France: 'FR', France: "FR",
Spain: 'SP', Spain: "SP",
USA: 'US', USA: "US",
}; };
// Let's simulate the call to an api which would return the currency when // Let's simulate the call to an api which would return the currency when
// providing a country code. For the sake of simplicity, let's consider that it // providing a country code. For the sake of simplicity, let's consider that it
// cannot fail. // cannot fail.
type Currency = 'EUR' | 'DOLLAR'; type Currency = "EUR" | "DOLLAR";
export const getCountryCurrency: (countryCode: CountryCode) => Task<Currency> = export const getCountryCurrency: (countryCode: CountryCode) => Task<Currency> =
(countryCode: CountryCode): Task<Currency> => (countryCode: CountryCode): Task<Currency> => async () => {
async () => { if (countryCode === "US") {
if (countryCode === 'US') { return "DOLLAR";
return 'DOLLAR';
} }
return 'EUR'; return "EUR";
}; };
// Let's simulate a way for the user to provide a country name. // Let's simulate a way for the user to provide a country name.
@ -68,9 +67,9 @@ export const getCountryCode: (countryName: string) => Option<CountryCode> = (
// country name and return its currency if it knows the country. // country name and return its currency if it knows the country.
// A naive implementation would be mapping on each `Task` and `Option` to call // A naive implementation would be mapping on each `Task` and `Option` to call
// the correct method: // the correct method:
export const naiveGiveCurrencyOfCountryToUser = ( export const naiveGiveCurrencyOfCountryToUser: (
countryNameFromUserMock: string, countryNameFromUserMock: string,
) => ) => Task<Option<Task<Currency>>> = (countryNameFromUserMock) =>
pipe( pipe(
getCountryNameFromUser(countryNameFromUserMock), getCountryNameFromUser(countryNameFromUserMock),
task.map(getCountryCode), task.map(getCountryCode),
@ -96,7 +95,9 @@ export const naiveGiveCurrencyOfCountryToUser = (
export const getCountryCurrencyOfOptionalCountryCode: ( export const getCountryCurrencyOfOptionalCountryCode: (
optionalCountryCode: Option<CountryCode>, optionalCountryCode: Option<CountryCode>,
) => Task<Option<Currency>> = unimplementedAsync; ) => Task<Option<Currency>> = option.traverse(task.ApplicativePar)(
getCountryCurrency,
);
// Let's now use this function in our naive implementation's pipe to see how it // Let's now use this function in our naive implementation's pipe to see how it
// improves it. // improves it.
@ -108,12 +109,24 @@ export const getCountryCurrencyOfOptionalCountryCode: (
export const giveCurrencyOfCountryToUser: ( export const giveCurrencyOfCountryToUser: (
countryNameFromUserMock: string, countryNameFromUserMock: string,
) => Task<Option<Currency>> = unimplementedAsync; ) => Task<Option<Currency>> = flow(
getCountryNameFromUser,
task.map(getCountryCode),
task.chain(getCountryCurrencyOfOptionalCountryCode),
);
// BONUS: We don't necessarily need `traverse` to do this. Try implementing // BONUS: We don't necessarily need `traverse` to do this. Try implementing
// `giveCurrencyOfCountryToUser` by lifting some of the functions' results to // `giveCurrencyOfCountryToUser` by lifting some of the functions' results to
// `TaskOption` // `TaskOption`
export const giveCurrencyOfCountryToUser2: (
countryNameFromUserMock: string,
) => Task<Option<Currency>> = flow(
getCountryNameFromUser,
task.map(getCountryCode),
taskOption.flatMapTask(getCountryCurrency),
);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// TRAVERSING ARRAYS // // TRAVERSING ARRAYS //
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -148,7 +161,7 @@ export const getCountryCodeOfCountryNames = (
export const getValidCountryCodeOfCountryNames: ( export const getValidCountryCodeOfCountryNames: (
countryNames: ReadonlyArray<string>, countryNames: ReadonlyArray<string>,
) => Option<ReadonlyArray<CountryCode>> = unimplemented; ) => Option<ReadonlyArray<CountryCode>> = option.traverseArray(getCountryCode);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// TRAVERSING ARRAYS ASYNCHRONOUSLY // // TRAVERSING ARRAYS ASYNCHRONOUSLY //
@ -165,7 +178,7 @@ export const getValidCountryCodeOfCountryNames: (
// Let's simulate a method that reads a number in a database, does some async // Let's simulate a method that reads a number in a database, does some async
// computation with it, replaces this number in the database by the result of // computation with it, replaces this number in the database by the result of
// the computation and returns it // the computation and returns it
const createSimulatedAsyncMethod = (): ((toAdd: number) => Task<number>) => { const createSimulatedAsyncMethod = (): (toAdd: number) => Task<number> => {
let number = 0; let number = 0;
return (toAdd: number) => async () => { return (toAdd: number) => async () => {
@ -186,7 +199,9 @@ const createSimulatedAsyncMethod = (): ((toAdd: number) => Task<number>) => {
export const simulatedAsyncMethodForParallel = createSimulatedAsyncMethod(); export const simulatedAsyncMethodForParallel = createSimulatedAsyncMethod();
export const performAsyncComputationInParallel: ( export const performAsyncComputationInParallel: (
numbers: ReadonlyArray<number>, numbers: ReadonlyArray<number>,
) => Task<ReadonlyArray<number>> = unimplementedAsync; ) => Task<ReadonlyArray<number>> = task.traverseArray(
simulatedAsyncMethodForParallel,
);
// Write a method to traverse an array by running the method // Write a method to traverse an array by running the method
// `simulatedAsyncMethodForSequence: (toAdd: number) => Task<number>` // `simulatedAsyncMethodForSequence: (toAdd: number) => Task<number>`
@ -198,7 +213,9 @@ export const performAsyncComputationInParallel: (
export const simulatedAsyncMethodForSequence = createSimulatedAsyncMethod(); export const simulatedAsyncMethodForSequence = createSimulatedAsyncMethod();
export const performAsyncComputationInSequence: ( export const performAsyncComputationInSequence: (
numbers: ReadonlyArray<number>, numbers: ReadonlyArray<number>,
) => Task<ReadonlyArray<number>> = unimplementedAsync; ) => Task<ReadonlyArray<number>> = task.traverseSeqArray(
simulatedAsyncMethodForSequence,
);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// SEQUENCE // // SEQUENCE //
@ -220,11 +237,11 @@ export const performAsyncComputationInSequence: (
export const sequenceOptionTask: ( export const sequenceOptionTask: (
optionOfTask: Option<Task<Currency>>, optionOfTask: Option<Task<Currency>>,
) => Task<Option<Currency>> = unimplementedAsync; ) => Task<Option<Currency>> = option.sequence(task.ApplicativePar);
export const sequenceOptionArray: ( export const sequenceOptionArray: (
arrayOfOptions: ReadonlyArray<Option<CountryCode>>, arrayOfOptions: ReadonlyArray<Option<CountryCode>>,
) => Option<ReadonlyArray<CountryCode>> = unimplemented; ) => Option<ReadonlyArray<CountryCode>> = option.sequenceArray;
// BONUS: try using these two functions in the exercises 'TRAVERSING OPTIONS' // BONUS: try using these two functions in the exercises 'TRAVERSING OPTIONS'
// and 'TRAVERSING ARRAYS' above // and 'TRAVERSING ARRAYS' above

View file

@ -1,10 +1,11 @@
// `fp-ts` training Exercise 6 // `fp-ts` training Exercise 6
// Introduction to `ReaderTaskEither` // Introduction to `ReaderTaskEither`
import { ReaderTaskEither } from 'fp-ts/lib/ReaderTaskEither'; import { ReaderTaskEither } from "fp-ts/lib/ReaderTaskEither";
import { unimplemented } from '../utils'; import { Application } from "./application";
import { Application } from './application'; import { User } from "./domain";
import { User } from './domain'; import { flow, pipe } from "fp-ts/lib/function";
import { readerTaskEither as rte } from "fp-ts";
// In real world applications you will mostly manipulate `ReaderTaskEither` aka // In real world applications you will mostly manipulate `ReaderTaskEither` aka
// `rte` in the use-cases of the application. // `rte` in the use-cases of the application.
@ -23,13 +24,20 @@ import { User } from './domain';
// current context. In the following example, we need to fetch a user by its id // current context. In the following example, we need to fetch a user by its id
// and then we want to return its capitalized. // and then we want to return its capitalized.
const capitalizeUserName: (user: User.User) => string = (user) =>
user.name[0].toUpperCase() + user.name.slice(1);
export const getCapitalizedUserName: (args: { export const getCapitalizedUserName: (args: {
userId: string; userId: string;
}) => ReaderTaskEither< }) => ReaderTaskEither<
User.Repository.Access, User.Repository.Access,
User.Repository.UserNotFoundError, User.Repository.UserNotFoundError,
string string
> = unimplemented; > = ({ userId }) =>
pipe(
User.Repository.getById(userId),
rte.map(capitalizeUserName),
);
// Sometimes you will need to get multiple data points before performing an operation // Sometimes you will need to get multiple data points before performing an operation
// on them. In this case, it is very convenient to use the `Do` notation. // on them. In this case, it is very convenient to use the `Do` notation.
@ -52,7 +60,13 @@ export const getConcatenationOfTheTwoUserNames: (args: {
User.Repository.Access, User.Repository.Access,
User.Repository.UserNotFoundError, User.Repository.UserNotFoundError,
string string
> = unimplemented; > = ({ userIdOne, userIdTwo }) =>
pipe(
rte.Do,
rte.apS("userOneName", getCapitalizedUserName({ userId: userIdOne })),
rte.apS("userTwoName", getCapitalizedUserName({ userId: userIdTwo })),
rte.map(({ userOneName, userTwoName }) => userOneName + userTwoName),
);
// Sometimes, you will need to feed the current context with data that you can // Sometimes, you will need to feed the current context with data that you can
// only retrieve after performing some operations, in other words, operations // only retrieve after performing some operations, in other words, operations
@ -69,7 +83,16 @@ export const getConcatenationOfTheBestFriendNameAndUserName: (args: {
User.Repository.Access, User.Repository.Access,
User.Repository.UserNotFoundError, User.Repository.UserNotFoundError,
string string
> = unimplemented; > = ({ userIdOne }) =>
pipe(
rte.Do,
rte.apS("user", User.Repository.getById(userIdOne)),
rte.bindW("bestFriend", ({ user }) =>
User.Repository.getById(user.bestFriendId)),
rte.map(({ user, bestFriend }) =>
capitalizeUserName(user) + capitalizeUserName(bestFriend)
),
);
// Most of the time, you will need to use several external services. // Most of the time, you will need to use several external services.
// The challenge of this usecase is to use TimeService in the flow of our `rte` // The challenge of this usecase is to use TimeService in the flow of our `rte`
@ -81,4 +104,13 @@ export const getConcatenationOfUserNameAndCurrentYear: (args: {
Dependencies, Dependencies,
User.Repository.UserNotFoundError, User.Repository.UserNotFoundError,
string string
> = unimplemented; > = ({ userIdOne }) =>
pipe(
rte.Do,
rte.apS("user", User.Repository.getById(userIdOne)),
rte.bindW(
"currentYear",
flow(Application.TimeService.thisYear, rte.fromReader),
),
rte.map(({ user, currentYear }) => user.name + currentYear),
);

View file

@ -1,7 +1,15 @@
// `fp-ts` training Exercise 7 // `fp-ts` training Exercise 7
// Manipulate collections with type-classes // Manipulate collections with type-classes
import { unimplemented } from '../utils'; import {
number,
readonlyArray,
readonlyMap,
readonlySet,
semigroup,
string,
} from "fp-ts";
import { pipe } from "fp-ts/lib/function";
// In this exercise, we will learn how to manipulate essential collections // In this exercise, we will learn how to manipulate essential collections
// such as `Set` and `Map`. // such as `Set` and `Map`.
@ -41,7 +49,9 @@ export const numberArray: ReadonlyArray<number> = [7, 42, 1337, 1, 0, 1337, 42];
// - `fp-ts` doesn't know how you want to define equality for the inner type // - `fp-ts` doesn't know how you want to define equality for the inner type
// and requires you to provide an `Eq` instance // and requires you to provide an `Eq` instance
export const numberSet: ReadonlySet<number> = unimplemented(); export const numberSet: ReadonlySet<number> = readonlySet.fromReadonlyArray(
number.Eq,
)(numberArray);
// Convert `numberSet` back to an array in `numberArrayFromSet`. // Convert `numberSet` back to an array in `numberArrayFromSet`.
// You need to use the `ReadonlySet` module from `fp-ts` instead of the // You need to use the `ReadonlySet` module from `fp-ts` instead of the
@ -57,7 +67,8 @@ export const numberSet: ReadonlySet<number> = unimplemented();
// the values to be ordered in the output array, by providing an `Ord` // the values to be ordered in the output array, by providing an `Ord`
// instance. // instance.
export const numberArrayFromSet: ReadonlyArray<number> = unimplemented(); export const numberArrayFromSet: ReadonlyArray<number> = readonlySet
.toReadonlyArray(number.Ord)(numberSet);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// MAP // // MAP //
@ -73,11 +84,11 @@ export const numberArrayFromSet: ReadonlyArray<number> = unimplemented();
// numbers, etc...) // numbers, etc...)
export const associativeArray: ReadonlyArray<[number, string]> = [ export const associativeArray: ReadonlyArray<[number, string]> = [
[1, 'Alice'], [1, "Alice"],
[2, 'Bob'], [2, "Bob"],
[3, 'Clara'], [3, "Clara"],
[4, 'Denise'], [4, "Denise"],
[2, 'Robert'], [2, "Robert"],
]; ];
// Construct `mapWithLastEntry` from the provided `associativeArray`. // Construct `mapWithLastEntry` from the provided `associativeArray`.
@ -101,7 +112,14 @@ export const associativeArray: ReadonlyArray<[number, string]> = [
// long as they implement `Foldable`. Here, you can simply pass the standard // long as they implement `Foldable`. Here, you can simply pass the standard
// `readonlyArray.Foldable` instance. // `readonlyArray.Foldable` instance.
export const mapWithLastEntry: ReadonlyMap<number, string> = unimplemented(); export const mapWithLastEntry: ReadonlyMap<number, string> = pipe(
associativeArray,
readonlyMap.fromFoldable(
number.Eq,
semigroup.last<string>(),
readonlyArray.Foldable,
),
);
// Same thing as above, except that upon key collision we don't want to simply // Same thing as above, except that upon key collision we don't want to simply
// select the newest entry value but append it to the previous one. // select the newest entry value but append it to the previous one.
@ -121,8 +139,14 @@ export const mapWithLastEntry: ReadonlyMap<number, string> = unimplemented();
// Did you find something in the `Semigroup` module that may have been // Did you find something in the `Semigroup` module that may have been
// helpful in defining `mapWithLastEntry`? // helpful in defining `mapWithLastEntry`?
export const mapWithConcatenatedEntries: ReadonlyMap<number, string> = export const mapWithConcatenatedEntries: ReadonlyMap<number, string> = pipe(
unimplemented(); associativeArray,
readonlyMap.fromFoldable(
number.Eq,
string.Semigroup,
readonlyArray.Foldable,
),
);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// DIFFERENCE / UNION / INTERSECTION // // DIFFERENCE / UNION / INTERSECTION //
@ -137,12 +161,18 @@ export const odds = new Set([1, 3, 5, 7, 9]);
// HINT: // HINT:
// - Be mindful of the order of operands for the operator you will choose. // - Be mindful of the order of operands for the operator you will choose.
export const nonPrimeOdds: ReadonlySet<number> = unimplemented(); export const nonPrimeOdds: ReadonlySet<number> = pipe(
odds,
readonlySet.difference(number.Eq)(primes),
);
// Construct the set `primeOdds` from the two sets defined above. It should // Construct the set `primeOdds` from the two sets defined above. It should
// only include the odd numbers that are also prime. // only include the odd numbers that are also prime.
export const primeOdds: ReadonlySet<number> = unimplemented(); export const primeOdds: ReadonlySet<number> = pipe(
odds,
readonlySet.intersection(number.Eq)(primes),
);
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -159,29 +189,43 @@ export type Analytics = {
export const pageViewsA = new Map( export const pageViewsA = new Map(
[ [
{ page: 'home', views: 5 }, { page: "home", views: 5 },
{ page: 'about', views: 2 }, { page: "about", views: 2 },
{ page: 'blog', views: 7 }, { page: "blog", views: 7 },
].map(entry => [entry.page, entry]), ].map((entry) => [entry.page, entry]),
); );
export const pageViewsB = new Map( export const pageViewsB = new Map(
[ [
{ page: 'home', views: 10 }, { page: "home", views: 10 },
{ page: 'blog', views: 35 }, { page: "blog", views: 35 },
{ page: 'faq', views: 5 }, { page: "faq", views: 5 },
].map(entry => [entry.page, entry]), ].map((entry) => [entry.page, entry]),
); );
const AnalyticsSemigroup = semigroup.struct({
page: semigroup.first<string>(),
views: number.SemigroupSum,
});
// Construct the `Map` with the total page views for all the pages in both sources // Construct the `Map` with the total page views for all the pages in both sources
// of analytics `pageViewsA` and `pageViewsB`. // of analytics `pageViewsA` and `pageViewsB`.
// //
// In case a page appears in both sources, their view count should be summed. // In case a page appears in both sources, their view count should be summed.
export const allPageViews: ReadonlyMap<string, Analytics> = unimplemented(); export const allPageViews: ReadonlyMap<string, Analytics> = pipe(
pageViewsA,
readonlyMap.union(string.Eq, AnalyticsSemigroup)(
pageViewsB,
),
);
// Construct the `Map` with the total page views but only for the pages that // Construct the `Map` with the total page views but only for the pages that
// appear in both sources of analytics `pageViewsA` and `pageViewsB`. // appear in both sources of analytics `pageViewsA` and `pageViewsB`.
export const intersectionPageViews: ReadonlyMap<string, Analytics> = export const intersectionPageViews: ReadonlyMap<string, Analytics> = pipe(
unimplemented(); pageViewsA,
readonlyMap.intersection(string.Eq, AnalyticsSemigroup)(
pageViewsB,
),
);

View file

@ -1,9 +1,10 @@
// `fp-ts` training Exercise 8 // `fp-ts` training Exercise 8
// Define your own combinators // Define your own combinators
import { Either } from 'fp-ts/Either'; import { Either } from "fp-ts/Either";
import { ReaderTaskEither } from 'fp-ts/ReaderTaskEither'; import { ReaderTaskEither } from "fp-ts/ReaderTaskEither";
import { unimplemented } from '../utils'; import { readerTaskEither as rte } from "fp-ts";
import { Reader } from "fp-ts/lib/Reader";
// Technically, a combinator is a pure function with no free variables in it, // Technically, a combinator is a pure function with no free variables in it,
// ie. one that does not depend on any variable from its enclosing scope. // ie. one that does not depend on any variable from its enclosing scope.
@ -75,12 +76,21 @@ export const bindEitherK: <N extends string, A, E, B>(
R, R,
E, E,
{ readonly [K in N | keyof A]: K extends keyof A ? A[K] : B } { readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }
> = unimplemented; > = (name, f) => rte.bind(name, (a) => rte.fromEither(f(a)));
// Write the implementation and type definition of `bindEitherKW`, the // Write the implementation and type definition of `bindEitherKW`, the
// "Widened" version of `bindEitherK`. // "Widened" version of `bindEitherK`.
export const bindEitherKW = unimplemented; export const bindEitherKW: <N extends string, A, E2, B>(
name: Exclude<N, keyof A>,
f: (a: A) => Either<E2, B>,
) => <R, E1>(
ma: ReaderTaskEither<R, E1, A>,
) => ReaderTaskEither<
R,
E2 | E1,
{ readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }
> = (name, f) => rte.bindW(name, (a) => rte.fromEither(f(a)));
// Write the implementations and type definitions of `apSEitherK` and // Write the implementations and type definitions of `apSEitherK` and
// `apSEitherKW`. // `apSEitherKW`.
@ -89,9 +99,27 @@ export const bindEitherKW = unimplemented;
// - remember that "widen" in the case of `Either` means the union of the // - remember that "widen" in the case of `Either` means the union of the
// possible error types // possible error types
export const apSEitherK = unimplemented; export const apSEitherK: <N extends string, A, E, B>(
name: Exclude<N, keyof A>,
fa: Either<E, B>,
) => <R>(
ma: ReaderTaskEither<R, E, A>,
) => ReaderTaskEither<
R,
E,
{ readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }
> = (name, fa) => rte.apS(name, rte.fromEither(fa));
export const apSEitherKW = unimplemented; export const apSEitherKW: <N extends string, A, E2, B>(
name: Exclude<N, keyof A>,
fa: Either<E2, B>,
) => <R, E1>(
ma: ReaderTaskEither<R, E1, A>,
) => ReaderTaskEither<
R,
E2 | E1,
{ readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }
> = (name, fa) => rte.apSW(name, rte.fromEither(fa));
// Write the implementations and type definitions of `bindReaderK` and // Write the implementations and type definitions of `bindReaderK` and
// `bindReaderKW`. // `bindReaderKW`.
@ -100,6 +128,24 @@ export const apSEitherKW = unimplemented;
// - remember that "widen" in the case of `Reader` means the intersection of // - remember that "widen" in the case of `Reader` means the intersection of
// the possible environment types // the possible environment types
export const bindReaderK = unimplemented; export const bindReaderK: <N extends string, A, R, B>(
name: Exclude<N, keyof A>,
f: (a: A) => Reader<R, B>,
) => <E>(
ma: ReaderTaskEither<R, E, A>,
) => ReaderTaskEither<
R,
E,
{ readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }
> = (name, f) => rte.bind(name, (a) => rte.fromReader(f(a)));
export const bindReaderKW = unimplemented; export const bindReaderKW: <N extends string, A, R2, B>(
name: Exclude<N, keyof A>,
f: (a: A) => Reader<R2, B>,
) => <E, R1>(
ma: ReaderTaskEither<R1, E, A>,
) => ReaderTaskEither<
R1 & R2,
E,
{ readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }
> = (name, f) => rte.bindW(name, (a) => rte.fromReader(f(a)));

7386
yarn.lock

File diff suppressed because it is too large Load diff