gcanti / fp-ts

Functional programming in TypeScript
https://gcanti.github.io/fp-ts/
MIT License
10.74k stars 503 forks source link

Compositions & pipe #925

Closed pbadenski closed 4 years ago

pbadenski commented 5 years ago

I looked in both documentation and issues, but couldn't find any information on this...

Is it possible (right now or planned as a feature) to make composition "pipe-friendly" with a simple syntax?

Currently *Composition signatures cannot be nicely piped without introducing a function wrapper. For example in case of a OptionT monad transformer below:

const optionArray = optionT.getOptionM(array.array);

pipe(
  optionArray.of(1),
  m => optionArray.map(m, v => v + 1)
  // how can you make below possible?
  // optionArray.map(v => v + 1)
)
gcanti commented 5 years ago
import * as array from 'fp-ts/lib/Array'
import { Monad1 } from 'fp-ts/lib/Monad'
import { Option, some, none } from 'fp-ts/lib/Option'
import * as optionT from 'fp-ts/lib/OptionT'
import { pipe, pipeable } from 'fp-ts/lib/pipeable'

// a Monad instance for `Array<Option<A>>`...
declare module 'fp-ts/lib/HKT' {
  interface URItoKind<A> {
    ArrayOption: Array<Option<A>>
  }
}

const arrayOption: Monad1<'ArrayOption'> = {
  URI: 'ArrayOption',
  ...optionT.getOptionM(array.array)
}

// ...getting pipeable functions...
const { ap, apFirst, apSecond, chain, chainFirst, flatten, map } = pipeable(arrayOption)

// ...usage
pipe(
  arrayOption.of(1),
  map(v => v + 1),
  chain(n => (n > 0 ? [some(n + 1), some(n - 1)] : [none]))
)
pbadenski commented 5 years ago

Thanks! I was worried that was the answer.. Out of curiosity do you think there's TypeScript features coming any time soon that could make it less verbose?

raveclassic commented 5 years ago

@gcanti Shouldn't we reconsider changing typeclasses signatures to be always data-last? I have exactly the same problem right now trying to write tests for new RemoteDataT transformer and it's quite weird for tests to pollute the source with globally registered monad instance only to make pipe work

raveclassic commented 5 years ago

Just played a bit, looks nice:

import { HKT, Kind, URIS } from 'fp-ts/lib/HKT';
import { pipe } from 'fp-ts/lib/pipeable';

///////
export interface Functor<F> {
    readonly URI: F;
    readonly map: <A, B>(f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B>;
}

export interface Functor1<F extends URIS> {
    readonly URI: F;
    readonly map: <A, B>(f: (a: A) => B) => (fa: Kind<F, A>) => Kind<F, B>;
}
///////

export interface Nothing {
    readonly _tag: 'Nothing';
}

export const nothing: Maybe<never> = {
    _tag: 'Nothing',
};

export interface Just<A> {
    readonly _tag: 'Just';
    readonly value: A;
}

export const just = <A>(value: A): Maybe<A> => ({
    _tag: 'Just',
    value,
});

export type Maybe<A> = Nothing | Just<A>;

declare module 'fp-ts/lib/HKT' {
    interface URItoKind<A> {
        Maybe: Maybe<A>;
    }
}

export const maybe: Functor1<'Maybe'> = {
    URI: 'Maybe',
    map: f => fa => (fa._tag === 'Just' ? just(f(fa.value)) : nothing),
};

//////////

export type MaybeT<M, A> = HKT<M, Maybe<A>>;
export type MaybeT1<M extends URIS, A> = Kind<M, Maybe<A>>;

export interface MaybeM<M> {
    map: <A, B>(f: (a: A) => B) => (ma: MaybeT<M, A>) => MaybeT<M, B>;
}

export interface MaybeM1<M extends URIS> {
    map: <A, B>(f: (a: A) => B) => (ma: MaybeT1<M, A>) => MaybeT1<M, A>;
}

export function getMaybeM<M extends URIS>(M: Functor1<M>): MaybeM1<M>;
export function getMaybeM<M>(M: Functor<M>): MaybeM<M>;
export function getMaybeM<M>(M: Functor<M>): MaybeM<M> {
    return {
        map: f => M.map(maybe.map(f)),
    };
}

////////

export type Box<A> = [A];
declare module 'fp-ts/lib/HKT' {
    interface URItoKind<A> {
        Box: Box<A>;
    }
}
export const box: Functor1<'Box'> = {
    URI: 'Box',
    map: f => fa => [f(fa[0])],
};

//////////

const M = getMaybeM(box);
const inc = (n: number) => n + 1;
const test1: Maybe<number> = pipe(
    just(123),
    maybe.map(inc),
);
const test2: Box<Maybe<number>> = pipe(
    [just(123)],
    M.map(inc),
);
OliverJAsh commented 4 years ago

How would you make compositions like traverse pipeable?

import * as O from 'fp-ts/lib/Option';
import * as T from 'fp-ts/lib/Task';
O.option.traverse(T.task);
mlegenhausen commented 4 years ago

@OliverJAsh @gcanti already tried https://github.com/gcanti/fp-ts/issues/823#issuecomment-504086813

raveclassic commented 4 years ago

@OliverJAsh Easily (if typeclasses were curried data-last):

import { HKT, Kind, URIS } from 'fp-ts/lib/HKT';
import { pipe } from 'fp-ts/lib/pipeable';

///////
export interface Functor<F> {
    readonly URI: F;
    readonly map: <A, B>(f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B>;
}
export interface Functor1<F extends URIS> {
    readonly URI: F;
    readonly map: <A, B>(f: (a: A) => B) => (fa: Kind<F, A>) => Kind<F, B>;
}
export interface Apply<F> extends Functor<F> {
    readonly ap: <A, B>(fab: HKT<F, (a: A) => B>) => (fa: HKT<F, A>) => HKT<F, B>;
}
export interface Apply1<F extends URIS> extends Functor1<F> {
    readonly ap: <A, B>(fab: Kind<F, (a: A) => B>) => (fa: Kind<F, A>) => Kind<F, B>;
}
export interface Applicative<F> extends Apply<F> {
    readonly of: <A>(a: A) => HKT<F, A>;
}
export interface Applicative1<F extends URIS> extends Apply1<F> {
    readonly of: <A>(a: A) => Kind<F, A>;
}
export interface Traverse<T> {
    <F extends URIS>(F: Applicative1<F>): <A, B>(f: (a: A) => Kind<F, B>) => (ta: HKT<T, A>) => Kind<F, HKT<T, B>>;
    <F>(F: Applicative<F>): <A, B>(f: (a: A) => HKT<F, B>) => (ta: HKT<T, A>) => HKT<F, HKT<T, B>>;
}
export interface Traverse1<T extends URIS> {
    <F extends URIS>(F: Applicative1<F>): <A, B>(f: (a: A) => Kind<F, B>) => (ta: Kind<T, A>) => Kind<F, Kind<T, B>>;
    <F>(F: Applicative<F>): <A, B>(f: (a: A) => HKT<F, B>) => (ta: Kind<T, A>) => HKT<F, Kind<T, B>>;
}
export interface Traversable<T> extends Functor<T> {
    readonly traverse: Traverse<T>;
}
export interface Traversable1<T extends URIS> extends Functor1<T> {
    readonly traverse: Traverse1<T>;
}

///////

export interface Nothing {
    readonly _tag: 'Nothing';
}

export const nothing: Maybe<never> = {
    _tag: 'Nothing',
};

export interface Just<A> {
    readonly _tag: 'Just';
    readonly value: A;
}

export const just = <A>(value: A): Maybe<A> => ({
    _tag: 'Just',
    value,
});

export type Maybe<A> = Nothing | Just<A>;

declare module 'fp-ts/lib/HKT' {
    interface URItoKind<A> {
        Maybe: Maybe<A>;
    }
}

export const maybe: Applicative1<'Maybe'> & Traversable1<'Maybe'> = {
    URI: 'Maybe',
    map: f => fa => (fa._tag === 'Just' ? just(f(fa.value)) : nothing),
    ap: fab => fa => (fab._tag === 'Nothing' ? nothing : fa._tag === 'Nothing' ? nothing : just(fab.value(fa.value))),
    of: just,
    traverse: <F>(
        F: Applicative<F>,
    ): (<A, B>(f: (a: A) => HKT<F, B>) => (ta: Maybe<A>) => HKT<F, Maybe<B>>) => f => ta =>
        ta._tag === 'Nothing'
            ? F.of(nothing)
            : pipe(
                    ta.value,
                    f,
                    F.map(just),
              ),
};

////////

export type Box<A> = [A];
declare module 'fp-ts/lib/HKT' {
    interface URItoKind<A> {
        Box: Box<A>;
    }
}
export const box: Applicative1<'Box'> & Traversable1<'Box'> = {
    URI: 'Box',
    map: f => fa => [f(fa[0])],
    ap: fab => fa => [fab[0](fa[0])],
    of: a => [a],
    traverse: <F>(F: Applicative<F>): (<A, B>(f: (a: A) => HKT<F, B>) => (ta: Box<A>) => HKT<F, Box<B>>) => f => ta =>
        pipe(
            ta[0],
            f,
            F.map(v => [v]),
        ),
};

//////////

const test: Box<Maybe<number>> = pipe(
    just(123),
    maybe.traverse(box)(n => [n * 2]),
);
const test2: Maybe<Box<number>> = pipe(
    [123],
    box.traverse(maybe)(n => just(n * 2)),
);
raveclassic commented 4 years ago

@gcanti Why closing? Do you have any considerations about moving to data-last at least in the next major? Registering any monad transformer or composition factory output in HKT registry only to fit pipeable (which is also incapable of adding any extra helpers outside the lib core) is terrible. And what about pipeable traversable?

gcanti commented 4 years ago

tagged as a reminder