Last active
June 27, 2021 19:34
-
-
Save gvergnaud/694d9a65b56dfab66b28e72b2341cbbb to your computer and use it in GitHub Desktop.
Task.js — Full Implementation (SemiGroup, Monoid, Functor, Monad, Applicative)
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
import { compose, add, map, range } from 'lodash/fp' | |
import { Just, Nothing } from './Maybe' | |
class Task { | |
constructor(fork) { | |
this.fork = fork | |
} | |
static of(x) { | |
return Task.resolve(x) | |
} | |
static resolve(x) { | |
return new Task((_, resolve) => resolve(x)) | |
} | |
static reject(x) { | |
return new Task((reject, _) => reject(x)) | |
} | |
static empty() { | |
return new Task((_, resolve) => resolve()) | |
} | |
/** | |
* use the first Task to resolve | |
* @summary concat :: Task a b -> Task a b -> Task a b | |
*/ | |
concat(task) { | |
return new Task((reject, resolve) => { | |
let done = false | |
const guard = f => x => { | |
if (!done) { | |
done = true | |
f(x) | |
} | |
} | |
task.fork(guard(reject), guard(resolve)) | |
this.fork(guard(reject), guard(resolve)) | |
}) | |
} | |
/** | |
* @summary map :: (a -> b) -> Task Err a -> Task Err b | |
*/ | |
map(f) { | |
return new Task((reject, resolve) => this.fork( | |
reject, | |
compose(resolve, f) | |
)) | |
} | |
/** | |
* @summary chain :: (a -> Task Err b) -> Task Err a -> Task Err b | |
*/ | |
chain(f) { | |
return new Task((reject, resolve) => this.fork( | |
reject, | |
x => f(x).fork(reject, resolve) | |
)) | |
} | |
/** | |
* @summary ap :: Task Err (a -> b) -> Task Err a -> Task Err b | |
*/ | |
ap(task) { | |
return new Task((reject, resolve) => { | |
this.fork( | |
reject, | |
f => task.fork(reject, compose(resolve, f)) | |
) | |
}) | |
} | |
/** | |
* fold the Rejected or Resolved Task into a Resolved Task by applying a | |
* different mapper whether its Rejected or Resolved. | |
* @summary fold :: (a -> b) -> (c -> b) -> Task a c -> Task _ b | |
*/ | |
fold(f, g) { | |
return new Task((reject, resolve) => this.fork( | |
compose(resolve, f), | |
compose(resolve, g) | |
)) | |
} | |
/** | |
* like fold but using pattern matching | |
* @summary cata :: { Rejected :: a -> c, Resolved :: b -> c } -> Task a b -> Task _ c | |
*/ | |
cata({ Rejected, Resolved }) { | |
return this.fold(Rejected, Resolved) | |
} | |
/** | |
* transform a Rejected to a Resolved and vice versa | |
* @summary swap :: _ -> Task a b -> Task b a | |
*/ | |
swap() { | |
return new Task((reject, resolve) => this.fork(resolve, reject)) | |
} | |
/** | |
* apply a mapper to the rejected or resolved Task | |
* @summary bimap :: (a -> b) -> (c -> d) -> Task a c -> Task b d | |
*/ | |
bimap(f, g) { | |
return new Task((reject, resolve) => this.fork( | |
compose(reject, f), | |
compose(resolve, g) | |
)) | |
} | |
/** | |
* @summary rejectedMap :: (a -> b) -> Task a x -> Task b x | |
*/ | |
rejectMap(f) { | |
return new Task((reject, resolve) => this.fork( | |
compose(reject, f), | |
resolve | |
)) | |
} | |
/** | |
* @summary rejectedChain :: (a -> Task b x) -> Task a x -> Task b x | |
*/ | |
rejectChain(f) { | |
return new Task((reject, resolve) => this.fork( | |
x => f(x).compose(reject, f), | |
resolve | |
)) | |
} | |
/** | |
* @summary toMaybe :: _ -> Task a b -> Task Nothing (Just b) | |
*/ | |
toMaybe() { | |
return new Task((reject, resolve) => this.fork( | |
compose(resolve, Nothing.of), | |
compose(resolve, Just.of) | |
)) | |
} | |
/** | |
* @summary fromMaybe :: a -> Maybe x -> Task a x | |
*/ | |
static fromMaybe(defaultValue, maybe) { | |
return new Task((reject, resolve) => { | |
cata({ | |
Nothing: () => reject(defaultValue), | |
Just: x => resolve(x) | |
})(maybe) | |
}) | |
} | |
/** | |
* @summary toPromise :: Task a b -> Promise b a | |
*/ | |
toPromise() { | |
return new Promise((resolve, reject) => this.fork(reject, resolve)) | |
} | |
/** | |
* @summary fromPromise :: Promise b a -> Task a b | |
*/ | |
static fromPromise(promise) { | |
return new Task((reject, resolve) => promise.then(resolve, reject)) | |
} | |
} | |
/* ----------------------------------------- * | |
Let's use that | |
* ----------------------------------------- */ | |
const compose = (...fns) => x => fns.reduceRight((acc, f) => f(acc), x) | |
const map = f => xs => xs.map(f) | |
const range = (a, b) => a === b | |
? [b] | |
: [a, ...range(a + 1, b)] | |
const add = a => b => a + b | |
const noop = () => {} | |
const delay = (duration, value) => new Task((_, resolve) => setTimeout(() => resolve(value), duration)) | |
const getUsers = count => new Task((_, resolve) => { | |
setTimeout(() => { | |
resolve(range(0, count).map(x => ({ id: x, username: 'Bob' }))) | |
}, 200) | |
}) | |
const userComponent = ({ id, username }) => ` | |
<div> | |
<p>userID: ${id}</p> | |
<p>user name: ${username}</p> | |
</div> | |
` | |
const div = x => `<div>${[].concat(x).join('')}</div>` | |
Task | |
.of(add) | |
.ap(Task.of(3)) | |
.ap(Task.of(8)) | |
.map(x => x / 10) | |
.map(add) | |
.ap(delay(1000, 70)) | |
.map(Math.ceil) | |
.chain(getUsers) | |
.map(map(userComponent)) | |
.map(div) | |
.fork(noop, x => console.log(x)) | |
/* | |
=> ` | |
<div> | |
<div> | |
<p>userID: 1</p> | |
<p>user name: Bob</p> | |
</div> | |
<div> | |
<p>userID: 2</p> | |
<p>user name: Bob</p> | |
</div> | |
... | |
</div> | |
` | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment