gcanti / fp-ts

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

More real world usage examples in the document #1076

Open civilizeddev opened 4 years ago

civilizeddev commented 4 years ago

🚀 Feature request

I'm sorry I'm beginner.

To get familiar with fp-ts, I'm trying to convert and rewrite some tagless-final examples written in Scala into one using fp-ts. (Here is an example: https://github.com/amitayh/functional-testing-tagless-final)

I have not succeed with it yet. Some features in fp-ts are still confusing to me.

Current Behavior

Desired Behavior

Suggested Solution

Who does this impact? Who is this for?

For beginners who just get interested in fp-ts.

Describe alternatives you've considered

Add usage example to each item in the modules.

getReaderM (function)

Add explanatory description here...

Signature

export function getReaderM<M>(M: Monad<M>): ReaderM<M> { ... }

Usage
import * as RT from 'fp-ts/lib/ReaderT'
import * as R from 'fp-ts/lib/Reader'

// Describe what it is doing...
RT.getReaderM(R.Reader).fromReader(env => ...)

Additional context

N/A

Your environment

Software Version(s)
fp-ts 2.3.1
TypeScript 3.7.4
raveclassic commented 4 years ago

I played a bit with https://github.com/amitayh/functional-testing-tagless-final and ported the first example, take a look:

// package.ts
import { eq } from 'fp-ts';
import { pipe } from 'fp-ts/lib/pipeable';

export class UserId {
    constructor(readonly id: string) {}
}
export const eqUserId: eq.Eq<UserId> = pipe(
    eq.eqString,
    eq.contramap(userId => userId.id),
);

export class OrderId {
    constructor(readonly id: string) {}
}

export class UserProfile {
    constructor(readonly userId: UserId, readonly userName: string) {}
}

export class Order {
    constructor(readonly userId: UserId, readonly orderId: OrderId) {}
}

export class UserInformation {
    constructor(readonly userName: string, readonly orders: Order[]) {}

    static from(profile: UserProfile, orders: Order[]): UserInformation {
        return new UserInformation(profile.userName, orders);
    }
}
// algebras.ts
import { Order, OrderId, UserId, UserProfile } from './package';
import { HKT, Kind, URIS } from 'fp-ts/lib/HKT';
import { monadThrow } from 'fp-ts';

export class UserNotFound extends Error {
    constructor(userId: UserId) {
        super(`User with ID ${userId.id} does not exist`);
    }
}

export interface Users<F> {
    readonly profileFor: (userId: UserId) => HKT<F, UserProfile>;
}
export interface Users1<F extends URIS> {
    readonly profileFor: (userId: UserId) => Kind<F, UserProfile>;
}

export interface Orders<F> {
    readonly ordersFor: (orderId: OrderId) => HKT<F, Order[]>;
}
export interface Orders1<F extends URIS> {
    readonly ordersFor: (orderId: OrderId) => Kind<F, Order[]>;
}

export interface Logging<F> {
    readonly error: (e: Error) => HKT<F, void>;
}
export interface Logging1<F extends URIS> {
    readonly error: (e: Error) => Kind<F, void>;
}

export interface MonadThrowable<F> extends monadThrow.MonadThrow<F> {
    readonly onError: <A>(fa: HKT<F, A>, f: (e: Error) => HKT<F, void>) => HKT<F, A>;
}
export interface MonadThrowable1<F extends URIS> extends monadThrow.MonadThrow1<F> {
    readonly onError: <A>(fa: Kind<F, A>, f: (e: Error) => Kind<F, void>) => Kind<F, A>;
}
// fetch-user-information.ts
import { Logging, Logging1, MonadThrowable, MonadThrowable1, Orders, Orders1, Users, Users1 } from './algebras';
import { UserId, UserInformation } from './package';
import { HKT, Kind, URIS } from 'fp-ts/lib/HKT';
import { pipe, pipeable } from 'fp-ts/lib/pipeable';

export function fetchUserInformation<M extends URIS>(
    M: MonadThrowable1<M> & Users1<M> & Orders1<M> & Logging1<M>,
): (userId: UserId) => Kind<M, UserInformation>;
export function fetchUserInformation<M>(
    M: MonadThrowable<M> & Users<M> & Orders<M> & Logging<M>,
): (userId: UserId) => HKT<M, UserInformation>;
export function fetchUserInformation<M>(
    M: MonadThrowable<M> & Users<M> & Orders<M> & Logging<M>,
): (userId: UserId) => HKT<M, UserInformation> {
    const MP = pipeable(M);
    return userId =>
        pipe(
            M.profileFor(userId),
            MP.chain(profile =>
                pipe(
                    M.ordersFor(userId),
                    MP.map(orders => UserInformation.from(profile, orders)),
                ),
            ),
            ma => M.onError(ma, M.error),
        );
}
// test-env.ts
import { eqUserId, Order, OrderId, UserId, UserProfile } from './package';
import { array, map, option } from 'fp-ts';
import { pipe } from 'fp-ts/lib/pipeable';

export const insertAtUserId = map.insertAt(eqUserId);
export const lookupWithUserId = map.lookup(eqUserId);

export class TestEnv {
    static readonly empty: TestEnv = new TestEnv(map.empty, map.empty, array.empty);

    constructor(
        readonly profiles: Map<UserId, UserProfile>,
        readonly orders: Map<OrderId, Order[]>,
        readonly loggedErrors: Error[],
    ) {}

    withProfile(profile: UserProfile): TestEnv {
        return new TestEnv(
            pipe(this.profiles, insertAtUserId(profile.userId, profile)),
            this.orders,
            this.loggedErrors,
        );
    }

    withOrder(order: Order): TestEnv {
        const updatedUserOrders = [order, ...this.userOrders(order.userId)];
        return new TestEnv(
            this.profiles,
            pipe(this.orders, insertAtUserId(order.userId, updatedUserOrders)),
            this.loggedErrors,
        );
    }

    userOrders(userId: UserId): Order[] {
        return pipe(
            lookupWithUserId(userId, this.orders),
            option.getOrElse<Order[]>(() => array.empty),
        );
    }

    logError(e: Error): TestEnv {
        return new TestEnv(this.profiles, this.orders, [e, ...this.loggedErrors]);
    }
}
// user-information.spec.ts
import { lookupWithUserId, TestEnv } from './test-env';
import { Logging1, MonadThrowable1, Orders1, UserNotFound, Users1 } from './algebras';
import { either, option, stateT } from 'fp-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { Order, OrderId, UserId, UserInformation, UserProfile } from './package';
import { fetchUserInformation } from './fetch-user-information';

type EitherThrowableOr<A> = either.Either<Error, A>;
const eitherThrowableOr: MonadThrowable1<'EitherThrowableOr'> = {
    ...either.either,
    URI: 'EitherThrowableOr',
    throwError: e => either.left(either.toError(e)),
    onError: (fa, f) => {
        if (either.isLeft(fa)) {
            f(fa.left);
        }
        return fa;
    },
};
type Test<A> = stateT.StateT1<'EitherThrowableOr', TestEnv, A>;
declare module 'fp-ts/lib/HKT' {
    interface URItoKind<A> {
        EitherThrowableOr: EitherThrowableOr<A>;
        Test: Test<A>;
    }
}

const test: stateT.StateM1<'EitherThrowableOr'> &
    MonadThrowable1<'Test'> &
    Users1<'Test'> &
    Orders1<'Test'> &
    Logging1<'Test'> = {
    URI: 'Test',
    ...stateT.getStateM(eitherThrowableOr),
    throwError: e => test.fromM(eitherThrowableOr.throwError(e)),
    onError: (fa, f) => s => {
        const e = fa(s);
        if (either.isLeft(e)) {
            f(e.left);
        }
        return e;
    },
    profileFor: userId => env =>
        pipe(
            lookupWithUserId(userId, env.profiles),
            option.fold(
                () => either.left(new UserNotFound(userId)),
                user => either.right(user),
            ),
            either.map(a => [a, env]),
        ),
    ordersFor: orderId => test.gets(env => env.userOrders(orderId)),
    error: e => test.modify(env => env.logError(e)),
};

describe('UserInformationSpecV1', () => {
    const userId = new UserId('user-1234');
    const result = fetchUserInformation(test)(userId);
    it('should fetch user name and orders by ID', () => {
        const userId = new UserId('user-1234');
        const env = TestEnv.empty
            .withProfile(new UserProfile(userId, 'John Doe'))
            .withOrder(new Order(userId, new OrderId('order-1')))
            .withOrder(new Order(userId, new OrderId('order-2')));
        const info = test.evalState(result, env);
        expect(info).toEqual(
            either.right(
                new UserInformation('John Doe', [
                    new Order(userId, new OrderId('order-2')),
                    new Order(userId, new OrderId('order-1')),
                ]),
            ),
        );
    });
    it('should log an error if user does not exists', () => {
        const info = test.evalState(result, TestEnv.empty);
        expect(info).toEqual(either.left(new UserNotFound(userId)));
    });
});
civilizeddev commented 4 years ago

I played a bit with https://github.com/amitayh/functional-testing-tagless-final and ported the first example, take a look: ...

@raveclassic Thank you.

This example is very helpful to me.

I just got to know that I have to write HKT, Kind1 pairs for a type class instance.

I should have found and read this: Recipes / HKT

But It has been hidden in Recipes / Write type class instances.

Maybe I wouldn't have caught the point of what to do from the document without seeing any practical example.

So my opinion is:

Please provide more and more real world usage examples.

That's the way the kind of person like me learn something.

civilizeddev commented 4 years ago

@raveclassic

As for the examples above (algebras.ts, fetch-user-information.ts),

I found out that it still worked even without defining interfaces for HKT.

// export interface Users<F> {
//  readonly profileFor: (userId: UserId) => HKT<F, UserProfile>;
// }
export interface Users1<F extends URIS> {
    readonly profileFor: (userId: UserId) => Kind<F, UserProfile>;
}

...

And

// export function fetchUserInformation<M extends URIS>(
//  M: MonadThrowable1<M> & Users1<M> & Orders1<M> & Logging1<M>,
// ): (userId: UserId) => Kind<M, UserInformation>;
// export function fetchUserInformation<M>(
//  M: MonadThrowable<M> & Users<M> & Orders<M> & Logging<M>,
// ): (userId: UserId) => HKT<M, UserInformation>;
export function fetchUserInformation<M>(
    M: MonadThrowable<M> & Users<M> & Orders<M> & Logging<M>,
): (userId: UserId) => HKT<M, UserInformation> {
  ...
}

And I rewrote it like the following:

export function fetchUserInformation<M>(
    M: MonadThrowable1<M> & Users1<M> & Orders1<M> & Logging1<M>,
): (userId: UserId) => Kind<M, UserInformation> {
  ...
}

But I don't know why.

Could you tell me why you declared interfaces for HKT together with that for Kind?

What's the meaning of those 2 or 3 boilerplate declarations?

Thank you.

sobolevn commented 4 years ago

We try to approach the same idea in dry-python/returns: providing real-world examples for each monad. https://github.com/dry-python/returns

I know, that it is python. But, it is heavily inspired by fp-ts and might be helpful for some people.

raveclassic commented 4 years ago

@civilizeddev Well, that's because we have to overload such function for any kind we want it to work with. Here Kind2, Kind3 etc. are omitted for simplicity. Take a look https://gcanti.github.io/fp-ts/recipes/HKT.html

On the other hand the need for overloading HKT does look weird for me... Somehow it slipped my mind why we actually do that, because we can just use Kind as the last overloading.

import { Monad, Monad1, Monad2 } from 'fp-ts/lib/Monad';
import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/lib/HKT';
import { option } from 'fp-ts/lib/Option';
import { either } from 'fp-ts/lib/Either';

export function foo<M extends URIS2>(M: Monad2<M>): Kind2<M, unknown, number>;
export function foo<M extends URIS>(M: Monad1<M>): Kind<M, number>;
export function foo<M>(M: Monad<M>): HKT<M, number>;
export function foo<M>(M: Monad<M>): HKT<M, number> {
  return M.of(0);
}

export function bar<M extends URIS2>(M: Monad2<M>): Kind2<M, unknown, number>;
export function bar<M extends URIS>(M: Monad1<M>): Kind<M, number>;
export function bar<M extends URIS>(M: Monad1<M>): Kind<M, number> {
  return M.of(0);
}

const test1 = foo(option); // Option<number>
const test2 = foo(either); // Either<unknown, number>

const test3 = bar(option); // Option<number>
const test4 = bar(either); // Either<unknown, number>

@gcanti Could you remind me why we are doing this? 😄

gcanti commented 4 years ago

@raveclassic example:

import { getFoldableComposition } from 'fp-ts/lib/Foldable'
import { getFunctorComposition } from 'fp-ts/lib/Functor'
import { URIS } from 'fp-ts/lib/HKT'
import { Traversable1, TraversableComposition11 } from 'fp-ts/lib/Traversable'

export function getTraversableComposition<F extends URIS, G extends URIS>(
  F: Traversable1<F>,
  G: Traversable1<G>
): TraversableComposition11<F, G> {
  return {
    ...getFunctorComposition(F, G),
    ...getFoldableComposition(F, G),
    traverse: H => {
      const traverseF = F.traverse(H)
      const traverseG = G.traverse(H)
      return (fga, f) => traverseF(fga, ga => traverseG(ga, f))
    },
    sequence: H => {
      const sequenceF = F.sequence(H)
      const sequenceG = G.sequence(H)
      return fgha => sequenceF(F.map(fgha, sequenceG))
    }
  }
}

it doesn't compile, while this is fine

import { getFoldableComposition } from 'fp-ts/lib/Foldable'
import { getFunctorComposition } from 'fp-ts/lib/Functor'
import { URIS } from 'fp-ts/lib/HKT'
import { Traversable, TraversableComposition } from 'fp-ts/lib/Traversable'

export function getTraversableComposition<F, G>(
  F: Traversable<F>,
  G: Traversable<G>
): TraversableComposition<F, G> {
  return {
    ...getFunctorComposition(F, G),
    ...getFoldableComposition(F, G),
    traverse: H => {
      const traverseF = F.traverse(H)
      const traverseG = G.traverse(H)
      return (fga, f) => traverseF(fga, ga => traverseG(ga, f))
    },
    sequence: H => {
      const sequenceF = F.sequence(H)
      const sequenceG = G.sequence(H)
      return fgha => sequenceF(F.map(fgha, sequenceG))
    }
  }
}
raveclassic commented 4 years ago

@gcanti Indeed, thanks!

gcanti commented 4 years ago

@raveclassic @civilizeddev for simpler cases starting from Kind should be fine though, you can always add the HKT overloadings later if you hit some problem, I guess