Skip to content

Better encoding for HKTs #1208

@ENvironmentSet

Description

@ENvironmentSet

🚀 Feature request

Current Behavior

Currently, we're using declaration merging and type level defunctionalization to simulate HKTs.
They did their job well, but there was fundamental problem.

They're too verbose to use!

When we define some data type that's constructor is HKT, we need to define URI for it and extend proper URItoKind interface by declaration merging. this is not only boring work but also producing unreadable code.

fp-ts/src/Option.ts

Lines 51 to 65 in e708323

declare module './HKT' {
interface URItoKind<A> {
readonly Option: Option<A>
}
}
/**
* @since 2.0.0
*/
export const URI = 'Option'
/**
* @since 2.0.0
*/
export type URI = typeof URI

This is not only problem about data types, but also type classes.
Actually, It's even worse in case of type classes.

fp-ts/src/Functor.ts

Lines 19 to 163 in e708323

export interface Functor<F> {
readonly URI: F
readonly map: <A, B>(fa: HKT<F, A>, f: (a: A) => B) => HKT<F, B>
}
/**
* @since 2.0.0
*/
export interface Functor1<F extends URIS> {
readonly URI: F
readonly map: <A, B>(fa: Kind<F, A>, f: (a: A) => B) => Kind<F, B>
}
/**
* @since 2.0.0
*/
export interface Functor2<F extends URIS2> {
readonly URI: F
readonly map: <E, A, B>(fa: Kind2<F, E, A>, f: (a: A) => B) => Kind2<F, E, B>
}
/**
* @since 2.0.0
*/
export interface Functor2C<F extends URIS2, E> {
readonly URI: F
readonly _E: E
readonly map: <A, B>(fa: Kind2<F, E, A>, f: (a: A) => B) => Kind2<F, E, B>
}
/**
* @since 2.0.0
*/
export interface Functor3<F extends URIS3> {
readonly URI: F
readonly map: <R, E, A, B>(fa: Kind3<F, R, E, A>, f: (a: A) => B) => Kind3<F, R, E, B>
}
/**
* @since 2.2.0
*/
export interface Functor3C<F extends URIS3, E> {
readonly URI: F
readonly _E: E
readonly map: <R, A, B>(fa: Kind3<F, R, E, A>, f: (a: A) => B) => Kind3<F, R, E, B>
}
/**
* @since 2.0.0
*/
export interface Functor4<F extends URIS4> {
readonly URI: F
readonly map: <S, R, E, A, B>(fa: Kind4<F, S, R, E, A>, f: (a: A) => B) => Kind4<F, S, R, E, B>
}
/**
* @since 2.0.0
*/
export interface FunctorComposition<F, G> {
readonly map: <A, B>(fa: HKT<F, HKT<G, A>>, f: (a: A) => B) => HKT<F, HKT<G, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorCompositionHKT1<F, G extends URIS> {
readonly map: <A, B>(fa: HKT<F, Kind<G, A>>, f: (a: A) => B) => HKT<F, Kind<G, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorCompositionHKT2<F, G extends URIS2> {
readonly map: <E, A, B>(fa: HKT<F, Kind2<G, E, A>>, f: (a: A) => B) => HKT<F, Kind2<G, E, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorCompositionHKT2C<F, G extends URIS2, E> {
readonly map: <A, B>(fa: HKT<F, Kind2<G, E, A>>, f: (a: A) => B) => HKT<F, Kind2<G, E, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorComposition11<F extends URIS, G extends URIS> {
readonly map: <A, B>(fa: Kind<F, Kind<G, A>>, f: (a: A) => B) => Kind<F, Kind<G, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorComposition12<F extends URIS, G extends URIS2> {
readonly map: <E, A, B>(fa: Kind<F, Kind2<G, E, A>>, f: (a: A) => B) => Kind<F, Kind2<G, E, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorComposition12C<F extends URIS, G extends URIS2, E> {
readonly map: <A, B>(fa: Kind<F, Kind2<G, E, A>>, f: (a: A) => B) => Kind<F, Kind2<G, E, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorComposition21<F extends URIS2, G extends URIS> {
readonly map: <E, A, B>(fa: Kind2<F, E, Kind<G, A>>, f: (a: A) => B) => Kind2<F, E, Kind<G, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorComposition2C1<F extends URIS2, G extends URIS, E> {
readonly map: <A, B>(fa: Kind2<F, E, Kind<G, A>>, f: (a: A) => B) => Kind2<F, E, Kind<G, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorComposition22<F extends URIS2, G extends URIS2> {
readonly map: <FE, GE, A, B>(fa: Kind2<F, FE, Kind2<G, GE, A>>, f: (a: A) => B) => Kind2<F, FE, Kind2<G, GE, B>>
}
/**
* @since 2.0.0
*/
export interface FunctorComposition22C<F extends URIS2, G extends URIS2, E> {
readonly map: <FE, A, B>(fa: Kind2<F, FE, Kind2<G, E, A>>, f: (a: A) => B) => Kind2<F, FE, Kind2<G, E, B>>
}
/**
* @since 2.2.0
*/
export interface FunctorComposition23<F extends URIS2, G extends URIS3> {
readonly map: <FE, R, E, A, B>(fa: Kind2<F, FE, Kind3<G, R, E, A>>, f: (a: A) => B) => Kind2<F, FE, Kind3<G, R, E, B>>
}
/**
* @since 2.2.0
*/
export interface FunctorComposition23C<F extends URIS2, G extends URIS3, E> {
readonly map: <FE, R, A, B>(fa: Kind2<F, FE, Kind3<G, R, E, A>>, f: (a: A) => B) => Kind2<F, FE, Kind3<G, R, E, B>>
}

Why those things happens?
Well, IMHO, I think this problem has been caused from two property of current way of encoding HKTs.

  1. HKTs are separated based on their number of type parameters.

So we must write definition for all HKTs separately, by using something like function overloading(which makes code long, and verbose), instead of writing definition at once.

fp-ts/src/StateT.ts

Lines 164 to 185 in e708323

export function getStateM<M extends URIS3>(M: Monad3<M>): StateM3<M>
export function getStateM<M extends URIS3, E>(M: Monad3C<M, E>): StateM3C<M, E>
export function getStateM<M extends URIS2>(M: Monad2<M>): StateM2<M>
export function getStateM<M extends URIS2, E>(M: Monad2C<M, E>): StateM2C<M, E>
export function getStateM<M extends URIS>(M: Monad1<M>): StateM1<M>
export function getStateM<M>(M: Monad<M>): StateM<M>
export function getStateM<M>(M: Monad<M>): StateM<M> {
return {
map: (fa, f) => (s) => M.map(fa(s), ([a, s1]) => [f(a), s1]),
of: (a) => (s) => M.of([a, s]),
ap: (fab, fa) => (s) => M.chain(fab(s), ([f, s]) => M.map(fa(s), ([a, s]) => [f(a), s])),
chain: (fa, f) => (s) => M.chain(fa(s), ([a, s1]) => f(a)(s1)),
get: () => (s) => M.of([s, s]),
put: (s) => () => M.of([undefined, s]),
modify: (f) => (s) => M.of([undefined, f(s)]),
gets: (f) => (s) => M.of([f(s), s]),
fromState: (sa) => (s) => M.of(sa(s)),
fromM: (ma) => (s) => M.map(ma, (a) => [a, s]),
evalState: (ma, s) => M.map(ma(s), ([a]) => a),
execState: (ma, s) => M.map(ma(s), ([_, s]) => s)
}
}

  1. It's based on declaration merging & defunctionalization

Fundamental idea of current way of simulating HKTs is to express direct reference to HKTs by indirect reference(by using URI & URItoKind). As well as we choose to go around, it's natural to be suffered from boilerplate codes.

Desired Behavior

What I want is simpler & easier & readable encoding of HKTs.
And to break down current limitation of this way of encoding HKTs(ex: It's hard to write HKTs that takes HKTs)

This will make this library more practical.

Suggested Solution

I've found some interesting trick that could be used to encoding HKTs,

How about using this trick to encoding HKTs?
Indeed we need more research and investigations about this(ex: is this safe to use?, is there any limitation?, is there better way of using this trick? what else this trick can do?) but I think discussing about this would be valuable.

Who does this impact? Who is this for?

All of fp-ts users.

Additional context

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions