✨ 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/'],
|
||||
globals: {
|
||||
'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 { 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) {
|
||||
|
|
Reference in a new issue