This repository has been archived on 2023-05-29. You can view files and clone it, but cannot push or open issues or pull requests.
fp-ts-training/src/exo8/exo8.ts

152 lines
5.1 KiB
TypeScript

// `fp-ts` training Exercise 8
// Define your own combinators
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.
//
// We usually refer to functions that allow the manipulation of types like
// `Option`, `Either` and the likes such as `map`, `chain` and so on as
// combinators.
//
// The fp-ts library provides a rich collection of such combinators for each
// type and module but sometimes, you may want to reach for a combinator that
// doesn't yet exist in the library and it is useful to know how to define
// your own.
///////////////////////////////////////////////////////////////////////////////
// TRANSFORMER HELPERS //
///////////////////////////////////////////////////////////////////////////////
// Transformer stacks such as `ReaderTaskEither` already provide useful
// combinators such as `chainOptionK`, `chainEitherK[W]`, and so on... However,
// these combinators are not available to use in the context of the do-notation
// (as `bindXXX` variants).
// Part of the difficulty in writing your own combinator is writing its type
// definition properly.
// We provide the type definition for this first one as a stepping stone, but
// the following ones must be carefully defined on your own.
// Write the implementation of `bindEitherK`. It must behave like
// `rte.chainEitherK` but in the context of the do-notation.
//
// HINTS:
// - take some time to study the type definition carefully
// - the implementation really should be the easy part, as you are allowed to
// use any other combinators available from the library
// - you may want to define it using the existing `rte.bind` and somehow
// applying some conversion to the result
//
// **Read if you are STUCK**:
//
// Imagine you have a rte-based pipe, eg:
//
// ```ts
// const foo = pipe(
// rte.Do,
// rte.apS('user', getUser(userId)),
// ...
// );
// ```
//
// Say you want to apply a function `bar: (user: User) => Either<E, User>`
// inside that pipe to produce a result.
// How would you do it without `bindEitherK`?
//
// You would probably try to use something like:
// `rte.bind('newValue', ({ user }) => bar(user))`.
// Only that doesn't work because `rte.bind` expects a function that returns a
// `ReaderTaskEither`, not an `Either`. However, you can always convert (lift)
// an `Either` to a `ReaderTaskEither` with `rte.fromEither`.
//
// Well there you have it, `bindEitherK` is nothing more than
// `rte.bind(name, a => rte.fromEither(f(a)))`
export const bindEitherK: <N extends string, A, E, B>(
name: Exclude<N, keyof A>,
f: (a: A) => 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, 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: <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`.
//
// HINT:
// - remember that "widen" in the case of `Either` means the union of the
// possible error types
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: <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`.
//
// HINT:
// - remember that "widen" in the case of `Reader` means the intersection of
// the possible environment types
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: <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)));