-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b457ccc
commit 33887e8
Showing
3 changed files
with
240 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters