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

5
.gitignore vendored
View file

@ -13,4 +13,7 @@ node_modules
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`
// - `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;
@ -42,9 +42,10 @@ export const not = (value: boolean) => !value;
// `pipe` or `flow`), write the function `isOdd` that checks if a number is
// 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
// 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.
export const ifThenElse =
<A>(onTrue: () => A, onFalse: () => A) =>
(condition: boolean) =>
<A>(onTrue: () => A, onFalse: () => A) => (condition: boolean) =>
condition ? onTrue() : onFalse();
// Using `pipe` and `ifThenElse`, write the function that computes the next step in the Collatz
// 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
// a_n from the Collatz sequence, returns the number a_n+3 (ie. the number
// 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
// - TaskEither
import { Either } from 'fp-ts/Either';
import { Option } from 'fp-ts/Option';
import { TaskEither } from 'fp-ts/TaskEither';
import { unimplemented, sleep, unimplementedAsync } from '../utils';
import { Either } from "fp-ts/Either";
import { Option } from "fp-ts/Option";
import { TaskEither } from "fp-ts/TaskEither";
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 => {
return a / b;
@ -24,8 +26,12 @@ export const divide = (a: number, b: number): number => {
// - `option.some(value)`
// - `option.none`
export const safeDivide: (a: number, b: number) => Option<number> =
unimplemented;
export const safeDivide: (a: number, b: number) => Option<number> = (a, b) =>
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!
// 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)`
// 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 type DivisionByZeroError = "Error: Division by zero";
export const DivisionByZero = "Error: Division by zero" as const;
export const safeDivideWithError: (
a: number,
b: number,
) => Either<DivisionByZeroError, number> = unimplemented;
) => Either<DivisionByZeroError, number> = flow(
safeDivide,
either.fromOption(() => DivisionByZero),
);
///////////////////////////////////////////////////////////////////////////////
// TASKEITHER //
@ -70,7 +79,7 @@ export const asyncDivide = async (a: number, b: number) => {
await sleep(1000);
if (b === 0) {
throw new Error('BOOM!');
throw new Error("BOOM!");
}
return a / b;
@ -86,4 +95,5 @@ export const asyncDivide = async (a: number, b: number) => {
export const asyncSafeDivideWithError: (
a: 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
// Let's have fun with combinators!
import { Either } from 'fp-ts/Either';
import { Option } from 'fp-ts/Option';
import { Failure } from '../Failure';
import { unimplemented } from '../utils';
import { Either } from "fp-ts/Either";
import { Option } from "fp-ts/Option";
import { Failure } from "../Failure";
import { either, option, readonlyArray } from "fp-ts";
import { absurd, flow } from "fp-ts/lib/function";
import { Refinement } from "fp-ts/lib/Refinement";
///////////////////////////////////////////////////////////////////////////////
// SETUP //
@ -20,9 +22,9 @@ export type Character = Warrior | Wizard | Archer;
// We have three types of `Damage`, each corresponding to a character type.
export enum Damage {
Physical = 'Physical damage',
Magical = 'Magical damage',
Ranged = 'Ranged damage',
Physical = "Physical damage",
Magical = "Magical damage",
Ranged = "Ranged damage",
}
// A `Warrior` only can output physical damage.
@ -32,7 +34,7 @@ export class Warrior {
}
toString() {
return 'Warrior';
return "Warrior";
}
}
@ -43,7 +45,7 @@ export class Wizard {
}
toString() {
return 'Wizard';
return "Wizard";
}
}
@ -54,7 +56,7 @@ export class Archer {
}
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
export enum Exo2FailureType {
NoTarget = 'Exo2FailureType_NoTarget',
InvalidTarget = 'Exo2FailureType_InvalidTarget',
NoTarget = "Exo2FailureType_NoTarget",
InvalidTarget = "Exo2FailureType_InvalidTarget",
}
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
// 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: (
target: Option<Character>,
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = unimplemented;
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = flow(
checkTargetIsSelected,
either.flatMap(trySmash),
);
export const checkTargetAndBurn: (
target: Option<Character>,
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = unimplemented;
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = flow(
checkTargetIsSelected,
either.flatMap(tryBurn),
);
export const checkTargetAndShoot: (
target: Option<Character>,
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = unimplemented;
) => Either<NoTargetFailure | InvalidTargetFailure, Damage> = flow(
checkTargetIsSelected,
either.flatMap(tryShoot),
);
///////////////////////////////////////////////////////////////////////////////
// OPTION //
@ -146,14 +198,20 @@ export const checkTargetAndShoot: (
// BONUS POINTS: If you properly defined small private helpers in the previous
// section, they should be easily reused for those use-cases.
export const smashOption: (character: Character) => Option<Damage> =
unimplemented;
export const smashOption: (character: Character) => Option<Damage> = flow(
trySmash,
option.fromEither,
);
export const burnOption: (character: Character) => Option<Damage> =
unimplemented;
export const burnOption: (character: Character) => Option<Damage> = flow(
tryBurn,
option.fromEither,
);
export const shootOption: (character: Character) => Option<Damage> =
unimplemented;
export const shootOption: (character: Character) => Option<Damage> = flow(
tryShoot,
option.fromEither,
);
///////////////////////////////////////////////////////////////////////////////
// ARRAY //
@ -174,5 +232,24 @@ export interface TotalDamage {
[Damage.Ranged]: number;
}
export const attack: (army: ReadonlyArray<Character>) => TotalDamage =
unimplemented;
const emptyTotalDamage: () => TotalDamage = () => ({
[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
// Sort things out with `Ord`
import { Option } from 'fp-ts/Option';
import { unimplemented } from '../utils';
import { Option } from "fp-ts/Option";
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
// `ReadonlyArray` modules? They expose a load of functions to manipulate
@ -36,11 +38,11 @@ import { unimplemented } from '../utils';
export const sortStrings: (
strings: ReadonlyArray<string>,
) => ReadonlyArray<string> = unimplemented;
) => ReadonlyArray<string> = readonlyArray.sort(string.Ord);
export const sortNumbers: (
numbers: ReadonlyArray<number>,
) => ReadonlyArray<number> = unimplemented;
) => ReadonlyArray<number> = readonlyArray.sort(number.Ord);
///////////////////////////////////////////////////////////////////////////////
// REVERSE SORT //
@ -57,7 +59,7 @@ export const sortNumbers: (
export const sortNumbersDescending: (
numbers: ReadonlyArray<number>,
) => ReadonlyArray<number> = unimplemented;
) => ReadonlyArray<number> = readonlyArray.sort(ord.reverse(number.Ord));
///////////////////////////////////////////////////////////////////////////////
// SORT OPTIONAL VALUES //
@ -75,7 +77,9 @@ export const sortNumbersDescending: (
export const sortOptionalNumbers: (
optionalNumbers: ReadonlyArray<Option<number>>,
) => ReadonlyArray<Option<number>> = unimplemented;
) => ReadonlyArray<Option<number>> = readonlyArray.sort(
option.getOrd(number.Ord),
);
///////////////////////////////////////////////////////////////////////////////
// SORT COMPLEX OBJECTS //
@ -99,13 +103,25 @@ export interface Person {
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: (
persons: ReadonlyArray<Person>,
) => ReadonlyArray<Person> = unimplemented;
) => ReadonlyArray<Person> = readonlyArray.sort(ordPersonByName);
export const sortPersonsByAge: (
persons: ReadonlyArray<Person>,
) => ReadonlyArray<Person> = unimplemented;
) => ReadonlyArray<Person> = readonlyArray.sort(ordPersonByAge);
///////////////////////////////////////////////////////////////////////////////
// COMBINE SORTING SCHEMES //
@ -118,4 +134,7 @@ export const sortPersonsByAge: (
export const sortPersonsByAgeThenByName: (
persons: ReadonlyArray<Person>,
) => ReadonlyArray<Person> = unimplemented;
) => ReadonlyArray<Person> = readonlyArray.sortBy([
ordPersonByAge,
ordPersonByName,
]);

View file

@ -1,9 +1,10 @@
// `fp-ts` training Exercise 4
// 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,
// 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):
export enum Country {
France = 'France',
Spain = 'Spain',
USA = 'USA',
France = "France",
Spain = "Spain",
USA = "USA",
}
///////////////////////////////////////////////////////////////////////////////
@ -44,8 +45,21 @@ export enum Country {
//
// HINT: Take a look at `reader.ask` to access the environment value
export const exclamation: (sentence: string) => Reader<Country, string> =
unimplemented();
export const exclamation: (sentence: string) => Reader<Country, string> = (
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
// different words for saying "Hello":
@ -53,11 +67,13 @@ export const exclamation: (sentence: string) => Reader<Country, string> =
export const sayHello = (country: Country): string => {
switch (country) {
case Country.France:
return 'Bonjour';
return "Bonjour";
case Country.Spain:
return 'Buenos dìas';
return "Buenos dìas";
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`
// 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.
//
@ -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
// composing effects with `reader.chain`.
export const excitedlyGreet: (name: string) => Reader<Country, string> =
unimplemented();
export const excitedlyGreet: (name: string) => Reader<Country, string> = flow(
greet,
reader.flatMap(exclamation),
);

View file

@ -1,12 +1,12 @@
// `fp-ts` training Exercise 5
// Managing nested effectful data with `traverse`
import { option, readonlyRecord, task } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { Option } from 'fp-ts/lib/Option';
import { ReadonlyRecord } from 'fp-ts/lib/ReadonlyRecord';
import { Task } from 'fp-ts/lib/Task';
import { sleep, unimplemented, unimplementedAsync } from '../utils';
import { option, readonlyRecord, task, taskOption } from "fp-ts";
import { flow, pipe } from "fp-ts/lib/function";
import { Option } from "fp-ts/lib/Option";
import { ReadonlyRecord } from "fp-ts/lib/ReadonlyRecord";
import { Task } from "fp-ts/lib/Task";
import { sleep } from "../utils";
// 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',
@ -26,24 +26,23 @@ import { sleep, unimplemented, unimplementedAsync } from '../utils';
// Let's consider a small range of countries (here, France, Spain and the USA)
// 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> = {
France: 'FR',
Spain: 'SP',
USA: 'US',
France: "FR",
Spain: "SP",
USA: "US",
};
// 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
// cannot fail.
type Currency = 'EUR' | 'DOLLAR';
type Currency = "EUR" | "DOLLAR";
export const getCountryCurrency: (countryCode: CountryCode) => Task<Currency> =
(countryCode: CountryCode): Task<Currency> =>
async () => {
if (countryCode === 'US') {
return 'DOLLAR';
(countryCode: CountryCode): Task<Currency> => async () => {
if (countryCode === "US") {
return "DOLLAR";
}
return 'EUR';
return "EUR";
};
// 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.
// A naive implementation would be mapping on each `Task` and `Option` to call
// the correct method:
export const naiveGiveCurrencyOfCountryToUser = (
export const naiveGiveCurrencyOfCountryToUser: (
countryNameFromUserMock: string,
) =>
) => Task<Option<Task<Currency>>> = (countryNameFromUserMock) =>
pipe(
getCountryNameFromUser(countryNameFromUserMock),
task.map(getCountryCode),
@ -96,7 +95,9 @@ export const naiveGiveCurrencyOfCountryToUser = (
export const getCountryCurrencyOfOptionalCountryCode: (
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
// improves it.
@ -108,12 +109,24 @@ export const getCountryCurrencyOfOptionalCountryCode: (
export const giveCurrencyOfCountryToUser: (
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
// `giveCurrencyOfCountryToUser` by lifting some of the functions' results to
// `TaskOption`
export const giveCurrencyOfCountryToUser2: (
countryNameFromUserMock: string,
) => Task<Option<Currency>> = flow(
getCountryNameFromUser,
task.map(getCountryCode),
taskOption.flatMapTask(getCountryCurrency),
);
///////////////////////////////////////////////////////////////////////////////
// TRAVERSING ARRAYS //
///////////////////////////////////////////////////////////////////////////////
@ -148,7 +161,7 @@ export const getCountryCodeOfCountryNames = (
export const getValidCountryCodeOfCountryNames: (
countryNames: ReadonlyArray<string>,
) => Option<ReadonlyArray<CountryCode>> = unimplemented;
) => Option<ReadonlyArray<CountryCode>> = option.traverseArray(getCountryCode);
///////////////////////////////////////////////////////////////////////////////
// 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
// computation with it, replaces this number in the database by the result of
// the computation and returns it
const createSimulatedAsyncMethod = (): ((toAdd: number) => Task<number>) => {
const createSimulatedAsyncMethod = (): (toAdd: number) => Task<number> => {
let number = 0;
return (toAdd: number) => async () => {
@ -186,7 +199,9 @@ const createSimulatedAsyncMethod = (): ((toAdd: number) => Task<number>) => {
export const simulatedAsyncMethodForParallel = createSimulatedAsyncMethod();
export const performAsyncComputationInParallel: (
numbers: ReadonlyArray<number>,
) => Task<ReadonlyArray<number>> = unimplementedAsync;
) => Task<ReadonlyArray<number>> = task.traverseArray(
simulatedAsyncMethodForParallel,
);
// Write a method to traverse an array by running the method
// `simulatedAsyncMethodForSequence: (toAdd: number) => Task<number>`
@ -198,7 +213,9 @@ export const performAsyncComputationInParallel: (
export const simulatedAsyncMethodForSequence = createSimulatedAsyncMethod();
export const performAsyncComputationInSequence: (
numbers: ReadonlyArray<number>,
) => Task<ReadonlyArray<number>> = unimplementedAsync;
) => Task<ReadonlyArray<number>> = task.traverseSeqArray(
simulatedAsyncMethodForSequence,
);
///////////////////////////////////////////////////////////////////////////////
// SEQUENCE //
@ -220,11 +237,11 @@ export const performAsyncComputationInSequence: (
export const sequenceOptionTask: (
optionOfTask: Option<Task<Currency>>,
) => Task<Option<Currency>> = unimplementedAsync;
) => Task<Option<Currency>> = option.sequence(task.ApplicativePar);
export const sequenceOptionArray: (
arrayOfOptions: ReadonlyArray<Option<CountryCode>>,
) => Option<ReadonlyArray<CountryCode>> = unimplemented;
) => Option<ReadonlyArray<CountryCode>> = option.sequenceArray;
// BONUS: try using these two functions in the exercises 'TRAVERSING OPTIONS'
// and 'TRAVERSING ARRAYS' above

View file

@ -1,10 +1,11 @@
// `fp-ts` training Exercise 6
// Introduction to `ReaderTaskEither`
import { ReaderTaskEither } from 'fp-ts/lib/ReaderTaskEither';
import { unimplemented } from '../utils';
import { Application } from './application';
import { User } from './domain';
import { ReaderTaskEither } from "fp-ts/lib/ReaderTaskEither";
import { Application } from "./application";
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
// `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
// 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: {
userId: string;
}) => ReaderTaskEither<
User.Repository.Access,
User.Repository.UserNotFoundError,
string
> = unimplemented;
> = ({ userId }) =>
pipe(
User.Repository.getById(userId),
rte.map(capitalizeUserName),
);
// 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.
@ -52,7 +60,13 @@ export const getConcatenationOfTheTwoUserNames: (args: {
User.Repository.Access,
User.Repository.UserNotFoundError,
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
// only retrieve after performing some operations, in other words, operations
@ -69,7 +83,16 @@ export const getConcatenationOfTheBestFriendNameAndUserName: (args: {
User.Repository.Access,
User.Repository.UserNotFoundError,
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.
// The challenge of this usecase is to use TimeService in the flow of our `rte`
@ -81,4 +104,13 @@ export const getConcatenationOfUserNameAndCurrentYear: (args: {
Dependencies,
User.Repository.UserNotFoundError,
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
// 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
// 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
// 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`.
// 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`
// instance.
export const numberArrayFromSet: ReadonlyArray<number> = unimplemented();
export const numberArrayFromSet: ReadonlyArray<number> = readonlySet
.toReadonlyArray(number.Ord)(numberSet);
///////////////////////////////////////////////////////////////////////////////
// MAP //
@ -73,11 +84,11 @@ export const numberArrayFromSet: ReadonlyArray<number> = unimplemented();
// numbers, etc...)
export const associativeArray: ReadonlyArray<[number, string]> = [
[1, 'Alice'],
[2, 'Bob'],
[3, 'Clara'],
[4, 'Denise'],
[2, 'Robert'],
[1, "Alice"],
[2, "Bob"],
[3, "Clara"],
[4, "Denise"],
[2, "Robert"],
];
// 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
// `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
// 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
// helpful in defining `mapWithLastEntry`?
export const mapWithConcatenatedEntries: ReadonlyMap<number, string> =
unimplemented();
export const mapWithConcatenatedEntries: ReadonlyMap<number, string> = pipe(
associativeArray,
readonlyMap.fromFoldable(
number.Eq,
string.Semigroup,
readonlyArray.Foldable,
),
);
///////////////////////////////////////////////////////////////////////////////
// DIFFERENCE / UNION / INTERSECTION //
@ -137,12 +161,18 @@ export const odds = new Set([1, 3, 5, 7, 9]);
// HINT:
// - 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
// 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(
[
{ page: 'home', views: 5 },
{ page: 'about', views: 2 },
{ page: 'blog', views: 7 },
].map(entry => [entry.page, entry]),
{ page: "home", views: 5 },
{ page: "about", views: 2 },
{ page: "blog", views: 7 },
].map((entry) => [entry.page, entry]),
);
export const pageViewsB = new Map(
[
{ page: 'home', views: 10 },
{ page: 'blog', views: 35 },
{ page: 'faq', views: 5 },
].map(entry => [entry.page, entry]),
{ page: "home", views: 10 },
{ page: "blog", views: 35 },
{ page: "faq", views: 5 },
].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
// of analytics `pageViewsA` and `pageViewsB`.
//
// 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
// appear in both sources of analytics `pageViewsA` and `pageViewsB`.
export const intersectionPageViews: ReadonlyMap<string, Analytics> =
unimplemented();
export const intersectionPageViews: ReadonlyMap<string, Analytics> = pipe(
pageViewsA,
readonlyMap.intersection(string.Eq, AnalyticsSemigroup)(
pageViewsB,
),
);

View file

@ -1,9 +1,10 @@
// `fp-ts` training Exercise 8
// Define your own combinators
import { Either } from 'fp-ts/Either';
import { ReaderTaskEither } from 'fp-ts/ReaderTaskEither';
import { unimplemented } from '../utils';
import { Either } from "fp-ts/Either";
import { ReaderTaskEither } from "fp-ts/ReaderTaskEither";
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,
// 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,
E,
{ 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
// "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
// `apSEitherKW`.
@ -89,9 +99,27 @@ export const bindEitherKW = unimplemented;
// - remember that "widen" in the case of `Either` means the union of the
// 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
// `bindReaderKW`.
@ -100,6 +128,24 @@ export const apSEitherKW = unimplemented;
// - remember that "widen" in the case of `Reader` means the intersection of
// 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