diff --git a/jest.config.js b/jest.config.js index 8125681..55322fd 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,7 @@ module.exports = { testPathIgnorePatterns: ['/dist/', '/node_modules/'], globals: { 'ts-jest': { - diagnostics: false, + diagnostics: { warnOnly: true }, }, }, }; diff --git a/src/exo8/exo8.test.ts b/src/exo8/exo8.test.ts new file mode 100644 index 0000000..3190d01 --- /dev/null +++ b/src/exo8/exo8.test.ts @@ -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(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(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(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(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); + }); + }); +}); diff --git a/src/exo8/exo8.ts b/src/exo8/exo8.ts new file mode 100644 index 0000000..a8b7048 --- /dev/null +++ b/src/exo8/exo8.ts @@ -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` +// 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: ( + name: Exclude, + f: (a: A) => Either, +) => ( + ma: ReaderTaskEither, +) => 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; diff --git a/src/exo8/index.ts b/src/exo8/index.ts new file mode 100644 index 0000000..bb0c984 --- /dev/null +++ b/src/exo8/index.ts @@ -0,0 +1 @@ +export * from './exo8'; diff --git a/src/utils.ts b/src/utils.ts index 5f42e99..b29d1e1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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) {