✨ 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:
parent
e11c1b34c8
commit
40472e3fb7
5 changed files with 228 additions and 2 deletions
|
@ -4,7 +4,7 @@ module.exports = {
|
||||||
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
|
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
|
||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
diagnostics: false,
|
diagnostics: { warnOnly: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
120
src/exo8/exo8.test.ts
Normal file
120
src/exo8/exo8.test.ts
Normal 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
105
src/exo8/exo8.ts
Normal 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
1
src/exo8/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './exo8';
|
|
@ -2,7 +2,7 @@ import { reader } from 'fp-ts';
|
||||||
import { pipe } from 'fp-ts/lib/function';
|
import { pipe } from 'fp-ts/lib/function';
|
||||||
import { Reader } from 'fp-ts/lib/Reader';
|
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 const unimplementedAsync = () => () => undefined as any;
|
||||||
|
|
||||||
export function sleep(ms: number) {
|
export function sleep(ms: number) {
|
||||||
|
|
Reference in a new issue