Skip to content

Commit

Permalink
Implement Task as an applicative and update the README
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienfilion committed Sep 9, 2020
1 parent 8a73868 commit 9779caa
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 32 deletions.
117 changes: 108 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Functional

[![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/functional/SumType.js)
Common Functional Programming Algebraic data types for JavaScript that is compatible with most modern browsers and Deno.

[![deno land](http://img.shields.io/badge/available%20on-deno.land/x-lightgrey.svg?logo=deno&labelColor=black)](https://deno.land/x/cli_badges)
[![deno version](https://img.shields.io/badge/deno-^1.3.2-lightgrey?logo=deno)](https://github.com/denoland/deno)
[![GitHub release](https://img.shields.io/github/release/sebastienfilion/functional.svg)](https://github.com/sebastienfilion/functional/releases)
Expand All @@ -9,14 +10,53 @@
* [Maybe](#maybe-type)
* [Either](#either-type)
* [IO](#io-type)
* [Task](#task-type)
* [TypeScript](#typescript)

# Usage

This example uses the Ramda library - for simplification - but you should be able to use any library that implements
the [Fantasy-land specifications](https://github.com/fantasyland/fantasy-land).

```js
import { compose, converge, lift, map, prop } from "https://x.nest.land/[email protected]/source/index.js";
import Either from "https://deno.land/x/[email protected]/Either.js"
import Task from "https://deno.land/x/[email protected]/Task.js"

const fetchUser = userID => Task.wrap(_ => fetch(`${URL}/users/${userID}`).then(response => response.json()));

const sayHello = compose(
converge(
lift((username, email) => `Hello ${username} (${email})!`),
[
map(prop("username")),
map(prop("email"))
]
),
fetchUser
);

// Calling `sayHello` results in an instance of `Task` keeping the function pure.
assert(Task.is(sayHello(userID)));

// Finally, calling `Task#run` will call `fetch` and return a promise
sayHello(userID).run()
.then(container => {
// The returned value should be an instance of `Either.Right` or `Either.Left`
assert(Either.Right.is(container));
// Forcing to coerce the container to string will show that the final value is our message.
assert(container.toString(), `Either.Right("Hello johndoe ([email protected])!")`);
});

// sayHello(userID).run() === Either.Right("Hello johndoe ([email protected])!")
```

## Type factory

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

```js
import { factorizeType } from "https://deno.land/x/functional/SumType.js"
import { factorizeType } from "https://deno.land/x/functional@v0.5.0/SumType.js"

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

```js
import { factorizeSumType } from "https://deno.land/x/functional/SumType.js"
import { factorizeSumType } from "https://deno.land/x/functional@v0.5.0/SumType.js"

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

```js
import { factorizeSumType } from "https://deno.land/x/functional/SumType.js"
import { factorizeSumType } from "https://deno.land/x/functional@v0.5.0/SumType.js"

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

```js
import Maybe from "https://deno.land/x/functional/Maybe.js"
import Maybe from "https://deno.land/x/functional@v0.5.0/Maybe.js"

const container = Maybe.Just(42);

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

```js
import Either from "https://deno.land/x/functional/Either.js"
import Either from "https://deno.land/x/functional@v0.5.0/Either.js"

const container = Either.Right(42);

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

```js
import IO from "https://deno.land/x/functional/IO.js"
import IO from "https://deno.land/x/functional@v0.5.0/IO.js"

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

## `Task` type

The `Task` type represents a function that access IO. It will be lazily executed when the `#run` method is called.
Unlike IO, the Task type also abstract away the promise making for a more intuitive experience.
Note that the function must return an instance of [`Either`](#either-type); `Either.Right` to represent a success and
`Either.Left` to represent a failure. Also check-out the [`Task.wrap`](#task-wrap) method.

If the runtime throws an error, the final value will be `Either.Left(error)`.

```js
import Either from "https://deno.land/x/[email protected]/Either.js";
import Task from "https://deno.land/x/[email protected]/Task.js"

// Eventually 42
const container = Task(_ => Promise.resolve(Either.Right(42)));

const multiply = container.map(x => x * x);
const add = container.map(x => x + x);

// multiply === Task(Function)
// add === Task(Function)

const multiplyThenAdd = multiply.map(x => x + x);

// await multiply.run() === Either.Right(1764)
// await add.run() === Either.Right(84)
// await multiplyThenAdd.run() === Either.Right(3528)
```

### `Task.wrap`

Create a wrapped instance of Task. An instance of `Task` made using the `wrap` method is different in two ways:

1. The result of the function call is memoized;
2. If the function call was successful, the value will automatically be an instance of `Either.Right`;

```js
import Task from "https://deno.land/x/[email protected]/Task.js"

let count = 0;
const fetchUser = userID => Task.wrap(
_ => ++count && fetch(`${URL}/users/${userID}`).then(response => response.json())
);

const user = fetchUser(userID);
const username = user.map(({ username }) => username);
const email = user.map(({ email }) => email);

// await user.run() === Either.Right({ email: "[email protected]", username: "johndoe" })
// await username.run() === Either.Right("johndoe")
// await email.run() === Either.Right("[email protected]")
// count === 1
```

This implementation of Task is a valid [`Functor`](https://github.com/fantasyland/fantasy-land#functor),
[`Applicative`](https://github.com/fantasyland/fantasy-land#applicative),
[`Alternative`](https://github.com/fantasyland/fantasy-land#alternative) and
[`Monad`](https://github.com/fantasyland/fantasy-land#monad).

## TypeScript

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

```ts
// @deno-types="https://deno.land/x/functional/SumType.d.ts"
import { factorizeType, factorizeSumType } from "https://deno.land/x/functional/SumType.js";
// @deno-types="https://deno.land/x/functional@v0.5.0/SumType.d.ts"
import { factorizeType, factorizeSumType } from "https://deno.land/x/functional@v0.5.0/SumType.js";
```

## Deno
Expand Down
56 changes: 33 additions & 23 deletions library/Task.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { factorizeType } from "./SumType.js";
import Either from "./Either.js";

const $$value = Symbol.for("TypeValue");

export const Task = factorizeType("Task", [ "asyncFunction" ]);

Task.empty = _ => Task(_ => null);
// from :: Task => f -> T f
Task.from = (composedFunction) => Task(composedFunction);
Task.of = value => Task(_ => value);
// wrap :: Task => (* -> Promise a) -> Task e Promise a

// wrap :: Task t => (* -> Promise a) -> t e Promise a
Task.wrap = asyncFunction => {
let promise;
const proxyFunction = function (...argumentList) {
Expand All @@ -29,29 +27,41 @@ Task.wrap = asyncFunction => {
);
};

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

return Task(_ => {

return thatContainer.fold({
Left: _ => container,
Right: _ => this
});
});
};
// of :: Task t => a -> t a
Task.of = Task.prototype.of = Task.prototype["fantasy-land/of"] = unaryFunction =>
Task(_ => Promise.resolve(Either.Right(unaryFunction)));

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

return container.chain(unaryFunction => this.map(unaryFunction));

// return container.map(unaryFunction => {
// const promise = this.asyncFunction();
//
// return (promise instanceof Promise)
// ? promise.then(value => Either.Right(unaryFunction(value)), Either.Left)
// : unaryFunction(promise);
// });
return Task(_ => {
const maybePromiseUnaryFunction = this.asyncFunction();
const maybePromiseValue = container.asyncFunction();

return Promise.all([
(maybePromiseUnaryFunction instanceof Promise)
? maybePromiseUnaryFunction
: Promise.resolve(maybePromiseUnaryFunction),
(maybePromiseValue instanceof Promise)
? maybePromiseValue
: Promise.resolve(maybePromiseValue)
])
.then(([ maybeApplicativeUnaryFunction, maybeContainerValue ]) => {

return (
(Reflect.getPrototypeOf(maybeApplicativeUnaryFunction).ap)
? maybeApplicativeUnaryFunction
: Either.Right(maybeApplicativeUnaryFunction)
).ap(
(Reflect.getPrototypeOf(maybeContainerValue).ap)
? maybeContainerValue
: Either.Right(maybeContainerValue)
);
});
});
};

// chain :: Task e a ~> (a -> Task b) -> Task e b
Expand Down
57 changes: 57 additions & 0 deletions library/Task_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,63 @@ import Task from "./Task.js";
const add = x => x + 2;
const multiply = x => x * 2;

Deno.test(
"Task: #ap (promise)",
() => {
const containerA = Task.of(42);
const containerB = Task.of(x => x * x);
assert(Task.is(containerA));
assert(Task.is(containerB));

const containerC = containerA.ap(containerB);
assert(Task.is(containerC));

const promise = containerC.run();
assert(promise instanceof Promise);

return promise
.then(container => assertIsEquivalent(container, Either.Right(1764)));
}
);

Deno.test(
"Task: #ap - Composition",
async () => {
const containerA = Task.of(42);
const containerB = Task.of(x => x + 2);
const containerC = Task.of(x => x * 2);
assert(Task.is(containerA));
assert(Task.is(containerB));
assert(Task.is(containerC));

const containerD = await containerA.ap(containerB.ap(containerC.map(a => b => c => a(b(c))))).run();
const containerE = await containerA.ap(containerB).ap(containerC).run();
assert(Either.is(containerD));
assert(Either.is(containerE));

assertIsEquivalent(containerD, containerE);
}
);

Deno.test(
"Task: #ap with lift",
async () => {
const lift2 = (f, a, b) => b.ap(a.map(f));

const containerA = Task.of(42);
const containerB = Task.of(32);
const containerC = Task.of(x => y => x * y);
assert(Task.is(containerA));
assert(Task.is(containerB));
assert(Task.is(containerC));

const containerD = await lift2(x => y => x * y, containerA, containerB).run();
assert(Either.is(containerD));

assertIsEquivalent(containerD, Either.Right(1344));
}
);

Deno.test(
"Task: #map (promise)",
() => {
Expand Down

0 comments on commit 9779caa

Please sign in to comment.