Open civilizeddev opened 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)));
});
});
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.
Please provide more and more real world usage examples.
That's the way the kind of person like me learn something.
@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.
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.
@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? 😄
@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))
}
}
}
@gcanti Indeed, thanks!
@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
🚀 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
Modules
only provide APIs. Just few of them provide usage examples.fp-to-the-max-ii.ts
is not enough.Desired Behavior
Suggested Solution
Document / Recipes / Migrate from Scala with cats
Who does this impact? Who is this for?
For beginners who just get interested in fp-ts.
Describe alternatives you've considered
getReaderM (function)
Add explanatory description here...
Signature
export function getReaderM<M>(M: Monad<M>): ReaderM<M> { ... }
Usage
Additional context
N/A
Your environment