Fp-ts: Type Helpers to reduce boilerplate when writing methods for Type Classes

Created on 28 Mar 2019  路  11Comments  路  Source: gcanti/fp-ts

@gcanti - there is currently a ton of boilerplate when writing functions dealing with Type Classes like Apply/etc. I spent a bit of time tinkering with some helper types as I've detailed below, any thoughts?

import { Apply, Apply1, Apply2, Apply2C, Apply3, Apply3C, sequenceT } from 'fp-ts/lib/Apply'
import { log } from 'fp-ts/lib/Console'
import { Either, either } from 'fp-ts/lib/Either'
import { tuple } from 'fp-ts/lib/function'
import { HKT, Type, Type2, Type3, URIS, URIS2, URIS3 } from 'fp-ts/lib/HKT'
import { io } from 'fp-ts/lib/IO'

export type URIOf<F> = F extends { URI: infer A } ? A : never

export type UnknownApply<L = unknown, U = unknown> =
    | { [URI in URIS]: Apply1<URI> }[URIS]
    | { [URI in URIS2]: Apply2<URI> }[URIS2]
    | { [URI in URIS2]: Apply2C<URI, L> }[URIS2]
    | { [URI in URIS3]: Apply3<URI> }[URIS3]
    | { [URI in URIS3]: Apply3C<URI, U, L> }[URIS3]
    | Apply<unknown>

export type TypeOfApply<T, A, L = any, U = any> = T extends Apply1<infer URI>
    ? Type<URI, A>
    : T extends Apply2<infer URI>
    ? Type2<URI, L, A>
    : T extends Apply2C<infer URI, infer L2>
    ? Type2<URI, L2, A>
    : T extends Apply3<infer URI>
    ? Type3<URI, U, L, A>
    : T extends Apply3C<infer URI, infer U2, infer L2>
    ? Type3<URI, U2, L2, A>
    : T extends Apply<infer URI>
    ? HKT<URI, A>
    : never

export type AOf<T> = T extends { _A: infer A } ? A : never
export type MapAOf<T extends Array<any>> = { [k in keyof T]: AOf<T[k]> }
export type LOf<T> = T extends { _L: infer L } ? L : never
export type UOf<T> = T extends { _U: infer U } ? U : never

export type AOfArray<T extends any[]> = T extends Array<infer A> ? A : never

export function call<F extends UnknownApply>(
    F: F
): <TS extends TypeOfApply<F, any>[]>(
    ...fs: TS
) => <B>(f: (...as: MapAOf<TS>) => B) => TypeOfApply<F, B, LOf<AOfArray<TS>>, UOf<AOfArray<TS>>>
export function call<F extends Apply<any>>(F: F) {
    return <T extends HKT<URIOf<F>, any>, TS extends T[]>(...ts: TS) => <B>(
        f: (...as: MapAOf<TS>) => B
    ): HKT<URIOf<F>, B> => F.map(sequenceT(F)(...(ts as any)), as => f(...(as as any)))
}

const callIO = call(io)
const callEither = call(either)

const ok = <A>(a: A): Either<Error, A> => either.of<Error, A>(a)

const a = callIO(io.of('foo'), io.of(2))(tuple) // IO<[string, number]>
const b = callEither(ok('foo'), ok(2))(tuple) // Either<Error, [string, number]>

a.chain(log).run() // ['foo', 2]
b.fold(log, log).run() // ['foo', 2]
suggestion

Most helpful comment

Here's a rewrite with two updates:

1 - TypeParamsOf avoids reliance upon _A, _URI - uses inference from all known types

2 - By passing required first param, allow inference of L, U, X etc from usage - this is really helpful as the current implementation does not do this, and manually passing the type parameters requires passing the full args type as well

I added sequenceTv2 as an example as well

import { Apply, Apply1, Apply2, Apply2C, Apply3, Apply3C, sequenceT } from 'fp-ts/lib/Apply'
import { log } from 'fp-ts/lib/Console'
import { either, left } from 'fp-ts/lib/Either'
import { tuple } from 'fp-ts/lib/function'
import { HKT, HKT2, HKT3, HKT4, Type, Type2, Type3, Type4, URIS, URIS2, URIS3 } from 'fp-ts/lib/HKT'
import { io } from 'fp-ts/lib/IO'

export type TypeParamsOf<T> = T extends Type<infer URI, infer A>
    ? [URI, A, never, never, never]
    : T extends Type2<infer URI, infer L, infer A>
    ? [URI, A, L, never, never]
    : T extends Type3<infer URI, infer U, infer L, infer A>
    ? [URI, A, L, U, never]
    : T extends Type4<infer URI, infer X, infer U, infer L, infer A>
    ? [URI, A, L, U, X]
    : T extends HKT<infer URI, infer A>
    ? [URI, A, never, never, never]
    : T extends HKT2<infer URI, infer L, infer A>
    ? [URI, A, L, never, never]
    : T extends HKT3<infer URI, infer U, infer L, infer A>
    ? [URI, A, L, U, never]
    : T extends HKT4<infer URI, infer X, infer U, infer L, infer A>
    ? [URI, A, L, U, X]
    : [never, never, never, never, never]

export type URIOf<T> = TypeParamsOf<T>[0]
export type AOf<T> = TypeParamsOf<T>[1]
export type LOf<T> = TypeParamsOf<T>[2]
export type UOf<T> = TypeParamsOf<T>[3]
export type XOf<T> = TypeParamsOf<T>[4]

export type UnknownApply<L = unknown, U = unknown> =
    | { [URI in URIS]: Apply1<URI> }[URIS]
    | { [URI in URIS2]: Apply2<URI> }[URIS2]
    | { [URI in URIS2]: Apply2C<URI, L> }[URIS2]
    | { [URI in URIS3]: Apply3<URI> }[URIS3]
    | { [URI in URIS3]: Apply3C<URI, U, L> }[URIS3]
    | Apply<unknown>

export type TypeOfApply<T, A, L = unknown, U = unknown> = T extends Apply1<infer URI>
    ? Type<URI, A>
    : T extends Apply2<infer URI>
    ? Type2<URI, L, A>
    : T extends Apply2C<infer URI, infer L2>
    ? Type2<URI, L2, A>
    : T extends Apply3<infer URI>
    ? Type3<URI, U, L, A>
    : T extends Apply3C<infer URI, infer U2, infer L2>
    ? Type3<URI, U2, L2, A>
    : T extends Apply<infer URI>
    ? HKT<URI, A>
    : never

export type MapAOf<T> = { [k in keyof T]: AOf<T[k]> }
export type AOfArray<T extends unknown[]> = T extends Array<infer A> ? A : never
export type DomainOf<T extends Function> = T extends (...args: infer T) => any ? T : never

export function sequenceTv2<F extends UnknownApply>(
    F: F
): <TS extends TypeOfApply<F, any, L, U>[], A, L, U>(
    fa: TypeOfApply<F, A, L, U>,
    ...ts: TS
) => TypeOfApply<F, DomainOf<(a: A, ...rest: MapAOf<TS>) => any>, L, U>
export function sequenceTv2(F: Apply<unknown>) {
    return <TS extends HKT<any, A>[], A>(fa: HKT<any, A>, ...ts: TS) => sequenceT(F)(fa, ...ts)
}

export function call<F extends UnknownApply>(
    F: F
): <TS extends TypeOfApply<F, any, L, U>[], A, L, U>(
    fa: TypeOfApply<F, A, L, U>,
    ...ts: TS
) => <B>(f: (a: A, ...rest: MapAOf<TS>) => B) => TypeOfApply<F, B, L, A>
export function call(F: Apply<unknown>) {
    return <TS extends HKT<any, any>[], A>(fa: HKT<any, A>, ...ts: TS) => <B>(
        f: (a: A, ...rest: any[]) => B
    ) => F.map(sequenceTv2(F)(fa, ...ts), ([a, ...rest]) => f(a, ...rest))
}

const callIO = call(io)
const callEither = call(either)

const ok = <A>(a: A) => either.of<Error, A>(a)
const ko = <A>(e: Error) => left<Error, A>(e)

const a = callIO(io.of('foo'), io.of(2))(tuple) // IO<[string, number]>
const b = callEither(ok('foo'), ok(2))(tuple) // Either<Error, [string, number]>
const c = callEither(ok('foo'), ko(Error('Failed')))(tuple)

a.chain(log).run() // ['foo', 2]
b.fold(log, log).run() // ['foo', 2]
c.fold(log, log).run() // Error('Failed')

All 11 comments

Apologies for the mixed use of any/unknown - should we implement, defaults should always use unknown

@christianbradley which typescript version are you using? Line 47

export function call<F extends UnknownApply>(

raises the following error

Overload signature is not compatible with function implementation.ts(2394)

with [email protected] and [email protected]

Currently using 3.3.4 on my end - here's my tsconfig.json

{
    "compilerOptions": {
        "target": "es2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
        "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
        "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
        /* Strict Type-Checking Options */
        "strict": true /* Enable all strict type-checking options. */,
        "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
        "strictNullChecks": true /* Enable strict null checks. */,
        "strictFunctionTypes": true /* Enable strict checking of function types. */,
        "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
        "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
        "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
        "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,

        /* Additional Checks */
        "noUnusedLocals": true /* Report errors on unused locals. */,
        "noUnusedParameters": true /* Report errors on unused parameters. */,
        "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
        "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,

        /* Module Resolution Options */
        "esModuleInterop": false /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    }
}

Also, using [email protected]

Here's a quick rewrite - found a couple inconsistencies, and corrected unnecessary uses of any
Let me know if this works.

import { Apply, Apply1, Apply2, Apply2C, Apply3, Apply3C, sequenceT } from 'fp-ts/lib/Apply'
import { log } from 'fp-ts/lib/Console'
import { Either, either } from 'fp-ts/lib/Either'
import { tuple } from 'fp-ts/lib/function'
import { HKT, Type, Type2, Type3, URIS, URIS2, URIS3 } from 'fp-ts/lib/HKT'
import { io } from 'fp-ts/lib/IO'

export type URIOf<F> = F extends { URI: infer A } ? A : never

export type UnknownApply<L = unknown, U = unknown> =
    | { [URI in URIS]: Apply1<URI> }[URIS]
    | { [URI in URIS2]: Apply2<URI> }[URIS2]
    | { [URI in URIS2]: Apply2C<URI, L> }[URIS2]
    | { [URI in URIS3]: Apply3<URI> }[URIS3]
    | { [URI in URIS3]: Apply3C<URI, U, L> }[URIS3]
    | Apply<unknown>

export type TypeOfApply<T, A, L = unknown, U = unknown> = T extends Apply1<infer URI>
    ? Type<URI, A>
    : T extends Apply2<infer URI>
    ? Type2<URI, L, A>
    : T extends Apply2C<infer URI, infer L2>
    ? Type2<URI, L2, A>
    : T extends Apply3<infer URI>
    ? Type3<URI, U, L, A>
    : T extends Apply3C<infer URI, infer U2, infer L2>
    ? Type3<URI, U2, L2, A>
    : T extends Apply<infer URI>
    ? HKT<URI, A>
    : never

export type AOf<T> = T extends { _A: infer A } ? A : never
export type MapAOf<T extends Array<unknown>> = { [k in keyof T]: AOf<T[k]> }
export type LOf<T> = T extends { _L: infer L } ? L : never
export type UOf<T> = T extends { _U: infer U } ? U : never

export type AOfArray<T extends unknown[]> = T extends Array<infer A> ? A : never

export function call<F extends UnknownApply>(
    F: F
): <TS extends TypeOfApply<F, unknown>[]>(
    ...fs: TS & { 0: TypeOfApply<F, unknown> }
) => <B>(f: (...as: MapAOf<TS>) => B) => TypeOfApply<F, B, LOf<AOfArray<TS>>, UOf<AOfArray<TS>>>
export function call<F extends Apply<any>>(F: F) {
    return <TS extends HKT<URIOf<F>, any>[]>(...ts: TS & { 0: HKT<URIOf<F>, any> }) => <B>(
        f: (...as: MapAOf<TS>) => B
    ): HKT<URIOf<F>, B> => F.map(sequenceT(F)(...(ts as any)), as => f(...(as as any)))
}

const callIO = call(io)
const callEither = call(either)

const ok = <A>(a: A): Either<Error, A> => either.of<Error, A>(a)

const a = callIO(io.of('foo'), io.of(2))(tuple) // IO<[string, number]>
const b = callEither(ok('foo'), ok(2))(tuple) // Either<Error, [string, number]>

a.chain(log).run() // ['foo', 2]
b.fold(log, log).run() // ['foo', 2]

@christianbradley ok thanks, starting with a clean project doesn't give me any error, let me investigate..

@christianbradley I got it, in the project where I first tried your snippet I have the following declaration

declare module 'fp-ts/lib/HKT' {
  interface URI2HKT<A> {
    Response: Response<A>
  }
}

export interface Response<A> {
  readonly url: string
  readonly status: number
  readonly headers: Record<string, string>
  readonly body: A
}

and looks like your implementation relies on the presence of the _A, _URI fields

Here's a rewrite with two updates:

1 - TypeParamsOf avoids reliance upon _A, _URI - uses inference from all known types

2 - By passing required first param, allow inference of L, U, X etc from usage - this is really helpful as the current implementation does not do this, and manually passing the type parameters requires passing the full args type as well

I added sequenceTv2 as an example as well

import { Apply, Apply1, Apply2, Apply2C, Apply3, Apply3C, sequenceT } from 'fp-ts/lib/Apply'
import { log } from 'fp-ts/lib/Console'
import { either, left } from 'fp-ts/lib/Either'
import { tuple } from 'fp-ts/lib/function'
import { HKT, HKT2, HKT3, HKT4, Type, Type2, Type3, Type4, URIS, URIS2, URIS3 } from 'fp-ts/lib/HKT'
import { io } from 'fp-ts/lib/IO'

export type TypeParamsOf<T> = T extends Type<infer URI, infer A>
    ? [URI, A, never, never, never]
    : T extends Type2<infer URI, infer L, infer A>
    ? [URI, A, L, never, never]
    : T extends Type3<infer URI, infer U, infer L, infer A>
    ? [URI, A, L, U, never]
    : T extends Type4<infer URI, infer X, infer U, infer L, infer A>
    ? [URI, A, L, U, X]
    : T extends HKT<infer URI, infer A>
    ? [URI, A, never, never, never]
    : T extends HKT2<infer URI, infer L, infer A>
    ? [URI, A, L, never, never]
    : T extends HKT3<infer URI, infer U, infer L, infer A>
    ? [URI, A, L, U, never]
    : T extends HKT4<infer URI, infer X, infer U, infer L, infer A>
    ? [URI, A, L, U, X]
    : [never, never, never, never, never]

export type URIOf<T> = TypeParamsOf<T>[0]
export type AOf<T> = TypeParamsOf<T>[1]
export type LOf<T> = TypeParamsOf<T>[2]
export type UOf<T> = TypeParamsOf<T>[3]
export type XOf<T> = TypeParamsOf<T>[4]

export type UnknownApply<L = unknown, U = unknown> =
    | { [URI in URIS]: Apply1<URI> }[URIS]
    | { [URI in URIS2]: Apply2<URI> }[URIS2]
    | { [URI in URIS2]: Apply2C<URI, L> }[URIS2]
    | { [URI in URIS3]: Apply3<URI> }[URIS3]
    | { [URI in URIS3]: Apply3C<URI, U, L> }[URIS3]
    | Apply<unknown>

export type TypeOfApply<T, A, L = unknown, U = unknown> = T extends Apply1<infer URI>
    ? Type<URI, A>
    : T extends Apply2<infer URI>
    ? Type2<URI, L, A>
    : T extends Apply2C<infer URI, infer L2>
    ? Type2<URI, L2, A>
    : T extends Apply3<infer URI>
    ? Type3<URI, U, L, A>
    : T extends Apply3C<infer URI, infer U2, infer L2>
    ? Type3<URI, U2, L2, A>
    : T extends Apply<infer URI>
    ? HKT<URI, A>
    : never

export type MapAOf<T> = { [k in keyof T]: AOf<T[k]> }
export type AOfArray<T extends unknown[]> = T extends Array<infer A> ? A : never
export type DomainOf<T extends Function> = T extends (...args: infer T) => any ? T : never

export function sequenceTv2<F extends UnknownApply>(
    F: F
): <TS extends TypeOfApply<F, any, L, U>[], A, L, U>(
    fa: TypeOfApply<F, A, L, U>,
    ...ts: TS
) => TypeOfApply<F, DomainOf<(a: A, ...rest: MapAOf<TS>) => any>, L, U>
export function sequenceTv2(F: Apply<unknown>) {
    return <TS extends HKT<any, A>[], A>(fa: HKT<any, A>, ...ts: TS) => sequenceT(F)(fa, ...ts)
}

export function call<F extends UnknownApply>(
    F: F
): <TS extends TypeOfApply<F, any, L, U>[], A, L, U>(
    fa: TypeOfApply<F, A, L, U>,
    ...ts: TS
) => <B>(f: (a: A, ...rest: MapAOf<TS>) => B) => TypeOfApply<F, B, L, A>
export function call(F: Apply<unknown>) {
    return <TS extends HKT<any, any>[], A>(fa: HKT<any, A>, ...ts: TS) => <B>(
        f: (a: A, ...rest: any[]) => B
    ) => F.map(sequenceTv2(F)(fa, ...ts), ([a, ...rest]) => f(a, ...rest))
}

const callIO = call(io)
const callEither = call(either)

const ok = <A>(a: A) => either.of<Error, A>(a)
const ko = <A>(e: Error) => left<Error, A>(e)

const a = callIO(io.of('foo'), io.of(2))(tuple) // IO<[string, number]>
const b = callEither(ok('foo'), ok(2))(tuple) // Either<Error, [string, number]>
const c = callEither(ok('foo'), ko(Error('Failed')))(tuple)

a.chain(log).run() // ['foo', 2]
b.fold(log, log).run() // ['foo', 2]
c.fold(log, log).run() // Error('Failed')

@christianbradley Awesome!

I think I've found a better way - here's an example of rewriting a generic lift for Functor:

import { URIS, URIS2, URIS3, URIS4, Type4, Type3, Type2, Type, HKT } from 'fp-ts/lib/HKT'
import {
    Functor1,
    Functor2,
    Functor2C,
    Functor3C,
    Functor3,
    Functor4,
    Functor
} from 'fp-ts/lib/Functor'
import { io } from 'fp-ts/lib/IO'
import { either, left } from 'fp-ts/lib/Either'

export type AnyFunctor<U = any, L = any> =
    | { [URI in URIS]: Functor1<URI> }[URIS]
    | { [URI in URIS2]: Functor2<URI> | Functor2C<URI, L> }[URIS2]
    | { [URI in URIS3]: Functor3<URI> | Functor3C<URI, U, L> }[URIS3]
    | { [URI in URIS4]: Functor4<URI> }[URIS4]
    | Functor<any>

export type TypeOfFunctor<F extends AnyFunctor, X, U, L, A> = F extends Functor4<infer URI>
    ? Type4<URI, X, U, L, A>
    : F extends Functor3C<infer URI, infer U, infer L>
    ? Type3<URI, U, L, A>
    : F extends Functor3<infer URI>
    ? Type3<URI, U, L, A>
    : F extends Functor2C<infer URI, infer L>
    ? Type2<URI, L, A>
    : F extends Functor2<infer URI>
    ? Type2<URI, L, A>
    : F extends Functor1<infer URI>
    ? Type<URI, A>
    : F extends Functor<infer URI>
    ? HKT<URI, A>
    : HKT<unknown, A>

export interface Lift<F extends AnyFunctor> {
    <A, B>(f: (a: A) => B): <X, U, L>(
        fa: TypeOfFunctor<F, X, U, L, A>
    ) => TypeOfFunctor<F, X, U, L, B>
}

export function lift<F extends AnyFunctor>(F: F): Lift<F>
export function lift<URI>(F: Functor<URI>): Lift<Functor<URI>> {
    return f => fa => F.map(fa, f)
}

// examples
const length = (s: string) => s.length
const liftIO = lift(io)
const liftEither = lift(either)

const a = liftIO(length)(io.of('foo')) // IO<number>
const b = liftEither(length)(either.of<Error, string>('foo')) // Either<Error, number>
const c = liftEither(length)(left<Error, string>(Error('Failure'))) // Either<Error, number>

a.map(console.log).run() // 3
b.fold(console.error, console.log) // 3
c.fold(console.error, console.log) // Error: Failure

Started a branch in my fork (https://github.com/christianbradley/fp-ts/tree/typeclass-boilerplate) - here's a commit showing type helper types and refactoring of Functor#lift: https://github.com/christianbradley/fp-ts/commit/f27c4037179e431732093276819c12d5b37444cd#diff-42e265124f3b2ebf67776699cb8814d1

The types needed at minimum are TypeOfFunctor, AnyFunctor, and CaseOfFunctor. Same can be said for Apply, etc. This is what #lift looks like:

export type Lifted<F extends AnyFunctor, A, B> = CaseOfFunctor<
  F,
  <X, U, L>(fa: TypeOfFunctor<F, X, U, L, A>) => TypeOfFunctor<F, X, U, L, B>,
  <U, L>(fa: TypeOfFunctor<F, never, U, L, A>) => TypeOfFunctor<F, never, U, L, B>,
  <L>(fa: TypeOfFunctor<F, never, never, L, A>) => TypeOfFunctor<F, never, never, L, B>,
  (fa: TypeOfFunctor<F, never, never, never, A>) => TypeOfFunctor<F, never, never, never, B>
>

export type Lift<F extends AnyFunctor> = <A, B>(f: (a: A) => B) => Lifted<F, A, B>

export function lift<F extends AnyFunctor>(F: F): Lift<F>
export function lift<URI>(F: Functor<URI>): Lift<Functor<URI>> {
  return f => fa => F.map(fa, f)
}

@gcanti - any thoughts? If we can lock down the type names etc I don't mind going ahead and knocking this out and sending a pull request.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

steida picture steida  路  4Comments

Crashthatch picture Crashthatch  路  4Comments

gcanti picture gcanti  路  3Comments

amaurymartiny picture amaurymartiny  路  4Comments

josete89 picture josete89  路  3Comments