Last active
January 20, 2022 14:46
-
-
Save gillchristian/a1a5a60b14d46e7a54cf41f9eea3e0f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as Option from "fp-ts/Option"; | |
import * as Either from "fp-ts/Either"; | |
import * as ArrayFP from "fp-ts/Array"; | |
import * as TaskEither from "fp-ts/TaskEither"; | |
import { Ord, contramap } from "fp-ts/Ord"; | |
import { Ord as ordString, Semigroup as semigroupString } from "fp-ts/string"; | |
import { sequenceS } from "fp-ts/Apply"; | |
import { flow, pipe } from "fp-ts/function"; | |
// Functor | |
const a = [1, 2, 3]; | |
const toString = (x: number) => x.toString(); | |
toString(1); | |
const len = (s: string) => s.length; | |
a.map(toString); | |
// Array.map : (f: (a: A) => B) => Array<A> => Array<B> | |
// Option.map : (f: (a: A) => B) => Option<A> => Option<B> | |
pipe(Option.some(1), Option.map(toString)); | |
Promise.resolve(42).then(toString); | |
// composition | |
// Array<number> => Array<string> => Array<number> | |
a.map(toString).map(len) === a.map((x) => len(toString(x))); | |
a.map(toString).map(len) === a.map((x) => pipe(x, toString, len)); | |
// a.map(toString).map(len) === a.map((x) => x |> toString |> len); | |
a.map(toString).map(len) === a.map((x) => flow(toString, len)(x)); | |
a.map(toString).map(len) === a.map(flow(toString, len)); | |
const z = flow(toString, len); | |
// vs. | |
const z_ = (x: number) => pipe(x, toString, len); | |
a.map(toString).map(len) === a.map(z); | |
// Apply / Applicative | |
// Option<A> = None | Some<A> | |
// | |
// Either<L, R> = Left<L> | Right<R> | |
// Result<E, A> = Err<E> | Ok<A> | |
const f = (s: string) => (n: number) => `${s} ${n}`; | |
const optionA = Option.some(42); | |
const optionB = Option.some("foo"); | |
f("foo")(42); // "foo 42" | |
pipe( | |
optionB, | |
Option.map(f) // Option<(n: number) => string> | |
); | |
pipe( | |
sequenceS(Option.Apply)({ a: optionA, b: optionB }), // Option<{ a: number, b: string }> | |
Option.map(({ a, b }) => f(b)(a)) // Option<string> | |
); | |
const optionC: Option.Option<number> = Option.none; | |
pipe( | |
sequenceS(Option.Apply)({ a: optionC, b: optionB }), // Option<{ a: number, b: string }> | |
Option.map(({ a, b }) => f(b)(a)) // Option<string> | |
); | |
// Monad | |
Promise.resolve(42).then((x) => Promise.resolve(toString(x))); // is not -> Promise<Promise<string>> | |
// is actually -> Promise<string> | |
const lenIs5 = (s: string): Option.Option<string> => | |
s.length === 5 ? Option.some(s) : Option.none; | |
pipe( | |
Option.some("hola"), | |
Option.map(lenIs5), // Option<Option<string>> | |
(v) => v | |
); | |
// chain : ((a: A) => Option<B>) => Option<A> => Option<B> | |
pipe( | |
Option.some("hola"), | |
Option.chain(lenIs5), // Option<string> | |
(v) => v | |
); | |
pipe( | |
[1, 2, 3], | |
ArrayFP.map((x) => [x, 1, 2]), // Array<Array<number>> | |
// [[1, 1, 2], [2, 1, 2], [3, 1, 2]] | |
(v) => v | |
); | |
pipe( | |
[1, 2, 3], | |
ArrayFP.chain((x) => [x, 1, 2]), // Array<number> | |
// [1, 1, 2, 2, 1, 2, 3, 1, 2] | |
(v) => v | |
); | |
const filter = | |
<A>(f: (a: A) => boolean) => | |
(xs: A[]): A[] => | |
pipe( | |
xs, | |
ArrayFP.chain((x) => (f(x) ? [x] : [])) | |
); | |
pipe( | |
[1, 2, 3], | |
filter((x) => x > 2) // [3] | |
); | |
pipe( | |
Option.some(4), | |
Option.chain((x) => (x < 2 ? Option.some(x) : Option.none)), // Option<number> | |
Option.map(toString) // Option<number> | |
// Option.none | |
); | |
// When to use Option? -> null-able values or error handling | |
type PoorMansOption<A> = A | null; // cannot implement the interfaces (Functor, etc) laws | |
const x: PoorMansOption<number> = null; | |
const y: PoorMansOption<PoorMansOption<number>> = 123; // ??? | |
// Ord contramap | |
interface User { | |
name: string; | |
} | |
const ordUser: Ord<User> = contramap((user: User) => user.name)(ordString); | |
pipe( | |
[{ name: "Zoe" }, { name: "Jane" }, { name: "Mikel" }], | |
ArrayFP.sort(ordUser) | |
); | |
// Types don't lie | |
const ubsurd = <A, B>(a: A): B => null as any as B; | |
const identity = <A>(a: A): A => a; | |
const constant = | |
<A, B>(a: A) => | |
(b: B): A => | |
a; | |
const always1 = constant(1); | |
always1("hola"); // 1 | |
always1(false); // 1 | |
constant("foo")(false); // 'foo' | |
const optionToValue = <A>(optionA: Option.Option<A>): A => | |
pipe( | |
optionA, | |
Option.match( | |
() => { | |
throw new Error("No A"); | |
}, | |
(a) => a | |
) | |
); | |
optionToValue(Option.some(123)); // 123 | |
optionToValue(Option.none); // Error !!! | |
const lenIs5_option = (s: string): Option.Option<string> => | |
s.length === 5 ? Option.some(s) : Option.none; | |
const lenIs5_either = (s: string): Either.Either<string, string> => | |
s.length === 5 ? Either.right(s) : Either.left(`Lenght is ${s.length}`); | |
interface UserWithLast { | |
name: string; | |
last_name?: string; | |
} | |
const userHasLastName = ( | |
user: UserWithLast | |
): Either.Either<string, UserWithLast> => | |
user.last_name ? Either.right(user) : Either.left("No last name"); | |
const nameIsLongEnough = ( | |
user: UserWithLast | |
): Either.Either<string, UserWithLast> => | |
pipe( | |
user.name, | |
lenIs5_either, | |
Either.map((_name) => user) | |
); | |
// Either<string, UserWithLast> | |
const sergiy: UserWithLast = { name: "Sergiy" }; | |
pipe( | |
[userHasLastName(sergiy), nameIsLongEnough(sergiy)], | |
Either.sequenceArray // Left<'No last name'> | |
); | |
const maksym: UserWithLast = { name: "Maksym", last_name: "Borsy..." }; | |
pipe( | |
[userHasLastName(maksym), nameIsLongEnough(maksym)], | |
Either.sequenceArray // Right<maksym> | |
); | |
const zoe: UserWithLast = { name: "Zoe" }; | |
pipe( | |
[userHasLastName(zoe), nameIsLongEnough(zoe)], | |
Either.sequenceArray // Left<'No last name'> | |
); | |
// Validation lets acumulate errors | |
// TODO: find better example of how to work with it xD | |
type Validation<E, A> = Either.Either<E[], A>; | |
const validation = Either.getApplicativeValidation(semigroupString); | |
// traverse : (f: (a: A) => Either<E, B>) => Array<A> => Either<E, Array<B>> | |
const traverse = ArrayFP.traverse(Either.Applicative); | |
const allUsersWithLastName = pipe( | |
[sergiy, zoe, maksym], | |
traverse(userHasLastName) // Either<string, UserWithLast[]> | |
); | |
// Either | |
const validateNameAndUpercase = (user: UserWithLast) => | |
pipe( | |
user, | |
userHasLastName, // Either<string, User> | |
Either.mapLeft((errMsg) => new Error(errMsg)), // Either<Error, User> | |
Either.map(({ name }) => name.toUpperCase()) // Either<Error, string> | |
); | |
const validateNameAndUpercase_ = (user: UserWithLast) => | |
pipe( | |
user, | |
userHasLastName, // Either<string, User> | |
Either.map(({ name }) => name.toUpperCase()), // Either<string, string> | |
Either.mapLeft((errMsg) => new Error(errMsg)) // Either<Error, string> | |
); | |
validateNameAndUpercase(zoe); // Left<Error(...)> | |
validateNameAndUpercase(maksym); // Right<'MAKSYM'> | |
// What about async code? | |
const getUser = () => Promise.resolve(zoe); | |
getUser() | |
.then(userHasLastName) | |
.then(Either.map(({ name }) => name.toUpperCase())) | |
.then(Either.mapLeft((errMsg) => new Error(errMsg))); | |
pipe( | |
getUser(), | |
(p) => p.then(userHasLastName), | |
(p) => p.then(Either.map(({ name }) => Promise.resolve(name.toUpperCase()))) | |
// Promise<Either<string, Promise<string>>> | |
); | |
// traverse : (f: (a: A) => X<E, B>) => Y<A> => X<E, Y<B>> | |
// Here X and Y are "higher order types" -> | |
// ie. they are interfaces that implement Traversable and Applicative typeclasses | |
pipe( | |
zoe, | |
userHasLastName, // Either<string, UserWithLast> | |
Either.map(({ name }) => Promise.resolve(name.toUpperCase())), | |
// Either<string, Promise<string>> | |
Either.match( | |
(err) => Promise.resolve(Either.left(err)), | |
(promise) => promise.then(Either.right) | |
), | |
// Promise<Either<string, string>> | |
(v: Promise<Either.Either<string, string>>) => v | |
); | |
const traversePromiseEither = <E, A>( | |
e: Either.Either<E, Promise<A>> | |
): Promise<Either.Either<E, A>> => | |
pipe( | |
e, | |
Either.match( | |
(err) => Promise.resolve(Either.left(err)), | |
(promise) => promise.then(Either.right) | |
) | |
); | |
// fp-ts way: TaskEither (https://gcanti.github.io/fp-ts/modules/TaskEither.ts.html) | |
const validateNameAndUpercaseAsync = (user: UserWithLast) => | |
pipe( | |
user, | |
userHasLastName, // Either<string, User> | |
TaskEither.fromEither, | |
// TaskEither<Error, User> | |
TaskEither.mapLeft((errMsg) => new Error(errMsg)), | |
// TaskEither<Error, string> | |
TaskEither.map(({ name }) => name.toUpperCase()), | |
// TaskEither<Error, number> | |
TaskEither.chain((name) => TaskEither.right(name.length)), | |
// ^ this could be an actual async function | |
TaskEither.chain((n) => TaskEither.left(new Error("Failed"))) | |
// ^ this could be an actual async function | |
); | |
const getResult = validateNameAndUpercaseAsync(maksym); | |
getResult() | |
.then((result) => { | |
pipe( | |
result, | |
Either.match( | |
(err) => {}, | |
(succ) => {} | |
) | |
); | |
// Either<Error, number> | |
}) | |
.catch(() => { | |
console.log("Really unexpected error"); | |
}); | |
// Decoding items in a list independent from each other | |
// 1. First decode list as => t.arry(t.unkown) | |
// 2. Then decode each item idependelty and accumulate the errors | |
// | |
// https://github.com/catawiki/cw-fulfilment/blob/d4c5f1c99860165da8431b2642d5c3ebb9ec179f/app/assets/client/shared/features/OrderList/api.ts |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment