Skip to content

Commit 9779caa

Browse files
Implement Task as an applicative and update the README
1 parent 8a73868 commit 9779caa

File tree

3 files changed

+198
-32
lines changed

3 files changed

+198
-32
lines changed

README.md

+108-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Functional
22

3-
[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/functional/SumType.js)
3+
Common Functional Programming Algebraic data types for JavaScript that is compatible with most modern browsers and Deno.
4+
45
[![deno land](http://img.shields.io/badge/available%20on-deno.land/x-lightgrey.svg?logo=deno&labelColor=black)](https://deno.land/x/cli_badges)
56
[![deno version](https://img.shields.io/badge/deno-^1.3.2-lightgrey?logo=deno)](https://github.com/denoland/deno)
67
[![GitHub release](https://img.shields.io/github/release/sebastienfilion/functional.svg)](https://github.com/sebastienfilion/functional/releases)
@@ -9,14 +10,53 @@
910
* [Maybe](#maybe-type)
1011
* [Either](#either-type)
1112
* [IO](#io-type)
13+
* [Task](#task-type)
1214
* [TypeScript](#typescript)
15+
16+
# Usage
17+
18+
This example uses the Ramda library - for simplification - but you should be able to use any library that implements
19+
the [Fantasy-land specifications](https://github.com/fantasyland/fantasy-land).
20+
21+
```js
22+
import { compose, converge, lift, map, prop } from "https://x.nest.land/[email protected]/source/index.js";
23+
import Either from "https://deno.land/x/[email protected]/Either.js"
24+
import Task from "https://deno.land/x/[email protected]/Task.js"
25+
26+
const fetchUser = userID => Task.wrap(_ => fetch(`${URL}/users/${userID}`).then(response => response.json()));
27+
28+
const sayHello = compose(
29+
converge(
30+
lift((username, email) => `Hello ${username} (${email})!`),
31+
[
32+
map(prop("username")),
33+
map(prop("email"))
34+
]
35+
),
36+
fetchUser
37+
);
38+
39+
// Calling `sayHello` results in an instance of `Task` keeping the function pure.
40+
assert(Task.is(sayHello(userID)));
41+
42+
// Finally, calling `Task#run` will call `fetch` and return a promise
43+
sayHello(userID).run()
44+
.then(container => {
45+
// The returned value should be an instance of `Either.Right` or `Either.Left`
46+
assert(Either.Right.is(container));
47+
// Forcing to coerce the container to string will show that the final value is our message.
48+
assert(container.toString(), `Either.Right("Hello johndoe ([email protected])!")`);
49+
});
50+
51+
// sayHello(userID).run() === Either.Right("Hello johndoe ([email protected])!")
52+
```
1353

1454
## Type factory
1555

1656
The Type factory can be used to build complex data structure.
1757

1858
```js
19-
import { factorizeType } from "https://deno.land/x/functional/SumType.js"
59+
import { factorizeType } from "https://deno.land/x/functional@v0.5.0/SumType.js"
2060

2161
const Coordinates = factorizeType("Coordinates", [ "x", "y" ]);
2262
const vector = Coordinates(150, 200);
@@ -80,7 +120,7 @@ vector.toString();
80120
## Type Sum factory
81121

82122
```js
83-
import { factorizeSumType } from "https://deno.land/x/functional/SumType.js"
123+
import { factorizeSumType } from "https://deno.land/x/functional@v0.5.0/SumType.js"
84124

85125
const Shape = factorizeSumType(
86126
"Shape",
@@ -162,7 +202,7 @@ oval.toString();
162202
### Example of writing a binary tree with Sum Types
163203

164204
```js
165-
import { factorizeSumType } from "https://deno.land/x/functional/SumType.js"
205+
import { factorizeSumType } from "https://deno.land/x/functional@v0.5.0/SumType.js"
166206

167207
const BinaryTree = factorizeSumType('BinaryTree', {
168208
Node: ['left', 'x', 'right'],
@@ -215,7 +255,7 @@ const tree =
215255
The `Maybe` type represents potentially `Just` a value or `Nothing`.
216256

217257
```js
218-
import Maybe from "https://deno.land/x/functional/Maybe.js"
258+
import Maybe from "https://deno.land/x/functional@v0.5.0/Maybe.js"
219259

220260
const container = Maybe.Just(42);
221261

@@ -240,7 +280,7 @@ This implementation of Maybe is a valid [`Filterable`](https://github.com/fantas
240280
The `Either` type represents the possibility of two values; either an `a` or a `b`.
241281

242282
```js
243-
import Either from "https://deno.land/x/functional/Either.js"
283+
import Either from "https://deno.land/x/functional@v0.5.0/Either.js"
244284

245285
const container = Either.Right(42);
246286

@@ -263,7 +303,7 @@ This implementation of Maybe is a valid [`Functor`](https://github.com/fantasyla
263303
The `IO` type represents a function that access IO. It will be lazily executed when the `#run` method is called.
264304

265305
```js
266-
import IO from "https://deno.land/x/functional/IO.js"
306+
import IO from "https://deno.land/x/functional@v0.5.0/IO.js"
267307

268308
// Eventually 42
269309
const container = IO(_ => Promise.resolve(42));
@@ -285,14 +325,73 @@ This implementation of IO is a valid [`Functor`](https://github.com/fantasyland/
285325
[`Applicative`](https://github.com/fantasyland/fantasy-land#applicative) and
286326
[`Monad`](https://github.com/fantasyland/fantasy-land#monad).
287327

328+
## `Task` type
329+
330+
The `Task` type represents a function that access IO. It will be lazily executed when the `#run` method is called.
331+
Unlike IO, the Task type also abstract away the promise making for a more intuitive experience.
332+
Note that the function must return an instance of [`Either`](#either-type); `Either.Right` to represent a success and
333+
`Either.Left` to represent a failure. Also check-out the [`Task.wrap`](#task-wrap) method.
334+
335+
If the runtime throws an error, the final value will be `Either.Left(error)`.
336+
337+
```js
338+
import Either from "https://deno.land/x/[email protected]/Either.js";
339+
import Task from "https://deno.land/x/[email protected]/Task.js"
340+
341+
// Eventually 42
342+
const container = Task(_ => Promise.resolve(Either.Right(42)));
343+
344+
const multiply = container.map(x => x * x);
345+
const add = container.map(x => x + x);
346+
347+
// multiply === Task(Function)
348+
// add === Task(Function)
349+
350+
const multiplyThenAdd = multiply.map(x => x + x);
351+
352+
// await multiply.run() === Either.Right(1764)
353+
// await add.run() === Either.Right(84)
354+
// await multiplyThenAdd.run() === Either.Right(3528)
355+
```
356+
357+
### `Task.wrap`
358+
359+
Create a wrapped instance of Task. An instance of `Task` made using the `wrap` method is different in two ways:
360+
361+
1. The result of the function call is memoized;
362+
2. If the function call was successful, the value will automatically be an instance of `Either.Right`;
363+
364+
```js
365+
import Task from "https://deno.land/x/[email protected]/Task.js"
366+
367+
let count = 0;
368+
const fetchUser = userID => Task.wrap(
369+
_ => ++count && fetch(`${URL}/users/${userID}`).then(response => response.json())
370+
);
371+
372+
const user = fetchUser(userID);
373+
const username = user.map(({ username }) => username);
374+
const email = user.map(({ email }) => email);
375+
376+
// await user.run() === Either.Right({ email: "[email protected]", username: "johndoe" })
377+
// await username.run() === Either.Right("johndoe")
378+
// await email.run() === Either.Right("[email protected]")
379+
// count === 1
380+
```
381+
382+
This implementation of Task is a valid [`Functor`](https://github.com/fantasyland/fantasy-land#functor),
383+
[`Applicative`](https://github.com/fantasyland/fantasy-land#applicative),
384+
[`Alternative`](https://github.com/fantasyland/fantasy-land#alternative) and
385+
[`Monad`](https://github.com/fantasyland/fantasy-land#monad).
386+
288387
## TypeScript
289388

290389
I will try to publish TypeScript type hint files for those who needs it.
291390
So far, I've only implemented the Type factory functions.
292391

293392
```ts
294-
// @deno-types="https://deno.land/x/functional/SumType.d.ts"
295-
import { factorizeType, factorizeSumType } from "https://deno.land/x/functional/SumType.js";
393+
// @deno-types="https://deno.land/x/functional@v0.5.0/SumType.d.ts"
394+
import { factorizeType, factorizeSumType } from "https://deno.land/x/functional@v0.5.0/SumType.js";
296395
```
297396

298397
## Deno

library/Task.js

+33-23
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { factorizeType } from "./SumType.js";
22
import Either from "./Either.js";
33

4-
const $$value = Symbol.for("TypeValue");
5-
64
export const Task = factorizeType("Task", [ "asyncFunction" ]);
75

8-
Task.empty = _ => Task(_ => null);
6+
// from :: Task => f -> T f
97
Task.from = (composedFunction) => Task(composedFunction);
10-
Task.of = value => Task(_ => value);
11-
// wrap :: Task => (* -> Promise a) -> Task e Promise a
8+
9+
// wrap :: Task t => (* -> Promise a) -> t e Promise a
1210
Task.wrap = asyncFunction => {
1311
let promise;
1412
const proxyFunction = function (...argumentList) {
@@ -29,29 +27,41 @@ Task.wrap = asyncFunction => {
2927
);
3028
};
3129

32-
Task.prototype.alt = Task.prototype["fantasy-land/alt"] = function (container) {
30+
// empty :: Task t => () => t
31+
Task.empty = Task.prototype.empty = Task.prototype["fantasy-land/empty"] = _ => Task(_ => function () {});
3332

34-
return Task(_ => {
35-
36-
return thatContainer.fold({
37-
Left: _ => container,
38-
Right: _ => this
39-
});
40-
});
41-
};
33+
// of :: Task t => a -> t a
34+
Task.of = Task.prototype.of = Task.prototype["fantasy-land/of"] = unaryFunction =>
35+
Task(_ => Promise.resolve(Either.Right(unaryFunction)));
4236

4337
// ap :: Task a ~> Task (a -> b) -> Task b
4438
Task.prototype.ap = Task.prototype["fantasy-land/ap"] = function (container) {
4539

46-
return container.chain(unaryFunction => this.map(unaryFunction));
47-
48-
// return container.map(unaryFunction => {
49-
// const promise = this.asyncFunction();
50-
//
51-
// return (promise instanceof Promise)
52-
// ? promise.then(value => Either.Right(unaryFunction(value)), Either.Left)
53-
// : unaryFunction(promise);
54-
// });
40+
return Task(_ => {
41+
const maybePromiseUnaryFunction = this.asyncFunction();
42+
const maybePromiseValue = container.asyncFunction();
43+
44+
return Promise.all([
45+
(maybePromiseUnaryFunction instanceof Promise)
46+
? maybePromiseUnaryFunction
47+
: Promise.resolve(maybePromiseUnaryFunction),
48+
(maybePromiseValue instanceof Promise)
49+
? maybePromiseValue
50+
: Promise.resolve(maybePromiseValue)
51+
])
52+
.then(([ maybeApplicativeUnaryFunction, maybeContainerValue ]) => {
53+
54+
return (
55+
(Reflect.getPrototypeOf(maybeApplicativeUnaryFunction).ap)
56+
? maybeApplicativeUnaryFunction
57+
: Either.Right(maybeApplicativeUnaryFunction)
58+
).ap(
59+
(Reflect.getPrototypeOf(maybeContainerValue).ap)
60+
? maybeContainerValue
61+
: Either.Right(maybeContainerValue)
62+
);
63+
});
64+
});
5565
};
5666

5767
// chain :: Task e a ~> (a -> Task b) -> Task e b

library/Task_test.js

+57
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,63 @@ import Task from "./Task.js";
66
const add = x => x + 2;
77
const multiply = x => x * 2;
88

9+
Deno.test(
10+
"Task: #ap (promise)",
11+
() => {
12+
const containerA = Task.of(42);
13+
const containerB = Task.of(x => x * x);
14+
assert(Task.is(containerA));
15+
assert(Task.is(containerB));
16+
17+
const containerC = containerA.ap(containerB);
18+
assert(Task.is(containerC));
19+
20+
const promise = containerC.run();
21+
assert(promise instanceof Promise);
22+
23+
return promise
24+
.then(container => assertIsEquivalent(container, Either.Right(1764)));
25+
}
26+
);
27+
28+
Deno.test(
29+
"Task: #ap - Composition",
30+
async () => {
31+
const containerA = Task.of(42);
32+
const containerB = Task.of(x => x + 2);
33+
const containerC = Task.of(x => x * 2);
34+
assert(Task.is(containerA));
35+
assert(Task.is(containerB));
36+
assert(Task.is(containerC));
37+
38+
const containerD = await containerA.ap(containerB.ap(containerC.map(a => b => c => a(b(c))))).run();
39+
const containerE = await containerA.ap(containerB).ap(containerC).run();
40+
assert(Either.is(containerD));
41+
assert(Either.is(containerE));
42+
43+
assertIsEquivalent(containerD, containerE);
44+
}
45+
);
46+
47+
Deno.test(
48+
"Task: #ap with lift",
49+
async () => {
50+
const lift2 = (f, a, b) => b.ap(a.map(f));
51+
52+
const containerA = Task.of(42);
53+
const containerB = Task.of(32);
54+
const containerC = Task.of(x => y => x * y);
55+
assert(Task.is(containerA));
56+
assert(Task.is(containerB));
57+
assert(Task.is(containerC));
58+
59+
const containerD = await lift2(x => y => x * y, containerA, containerB).run();
60+
assert(Either.is(containerD));
61+
62+
assertIsEquivalent(containerD, Either.Right(1344));
63+
}
64+
);
65+
966
Deno.test(
1067
"Task: #map (promise)",
1168
() => {

0 commit comments

Comments
 (0)