Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support recent versions of the Fantasy Land specification #1

Merged
merged 1 commit into from
May 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 57 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ The `LazyEither` type is used to represent a lazy `Either` value. It is similar

The implementation is more favorable than the `Future` type because it is very easy to compose elegant pipelines, and it handles errors nicely. If `fork`-like branching is desired, it can be done just by resolving the pipeline using `value` and checking whether the result `isLeft` or `isRight` (though branching is not usually needed). See the examples section for more details.

The implementation follows the [Fantasy Land](https://github.com/fantasyland/fantasy-land) specifications. The `LazyEither` type is a `Functor`, `Applicative` and `Monad`. It is not (necessarily) a `Setoid` due to it's lazy/deferred nature.
The implementation follows the [Fantasy Land](https://github.com/fantasyland/fantasy-land) specifications. The `LazyEither` type is a `Functor`, `Applicative` and `Monad`. It is not (necessarily) a `Setoid` due to its lazy/deferred nature.


## Construction

The `LazyEither` type consists of a single constructor that accepts a function which must accept a continuation function used to resolve the `LazyEither` instance into an `Either` type:
The `LazyEither` type consists of a single constructor that accepts a function which must accept a continuation function used to resolve the `LazyEither` instance into an `Either` type:

```hs
LazyEither :: ((Either e a -> ()) -> ()) -> LazyEither e a
Expand All @@ -21,42 +21,48 @@ The resolved instance should be an [Either type](https://github.com/ramda/ramda-


```js
//:: Number -> a -> LazyEither Error a
//:: (Number, a) -> LazyEither (Either e a)
let delayed = (ms, val) => LazyEither(resolve => {
ms > 1000 ? resolve(S.Left(Error('Delay too long')))
: setTimeout(() => resolve(S.Right(val)), ms)
})
```

```js
delayed(500, 'Hello').value(result => console.log(result)) // returns Right('Hello')
delayed(1001, 'Hey').value(result => console.log(result)) // returns Left(Error('Delay too long'))
delayed(500, 'Hello').value(console.log) // returns Right('Hello')
delayed(1001, 'Hey').value(console.log) // returns Left(Error('Delay too long'))
```


## Interaction

Once a `LazyEither` instance has been created, the various methods attached to the instance can be used to instruct further transformations to take place. Nothing is actually executed until the `value` or `equals` method is called.
Once a `LazyEither` instance has been created, the various methods attached to the instance can be used to instruct further transformations to take place. Nothing is actually executed until the `value` or `fantasy-land/equals` method is called.

The `map`, `ap` and `chain` functions can be used to transform resolved values of a `LazyEither` instance.
The `fantasy-land/map`, `fantasy-land/ap` and `fantasy-land/chain` functions can be used to transform resolved values of a `LazyEither` instance.

```js
//:: String -> LazyEither Error [String]
let ls = path => LazyEither(resolve =>
fs.readdir(path, (err, files) => resolve(err ? S.Left(err) : S.Right(files))))
//:: String -> String -> String
const join = S.curry2(path.join)

//:: String -> LazyEither Error String
let cat = file => LazyEither(resolve =>
fs.readFile(file, 'utf8', (err, data) => resolve(err ? S.Left(err) : S.Right(data))))
//:: String -> LazyEither (Either e [String])
const ls = path => LazyEither(resolve =>
fs.readdir(path, (err, files) => resolve(err ? S.Left(err) : S.Right(S.map(join(path), files)))))

//:: String -> LazyEither Error String
let catDir = dir => ls(dir.value).chain(R.traverse(LazyEither.of, cat)).map(R.join('\n'))
//:: String -> LazyEither (Either e String)
const cat = file => LazyEither(resolve =>
fs.readFile(file, {encoding: 'utf8'}, (err, data) => resolve(err ? S.Left(err) : S.Right(data))))

//:: String -> LazyEither (Either e String)
const catDir =
S.pipe([ls,
S.chain(S.traverse(LazyEither, cat)),
S.map(S.unlines)])
```

A `LazyEither` instance is executed when `value` or `equals` gets called:
A `LazyEither` instance is executed when `value` or `fantasy-land/equals` gets called:

```js
catDir(S.Right('.')).value(data => console.log(data.value))
catDir(os.homedir()).value(S.either(console.error, console.log))
```


Expand All @@ -69,45 +75,54 @@ catDir(S.Right('.')).value(data => console.log(data.value))
```hs
:: ((Either e a -> ()) -> ()) -> LazyEither e a
```
Constructs a `LazyEither` instance that represents some action that may possibly fail. It takes a function which must accept a continuation function that takes an `Either` type used to represent success or failure.

Constructs a `LazyEither` instance that represents some action that may possibly fail. It takes a function which must accept a continuation function that takes an `Either` type used to represent success or failure.

#### `LazyEither.Right`

```hs
:: a -> LazyEither e a
```

Creates a `LazyEither` instance that resolves to a `Right` with the given value.

#### `LazyEither.Left`

```hs
:: e -> LazyEither e a
```

Creates a `LazyEither` instance that resolves to a `Left` with the given value.


### Static methods

#### `LazyEither.of`
#### `LazyEither['fantasy-land/of']`

```hs
:: a -> LazyEither e a
```

Creates a pure instance that resolves to a `Right` with the given value. This is also an instance method.
Creates a pure instance that resolves to a `Right` with the given value.

#### `LazyEither.lift`

```hs
:: (a -> b) -> a -> LazyEither e b
```

Lifts a function of arity `1` into one that returns a `LazyEither` instance.

#### `LazyEither.liftN`

```hs
:: n -> (a -> .. -> z) -> a -> .. -> z -> LazyEither e z
```

Lifts a function of arity `n` into one that returns a `LazyEither` instance.

#### `LazyEither.promote`

```hs
:: Either a b -> LazyEither a b
```
Expand All @@ -117,38 +132,50 @@ Promotes an `Either` type to a `LazyEither` type.

### Instance methods

#### `lazyEither.map`
#### `lazyEither['fantasy-land/map']`

```hs
:: LazyEither e a ~> (a -> b) -> LazyEither e b
```

Transforms the resolved `Either` value of this `LazyEither` instance with the given function. If the instance resolves as a `Left` value, the provided function is not called and the returned `LazyEither` instance will resolve with that `Left` value.

#### `lazyEither.ap`
#### `lazyEither['fantasy-land/ap']`

```hs
:: LazyEither e (a -> b) ~> LazyEither e a -> LazyEither e b
:: LazyEither e a ~> LazyEither e (a -> b) -> LazyEither e b
```
Applies the `Either` function of this `LazyEither` instance to the `Either` value of the provided `LazyEither` instance, producing a `LazyEither` instance of the result. If either `LazyEither` resolves as a `Left` value, then the returned `LazyEither` instance will resolve with that `Left` value.

#### `lazyEither.chain`
Applies the `Either` function of the provided `LazyEither` instance to the `Either` value of this `LazyEither` instance, producing a `LazyEither` instance of the result. If either `LazyEither` resolves as a `Left` value, then the returned `LazyEither` instance will resolve with that `Left` value.

#### `lazyEither['fantasy-land/chain']`

```hs
:: LazyEither e a ~> (a -> LazyEither e b) -> LazyEither e b
```

Calls the provided function with the value of this `LazyEither` instance, returning the new `LazyEither` instance. If either `LazyEither` instance resolves as a `Left` value, the returned `LazyEither` instance will resolve with that `Left` value. The provided function can be used to try to recover the error.

#### `lazyEither.bimap`
#### `lazyEither['fantasy-land/bimap']`

```hs
:: LazyEither e a ~> (e -> f) -> (a -> b) -> LazyEither f b
```

Uses the provided functions to transform this `LazyEither` instance when it resolves to a `Left` or a `Right` value, respectively.

#### `lazyEither.value`

```hs
:: LazyEither e a ~> ((Either e a) -> ()) -> ()
:: LazyEither e a ~> (Either e a -> ()) -> ()
```

Calls the provided function with the value of this `LazyEither` instance without returning a new `LazyEither` instance. It is similar to `Future.fork`. This function can be used as a final processing step for the returned `Either` value, or to create a branch of two seperate execution streams to handle the resolved `Left` or `Right` value.

#### `lazyEither.equals`
#### `lazyEither['fantasy-land/equals']`

```hs
:: LazyEither a b ~> LazyEither c d -> ((Boolean) -> ()) -> ()
:: LazyEither a b ~> LazyEither c d -> (Boolean -> ()) -> ()
```
Compares the `Either` value of this `LazyEither` instance with the `Either` value of the provided `LazyEither` instance, and calls the provided function with the `Boolean` result of the comparison. Like `value`, this function will resolve the pipeline. The result will return `True` if both `Either` values match or `False` if they do not match.

Compares the `Either` value of this `LazyEither` instance with the `Either` value of the provided `LazyEither` instance, and calls the provided function with the `Boolean` result of the comparison. Like `value`, this function will resolve the pipeline. The result will return `true` if both `Either` values match or `false` if they do not match.
8 changes: 4 additions & 4 deletions examples/readme1.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use strict'
const LazyEither = require('lazy-either').LazyEither
const LazyEither = require('..')
const S = require('sanctuary')

//:: (Num -> a) -> LazyEither (Either e a)
//:: (Number, a) -> LazyEither (Either e a)
let delayed = (ms, val) => LazyEither(resolve => {
ms > 1000 ? resolve(S.Left(Error('Delay too long')))
: setTimeout(() => resolve(S.Right(val)), ms)
})

delayed(500, 'Hello').value(result => console.log(result)) // returns Right('Hello')
delayed(1001, 'Hey').value(result => console.log(result)) // returns Left(Error('Delay too long'))
delayed(500, 'Hello').value(console.log) // returns Right('Hello')
delayed(1001, 'Hey').value(console.log) // returns Left(Error('Delay too long'))
28 changes: 18 additions & 10 deletions examples/readme2.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
'use strict'
const LazyEither = require('lazy-either').LazyEither
const LazyEither = require('..')
const fs = require('fs')
, os = require('os')
, path = require('path')
, R = require('ramda')
, S = require('sanctuary')

//:: (Either e String) -> LazyEither (Either e [String])
let ls = path => LazyEither(resolve =>
fs.readdir(path, (err, files) => resolve(err ? S.Left(err) : S.Right(files))))
//:: String -> String -> String
const join = S.curry2(path.join)

//:: (Either e String) -> LazyEither (Either e String)
let cat = file => LazyEither(resolve =>
fs.readFile(file, 'utf8', (err, data) => resolve(err ? S.Left(err) : S.Right(data))))
//:: String -> LazyEither (Either e [String])
const ls = path => LazyEither(resolve =>
fs.readdir(path, (err, files) => resolve(err ? S.Left(err) : S.Right(S.map(join(path), files)))))

//:: (Either e String) -> LazyEither (Either e String)
let catDir = dir => ls(dir.value).chain(R.traverse(LazyEither.of, cat)).map(R.join('\n'))
//:: String -> LazyEither (Either e String)
const cat = file => LazyEither(resolve =>
fs.readFile(file, {encoding: 'utf8'}, (err, data) => resolve(err ? S.Left(err) : S.Right(data))))

//:: String -> LazyEither (Either e String)
const catDir =
S.pipe([ls,
S.chain(S.traverse(LazyEither, cat)),
S.map(S.unlines)])

// A LazyEither instance is executed when value gets called:
catDir(S.Right('.')).value(data => console.log(data.value))
catDir(os.homedir()).value(S.either(console.error, console.log))
57 changes: 33 additions & 24 deletions examples/vs-future1.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
'use strict'
/* The is the implementation using LazyEither. Notice that it's just one elegant pipeK with the safety of Either */
const R = require('ramda')
, S = require('sanctuary')
, fs = require('fs')
const LazyEither = require('lazy-either').LazyEither

let writeFile = R.curry((file, data) => {
return new LazyEither(resolve => {
fs.writeFile(file, data, err => resolve(err ? S.Left(err) : S.Right('Write successful')))
})
})
, os = require('os')
, path = require('path')
const LazyEither = require('..')

let readFile = path => {
return new LazyEither(resolve => {
fs.readFile(path, (err, data) => resolve(err ? S.Left(err) : S.Right(data.toString())))
})
}
const join = S.curry2(path.join)

let readDir = path => {
return new LazyEither(resolve => {
fs.readdir(path, (err, files) => resolve(err ? S.Left(err) : S.Right(files)))
const readDir = path =>
new LazyEither(resolve => {
fs.readdir(path,
(err, files) => resolve(err != null ? S.Left(err) : S.Right(S.map(join(path), files))))
})
}

let filterJs = R.pipe(R.filter(file => /\.js$/.test(file)), LazyEither.of)
, readFiles = files => R.traverse(LazyEither.of, readFile, files)
const readFile = path =>
new LazyEither(resolve => {
fs.readFile(path,
{encoding: 'utf8'},
(err, data) => resolve(err != null ? S.Left(err) : S.Right(data)))
})

let numRequires = file => LazyEither.Right(file.toString().split(/require\([^)]+\)/).length - 1)
, printTotal = total => LazyEither.Right(`Total requires: ${total}`)
const writeFile = S.curry2((file, data) =>
new LazyEither(resolve => {
fs.writeFile(file,
data,
err => resolve(err != null ? S.Left(err) : S.Right('Write successful')))
})
)

let getStats = R.pipeK(readDir, filterJs, readFiles, numRequires, printTotal, writeFile('stats.txt'))
const getStats =
S.pipe([readDir,
S.map(S.filter(S.test(/[.]js$/))),
S.chain(S.traverse(LazyEither, readFile)),
S.map(String),
S.map(S.matchAll(/require[(][^)]+[)]/g)),
S.map(S.prop('length')),
S.map(String),
S.map(S.concat('Total requires: ')),
S.chain(writeFile('stats.txt'))])

getStats(LazyEither.Right('.')).value(val => console.log(val))
//getStats(LazyEither.Right('blablah')).value(val => console.log(val))
getStats(__dirname).value(console.log)
//getStats('blablah').value(console.log)
2 changes: 1 addition & 1 deletion examples/vs-future2.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict'
/* This is the implementation using Futures. Notice how it doesn't have that extra type safety with Either,
/* This is the implementation using Futures. Notice how it doesn't have that extra type safety with Either,
* the chain has a kink (chain1 and chain2), and worst of all, the pyramid of doom is back!
*/
const futurizer = require('futurizer').futurizer
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@
"license": "MIT",
"devDependencies": {
"chai": "^3.5.0",
"fantasy-land": "^3.2.0",
"mocha": "^2.4.5"
},
"dependencies": {
"ramda": "^0.19.1",
"sanctuary": "^0.9.0"
"sanctuary": "^0.12.2"
}
}
Loading