Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
gillchristian committed Mar 10, 2022
1 parent b457ccc commit 33887e8
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 101 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
"prettier": "^1.14.3",
"pretty-quick": "^1.7.0",
"ts-jest": "^23.10.4",
"typescript": "^3.1.1"
"typescript": "^4.0.3"
},
"dependencies": {
"fp-ts": "^2.8.4"
}
}
323 changes: 227 additions & 96 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,115 +1,246 @@
const enum Types {
success = '__rd_success__',
failure = '__rd_failure__',
notAsked = '__rd_notAsked__',
loading = '__rd_loading__',
import { Functor2 } from 'fp-ts/lib/Functor';
import { Apply2 } from 'fp-ts/lib/Apply';
import { Applicative2 } from 'fp-ts/lib/Applicative';
import { Chain2 } from 'fp-ts/lib/Chain';
import { Monad2 } from 'fp-ts/lib/Monad';
import { MonadThrow2 } from 'fp-ts/lib/MonadThrow';
import { Bifunctor2 } from 'fp-ts/lib/Bifunctor';
import { pipeable } from 'fp-ts/lib/pipeable';
import { Monoid } from 'fp-ts/lib/Monoid';
import { Semigroup } from 'fp-ts/lib/Semigroup';

interface NotAsked {
readonly tag: 'NotAsked';
}
interface Success<Data> {
readonly _type: Types.success;
readonly data: Data;

interface Loading {
readonly tag: 'Loading';
}

interface Success<D> {
readonly tag: 'Success';
readonly data: D;
}

interface Failure<E> {
readonly _type: Types.failure;
readonly tag: 'Failure';
readonly error: E;
}
interface NotAsked {
readonly _type: Types.notAsked;
}
interface Loading {
readonly _type: Types.loading;

type RemoteData<E, D> = NotAsked | Loading | Failure<E> | Success<D>;

interface User {
name: string;
}

export type RemoteData<D, E> = NotAsked | Loading | ResolvedData<D, E>;
const notAsked: RemoteData<never, never> = { tag: 'NotAsked' };

// promises/tasks should resolve to ResolvedData
export type ResolvedData<D, E> = Failure<E> | Success<D>;
const loading: RemoteData<never, never> = { tag: 'Loading' };

export const RemoteData = {
of: <D, E>(data: D): Success<D> => ({ _type: Types.success, data }),
notAsked: (): NotAsked => ({ _type: Types.notAsked }),
loading: (): Loading => ({ _type: Types.loading }),
failure: <D, E>(error: E): Failure<E> => ({ _type: Types.failure, error }),
success: <D, E>(data: D): Success<D> => ({ _type: Types.success, data }),
};
const failure = <E = unknown>(error: E): RemoteData<E, never> => ({
tag: 'Failure',
error,
});

const isNotAsked = (rd: RemoteData<any, any>): rd is NotAsked =>
rd._type === Types.notAsked;
const isLoading = (rd: RemoteData<any, any>): rd is Loading =>
rd._type === Types.loading;
const isSuccess = (rd: RemoteData<any, any>): rd is Success<any> =>
rd._type === Types.success;
const isFailure = (rd: RemoteData<any, any>): rd is Failure<any> =>
rd._type === Types.failure;

const isLoaded = (rd: RemoteData<any, any>): rd is ResolvedData<any, any> =>
isSuccess(rd) || isFailure(rd);

export const is = {
notAsked: isNotAsked,
loading: isLoading,
success: isSuccess,
failure: isFailure,
loaded: isLoaded,
};
const success = <D = unknown>(data: D): RemoteData<never, D> => ({
tag: 'Success',
data,
});

interface Catafn<D, E, R> {
const fold = <E = unknown, D = unknown, R = unknown>(matcher: {
notAsked: () => R;
loading: () => R;
failure: (error: E) => R;
success: (data: D) => R;
}
export const cata = <D, E, R = void>(m: Catafn<D, E, R>) => (
rd: RemoteData<D, E>,
): R => {
if (isNotAsked(rd)) {
return m.notAsked();
failure: (error: E) => R;
}) => (rd: RemoteData<E, D>): R => {
if (rd.tag === 'NotAsked') {
return matcher.notAsked();
}
if (isLoading(rd)) {
return m.loading();
if (rd.tag === 'Loading') {
return matcher.loading();
}
if (isFailure(rd)) {
return m.failure(rd.error);
if (rd.tag === 'Failure') {
return matcher.failure(rd.error);
}
return m.success(rd.data);
return matcher.success(rd.data);
};

// TypeClasses

export const URI = 'RemoteData';

export type URI = typeof URI;

declare module 'fp-ts/lib/HKT' {
interface URItoKind2<E, A> {
readonly RemoteData: RemoteData<E, A>;
}
}

const functor: Functor2<URI> = {
URI,
map: <E, A, B>(rda: RemoteData<E, A>, f: (a: A) => B): RemoteData<E, B> =>
fold<E, A, RemoteData<E, B>>({
notAsked: () => notAsked,
loading: () => loading,
failure,
success: (data) => success(f(data)),
})(rda),
};

// <E, A, B>(fab: RemoteData<F, E, (a: A) => B>, fa: RemoteData<F, E, A>) => RemoteData<F, E, B>

const apply: Apply2<URI> = {
...functor,
ap: <E, A, B>(
rdf: RemoteData<E, (a: A) => B>,
rda: RemoteData<E, A>,
): RemoteData<E, B> =>
fold<E, (a: A) => B, RemoteData<E, B>>({
notAsked: () => notAsked,
loading: () => loading,
failure,
success: (f) => functor.map(rda, f),
})(rdf),
};

const applicative: Applicative2<URI> = {
...apply,
of: success,
};

// <E, A, B>(fa: RemoteData<E, A>, f: (a: A) => RemoteData<E, B>) => RemoteData<E, B>

const chain: Chain2<URI> = {
...apply,
chain: <E, A, B>(
rda: RemoteData<E, A>,
f: (a: A) => RemoteData<E, B>,
): RemoteData<E, B> =>
fold<E, A, RemoteData<E, B>>({
notAsked: () => notAsked,
loading: () => loading,
failure,
success: (a) => f(a),
})(rda),
};

// map :: (a -> b) -> RemoteData e a -> RemoteData e b
export const map = <D, E, R>(fn: (d: D) => R) =>
cata<D, E, RemoteData<R, E>>({
notAsked: RemoteData.notAsked,
loading: RemoteData.loading,
failure: RemoteData.failure,
success: (data) => RemoteData.of(fn(data)),
});

// chain :: (a -> RemoteData e b) -> RemoteData e a -> RemoteData e b
export const chain = <D, E, R>(fn: (d: D) => RemoteData<R, E>) =>
cata<D, E, RemoteData<R, E>>({
notAsked: RemoteData.notAsked,
loading: RemoteData.loading,
failure: RemoteData.failure,
success: (data) => fn(data),
});

// fold :: (a -> b) -> b -> RemoteData e a -> b
export const fold = <D, E, R>(fn: (d: D) => R) => (def: R) =>
cata<D, E, R>({
notAsked: () => def,
loading: () => def,
failure: () => def,
success: (data) => fn(data),
});

// ap :: RemoteData e (a -> b) -> RemoteData e a -> RemoteData e b
export const ap = <D, E, R>(rdfn: RemoteData<(a: D) => R, E>) =>
cata<D, E, RemoteData<R, E>>({
notAsked: RemoteData.notAsked,
loading: RemoteData.loading,
failure: RemoteData.failure,
success: (v) => map<(a: D) => R, E, R>((f) => f(v))(rdfn),
});

// lift2 :: (a -> b -> c) -> RemoteData e a -> RemoteData e b -> RemoteData e c
export const lift2 = <A, B, C, E>(f: (a: A) => (b: B) => C) => (
rda: RemoteData<A, E>,
) => ap<B, E, C>(map<A, E, (b: B) => C>(f)(rda));
const monad: Monad2<URI> = {
...chain,
...applicative,
};

const monadThrow: MonadThrow2<URI> = {
...monad,
throwError: failure,
};

// bimap: <E, A, G, B>(fea: Kind2<F, E, A>, f: (e: E) => G, g: (a: A) => B) => Kind2<F, G, B>
// mapLeft: <E, A, G>(fea: Kind2<F, E, A>, f: (e: E) => G) => Kind2<F, G, A>

const bifunctor: Bifunctor2<URI> = {
URI,
bimap: <E, A, G, B>(
rdea: RemoteData<E, A>,
f: (e: E) => G,
g: (a: A) => B,
): RemoteData<G, B> =>
fold<E, A, RemoteData<G, B>>({
notAsked: () => notAsked,
loading: () => loading,
failure: (error) => failure(f(error)),
success: (data) => success(g(data)),
})(rdea),
mapLeft: <E, A, G>(rda: RemoteData<E, A>, f: (a: E) => G): RemoteData<G, A> =>
fold<E, A, RemoteData<G, A>>({
notAsked: () => notAsked,
loading: () => loading,
failure: (error) => failure(f(error)),
success,
})(rda),
};

export const remoteData = pipeable({
...monadThrow,
...bifunctor,
});

const getSemigroupFirst = <E, A>(): Semigroup<RemoteData<E, A>> => ({
concat: (a: RemoteData<E, A>, b: RemoteData<E, A>) =>
fold<E, A, RemoteData<E, A>>({
notAsked: () => (b.tag === 'Success' ? b : a),
loading: () => (b.tag === 'Success' ? b : a),
failure: () => (b.tag === 'Success' ? b : a),
success: (data) => success(data),
})(a),
});

const getMonoidFirst = <E, A>(): Monoid<RemoteData<E, A>> => ({
empty: notAsked,
concat: getSemigroupFirst<E, A>().concat,
});

// Identity: F.map(fa, a => a) <-> fa
//
// functor.map(rd, a => a) === rd
//
// Composition: F.map(fa, a => bc(ab(a))) <-> F.map(F.map(fa, ab), bc)
//
// functor.map(rd, a => bc(ab(a))) === functor.map(functor.map(rd, ab), bc)

// -----------------------------------------------------------------------------

interface User {
name: string;
}

const jane = { name: 'Jane Doe' };
const mark = { name: 'Mark Twain' };

const fetchedUser = success<User>(jane);

const showMessage = fold<Error, User, string>({
notAsked: () => 'notAsked',
loading: () => 'loading',
success: ({ name }) => 'success: ' + name,
failure: ({ message }) => 'failure: ' + message,
});

showMessage(fetchedUser);

const getName = ({ name }: User) => name;
const getErrorMessage = ({ message }: Error) => message;

getName(jane); // string

getErrorMessage(new Error('Something went wrong'));

functor.map(fetchedUser, getName); // RemoteData<Error, string>

bifunctor.mapLeft(fetchedUser, getErrorMessage); // RemoteData<string, User>

bifunctor.bimap(fetchedUser, getErrorMessage, getName); // RemoteData<string, string>

const kill = (a: User) => (b: User) => `${a.name} kills ${b.name}`;

kill(jane)(mark); // 'Jane kills Mark'

// kill(success(jane))(success(mark));

apply.ap(
apply.ap(applicative.of(kill), applicative.of(jane)),
applicative.of(mark),
); // RemoteData<Error, string>

// { tag: 'Success', data: 'Jane kills Mark' }

const exclaim = ({ name }: User): RemoteData<Error, User> =>
success({ name: name + '!' });

chain.chain(fetchedUser, exclaim); // RemoteData<Error, User>

const monoidFirstNumber = getMonoidFirst<Error, User>();

monoidFirstNumber.concat(success(jane), loading); // success(jane)
monoidFirstNumber.concat(loading, success(mark)); // success(mark)
monoidFirstNumber.concat(success(jane), success(mark)); // success(jane)
13 changes: 9 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,11 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"

fp-ts@^2.8.4:
version "2.8.4"
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.8.4.tgz#d1af738a94de8591d441ef656153d04bd878edeb"
integrity sha512-J+kwce5SysU0YKuZ3aCnFk+dyezZD1mij6u26w1fCVfuLYgJR4eeXmVfJiUjthpZ+4yCRkRfcwMI5SkGw52oFA==

fragment-cache@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
Expand Down Expand Up @@ -4585,10 +4590,10 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"

typescript@^3.1.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.2.tgz#fe8101c46aa123f8353523ebdcf5730c2ae493e5"
integrity sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==
typescript@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5"
integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==

uglify-js@^3.1.4:
version "3.4.9"
Expand Down

0 comments on commit 33887e8

Please sign in to comment.