Add exercise 8 for defining new combinators (#109)

*  Add exercise 8 for defining new combinators

* ♻️ Handle review comments

* 💬 Add more helpful instructions
This commit is contained in:
Hugo Saracino 2023-01-11 09:41:36 +07:00 committed by GitHub
parent e11c1b34c8
commit 40472e3fb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 228 additions and 2 deletions

View file

@ -4,7 +4,7 @@ module.exports = {
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
globals: {
'ts-jest': {
diagnostics: false,
diagnostics: { warnOnly: true },
},
},
};

120
src/exo8/exo8.test.ts Normal file
View file

@ -0,0 +1,120 @@
import { either, reader, readerTaskEither as rte } from 'fp-ts';
import { pipe } from 'fp-ts/function';
import {
apEitherK as rteApEitherK,
apEitherKW as rteApEitherKW,
bindEitherK as rteBindEitherK,
bindEitherKW as rteBindEitherKW,
bindReaderK as rteBindReaderK,
bindReaderKW as rteBindReaderKW,
} from './exo8';
describe('exo8', () => {
describe('bindEitherK[W]', () => {
it('should be usable in a happy path do-notation rte pipeline', async () => {
const pipeline = await pipe(
rte.Do,
rte.apS('foo', rte.of(42)),
rteBindEitherK('bar', ({ foo }) => either.right(foo * 2)),
)({})();
const expected = await rte.of({ foo: 42, bar: 84 })({})();
expect(pipeline).toStrictEqual(expected);
});
it('should be usable in a failing do-notation rte pipeline', async () => {
const pipeline = await pipe(
rte.Do,
rte.apS('foo', rte.of<unknown, string, number>(42)),
rteBindEitherK('bar', ({ foo }) => either.right(foo * 2)),
rteBindEitherK('baz', ({ foo, bar }) =>
either.left(`Error: foo = ${foo}, bar = ${bar}`),
),
)({})();
const expected = await rte.left('Error: foo = 42, bar = 84')({})();
expect(pipeline).toStrictEqual(expected);
});
it('should widen the error type when using the `W` variant', async () => {
const pipeline = await pipe(
rte.Do,
rte.apS('foo', rte.of<unknown, string, number>(42)),
rteBindEitherK('bar', ({ foo }) => either.right(foo * 2)),
rteBindEitherKW('baz', ({ foo, bar }) => either.left(foo + bar)),
)({})();
const expected = await rte.left(126)({})();
expect(pipeline).toStrictEqual(expected);
});
});
describe('apEitherK[W]', () => {
it('should be usable in a happy path do-notation rte pipeline', async () => {
const pipeline = await pipe(
rte.Do,
rte.apS('foo', rte.of(42)),
rteApEitherK('bar', either.right(1337)),
)({})();
const expected = await rte.of({ foo: 42, bar: 1337 })({})();
expect(pipeline).toStrictEqual(expected);
});
it('should be usable in a failing do-notation rte pipeline', async () => {
const pipeline = await pipe(
rte.Do,
rte.apS('foo', rte.of<unknown, string, number>(42)),
rteApEitherK('bar', either.right(1337)),
rteApEitherK('baz', either.left(`Error!`)),
)({})();
const expected = await rte.left('Error!')({})();
expect(pipeline).toStrictEqual(expected);
});
it('should widen the error type when using the `W` variant', async () => {
const pipeline = await pipe(
rte.Do,
rte.apS('foo', rte.of<unknown, string, number>(42)),
rteApEitherK('bar', either.right(1337)),
rteApEitherKW('baz', either.left(0)),
)({})();
const expected = await rte.left(0)({})();
expect(pipeline).toStrictEqual(expected);
});
});
describe('bindReaderK[W]', () => {
it('should be usable in a do-notation rte pipeline', async () => {
const pipeline = await pipe(
rte.Do,
rte.apS('foo', rte.of(42)),
rteBindReaderK('bar', ({ foo }) => reader.of(`${foo}`)),
)({})();
const expected = await rte.of({ foo: 42, bar: '42' })({})();
expect(pipeline).toStrictEqual(expected);
});
it('should widen the error type when using the `W` variant', async () => {
const pipeline = await pipe(
rte.Do,
rte.apS('foo', rte.of<{ a: string }, string, number>(42)),
rteBindReaderKW('bar', ({ foo }) => reader.of<{ b: number }>(`${foo}`)),
)({ a: '', b: 0 })();
const expected = await rte.of({ foo: 42, bar: '42' })({})();
expect(pipeline).toStrictEqual(expected);
});
});
});

105
src/exo8/exo8.ts Normal file
View file

@ -0,0 +1,105 @@
// `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';
// 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 for 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 }
> = unimplemented;
// Write the implementation and type definition of `bindEitherKW`, the
// "Widened" version of `bindEitherK`.
export const bindEitherKW = unimplemented;
// Write the implementations and type definitions of `apEitherK` and
// `apEitherKW`.
//
// HINT:
// - remember that "widen" in the case of `Either` means the union of the
// possible error types
export const apEitherK = unimplemented;
export const apEitherKW = unimplemented;
// Write the implementations and type definitions of `bindReaderK` and
// `bindReaderKW`.
//
// HINT:
// - remember that "widen" in the case of `Reader` means the interesection of
// the possible environment types
export const bindReaderK = unimplemented;
export const bindReaderKW = unimplemented;

1
src/exo8/index.ts Normal file
View file

@ -0,0 +1 @@
export * from './exo8';

View file

@ -2,7 +2,7 @@ import { reader } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { Reader } from 'fp-ts/lib/Reader';
export const unimplemented = () => undefined as any;
export const unimplemented = (..._args: any) => undefined as any;
export const unimplementedAsync = () => () => undefined as any;
export function sleep(ms: number) {