├── .gitignore ├── .travis.yml ├── index.js ├── examples ├── readme1.js ├── readme2.js ├── vs-future1.js └── vs-future2.js ├── package.json ├── LICENSE ├── src └── either.js ├── README.md └── test └── either.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/either').LazyEither 2 | -------------------------------------------------------------------------------- /examples/readme1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const LazyEither = require('..') 3 | const S = require('sanctuary') 4 | 5 | //:: (Number, a) -> LazyEither (Either e a) 6 | let delayed = (ms, val) => LazyEither(resolve => { 7 | ms > 1000 ? resolve(S.Left(Error('Delay too long'))) 8 | : setTimeout(() => resolve(S.Right(val)), ms) 9 | }) 10 | 11 | delayed(500, 'Hello').value(console.log) // returns Right('Hello') 12 | delayed(1001, 'Hey').value(console.log) // returns Left(Error('Delay too long')) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lazy-either", 3 | "version": "2.0.0", 4 | "description": "A lazy implementation of the Fantasy Land Either type", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "engines": { 10 | "node": "4.1.0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://www.github.com/Risto-Stevcev/lazy-either.git" 15 | }, 16 | "keywords": [ 17 | "lazy", 18 | "either", 19 | "type", 20 | "fantasy", 21 | "land", 22 | "monad" 23 | ], 24 | "author": "Risto Stevcev", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "chai": "^3.5.0", 28 | "fantasy-land": "^3.2.0", 29 | "mocha": "^2.4.5" 30 | }, 31 | "dependencies": { 32 | "ramda": "^0.19.1", 33 | "sanctuary": "^0.12.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/readme2.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const LazyEither = require('..') 3 | const fs = require('fs') 4 | , os = require('os') 5 | , path = require('path') 6 | , R = require('ramda') 7 | , S = require('sanctuary') 8 | 9 | //:: String -> String -> String 10 | const join = S.curry2(path.join) 11 | 12 | //:: String -> LazyEither (Either e [String]) 13 | const ls = path => LazyEither(resolve => 14 | fs.readdir(path, (err, files) => resolve(err ? S.Left(err) : S.Right(S.map(join(path), files))))) 15 | 16 | //:: String -> LazyEither (Either e String) 17 | const cat = file => LazyEither(resolve => 18 | fs.readFile(file, {encoding: 'utf8'}, (err, data) => resolve(err ? S.Left(err) : S.Right(data)))) 19 | 20 | //:: String -> LazyEither (Either e String) 21 | const catDir = 22 | S.pipe([ls, 23 | S.chain(S.traverse(LazyEither, cat)), 24 | S.map(S.unlines)]) 25 | 26 | // A LazyEither instance is executed when value gets called: 27 | catDir(os.homedir()).value(S.either(console.error, console.log)) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Risto Stevcev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/vs-future1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const R = require('ramda') 3 | , S = require('sanctuary') 4 | , fs = require('fs') 5 | , os = require('os') 6 | , path = require('path') 7 | const LazyEither = require('..') 8 | 9 | const join = S.curry2(path.join) 10 | 11 | const readDir = path => 12 | new LazyEither(resolve => { 13 | fs.readdir(path, 14 | (err, files) => resolve(err != null ? S.Left(err) : S.Right(S.map(join(path), files)))) 15 | }) 16 | 17 | const readFile = path => 18 | new LazyEither(resolve => { 19 | fs.readFile(path, 20 | {encoding: 'utf8'}, 21 | (err, data) => resolve(err != null ? S.Left(err) : S.Right(data))) 22 | }) 23 | 24 | const writeFile = S.curry2((file, data) => 25 | new LazyEither(resolve => { 26 | fs.writeFile(file, 27 | data, 28 | err => resolve(err != null ? S.Left(err) : S.Right('Write successful'))) 29 | }) 30 | ) 31 | 32 | const getStats = 33 | S.pipe([readDir, 34 | S.map(S.filter(S.test(/[.]js$/))), 35 | S.chain(S.traverse(LazyEither, readFile)), 36 | S.map(String), 37 | S.map(S.matchAll(/require[(][^)]+[)]/g)), 38 | S.map(S.prop('length')), 39 | S.map(String), 40 | S.map(S.concat('Total requires: ')), 41 | S.chain(writeFile('stats.txt'))]) 42 | 43 | getStats(__dirname).value(console.log) 44 | //getStats('blablah').value(console.log) 45 | -------------------------------------------------------------------------------- /examples/vs-future2.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* This is the implementation using Futures. Notice how it doesn't have that extra type safety with Either, 3 | * the chain has a kink (chain1 and chain2), and worst of all, the pyramid of doom is back! 4 | */ 5 | const futurizer = require('futurizer').futurizer 6 | , R = require('ramda') 7 | , fs = require('fs') 8 | 9 | const Either = require('ramda-fantasy').Either 10 | , Future = require('ramda-fantasy').Future 11 | 12 | let readFile = futurizer(fs.readFile) // returns a buffer 13 | , readDir = futurizer(fs.readdir) 14 | 15 | let writeFile = R.curry((file, data) => { 16 | return Future((reject, resolve) => { 17 | fs.writeFile(file, data, (err) => { 18 | if (err) 19 | reject(err) 20 | else 21 | resolve('Write successful') 22 | }) 23 | }) 24 | }) 25 | 26 | let filterJs = R.pipe(R.filter(file => /\.js$/.test(file)), Future.of) 27 | , readFiles = files => R.traverse(Future.of, readFile, files) 28 | 29 | let numRequires = file => file.toString().split(/require\([^)]+\)/).length - 1 30 | 31 | let chain1 = path => readDir(path).chain(filterJs).chain(readFiles) 32 | , chain2 = R.pipe(R.map(numRequires), R.reduce(R.add, 0), R.concat('Total requires: ')) 33 | 34 | /** The Pyramid of Doom (a.k.a. callback hell) **/ 35 | chain1('.').fork( 36 | error => { 37 | console.error(`${error.name}: ${error.message}`) 38 | }, 39 | data => { 40 | R.pipe(chain2, writeFile('stats.txt'))(data).fork( 41 | error => { 42 | console.error(`${error.name}: ${error.message}`) 43 | }, 44 | data => { 45 | console.log(data) 46 | } 47 | ) 48 | } 49 | ) 50 | -------------------------------------------------------------------------------- /src/either.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const R = require('ramda') 3 | , S = require('sanctuary') 4 | 5 | const LazyEither = function(f) { 6 | if (!(this instanceof LazyEither)) { 7 | return new LazyEither(f) 8 | } 9 | this._value = f 10 | } 11 | 12 | LazyEither.prototype.value = function(resolve) { 13 | this._value(resolve) 14 | } 15 | 16 | LazyEither['fantasy-land/of'] = LazyEither.Right = function(x) { 17 | return new LazyEither(S.T(S.Right(x))) 18 | } 19 | 20 | LazyEither.Left = function(x) { 21 | return new LazyEither(S.T(S.Left(x))) 22 | } 23 | 24 | LazyEither.prototype['fantasy-land/chain'] = function(f) { 25 | const self = this 26 | return new LazyEither(resolve => { 27 | self._value(S.either(a => resolve(S.Left(a)), b => f(b)._value(resolve))) 28 | }) 29 | } 30 | 31 | LazyEither.prototype['fantasy-land/map'] = function(f) { 32 | return S.chain(S.compose(LazyEither.Right, f), this) 33 | } 34 | 35 | LazyEither.prototype['fantasy-land/bimap'] = function(leftFn, rightFn) { 36 | const self = this 37 | return new LazyEither(resolve => { 38 | self._value(S.compose(resolve, S.either(leftFn, rightFn))) 39 | }) 40 | } 41 | 42 | LazyEither.prototype['fantasy-land/ap'] = function(other) { 43 | const self = this 44 | return new LazyEither(resolve => { 45 | let applyFn, val 46 | const doReject = R.once(resolve) 47 | 48 | const resolveIfDone = () => { 49 | if (applyFn && applyFn.isLeft) 50 | doReject(applyFn) 51 | else if (val && val.isLeft) 52 | doReject(val) 53 | else if (applyFn != null && val != null) 54 | resolve(S.Right(applyFn.value(val.value))) 55 | } 56 | 57 | other._value(fn => { 58 | applyFn = fn 59 | resolveIfDone() 60 | }) 61 | 62 | self._value(result => { 63 | val = result 64 | resolveIfDone() 65 | }) 66 | }) 67 | } 68 | 69 | LazyEither.prototype['fantasy-land/equals'] = function(other, resolve) { 70 | this._value(res => { 71 | other._value(S.compose(resolve, S.equals(res))) 72 | }) 73 | } 74 | 75 | LazyEither.lift = f => R.pipe(f, LazyEither.Right) 76 | LazyEither.liftN = (n, f) => R.curryN(n, R.pipe(f, LazyEither.Right)) 77 | 78 | LazyEither.promote = S.compose(LazyEither, S.T) 79 | 80 | module.exports = { LazyEither: LazyEither } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 🦋 [Fluture](https://github.com/fluture-js/Fluture) is a similar project but which is actively developed and provides more features. 4 | 5 | --- 6 | 7 | # LazyEither 8 | 9 | [![Build Status](https://travis-ci.org/Risto-Stevcev/lazy-either.svg)](https://travis-ci.org/Risto-Stevcev/lazy-either) 10 | 11 | The `LazyEither` type is used to represent a lazy `Either` value. It is similar to the `Future` and `Promise` types. The constructor continuation function parameter and *eventual* return value is an `Either` type. The execution is delayed until the value is requested using one of it's methods. 12 | 13 | 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. 14 | 15 | 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. 16 | 17 | 18 | ## Construction 19 | 20 | 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: 21 | 22 | ```hs 23 | LazyEither :: ((Either e a -> ()) -> ()) -> LazyEither e a 24 | ``` 25 | 26 | The resolved instance should be an [Either type](https://github.com/ramda/ramda-fantasy/blob/master/docs/Either.md). 27 | 28 | 29 | ```js 30 | //:: (Number, a) -> LazyEither (Either e a) 31 | let delayed = (ms, val) => LazyEither(resolve => { 32 | ms > 1000 ? resolve(S.Left(Error('Delay too long'))) 33 | : setTimeout(() => resolve(S.Right(val)), ms) 34 | }) 35 | ``` 36 | 37 | ```js 38 | delayed(500, 'Hello').value(console.log) // returns Right('Hello') 39 | delayed(1001, 'Hey').value(console.log) // returns Left(Error('Delay too long')) 40 | ``` 41 | 42 | 43 | ## Interaction 44 | 45 | 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. 46 | 47 | The `fantasy-land/map`, `fantasy-land/ap` and `fantasy-land/chain` functions can be used to transform resolved values of a `LazyEither` instance. 48 | 49 | ```js 50 | //:: String -> String -> String 51 | const join = S.curry2(path.join) 52 | 53 | //:: String -> LazyEither (Either e [String]) 54 | const ls = path => LazyEither(resolve => 55 | fs.readdir(path, (err, files) => resolve(err ? S.Left(err) : S.Right(S.map(join(path), files))))) 56 | 57 | //:: String -> LazyEither (Either e String) 58 | const cat = file => LazyEither(resolve => 59 | fs.readFile(file, {encoding: 'utf8'}, (err, data) => resolve(err ? S.Left(err) : S.Right(data)))) 60 | 61 | //:: String -> LazyEither (Either e String) 62 | const catDir = 63 | S.pipe([ls, 64 | S.chain(S.traverse(LazyEither, cat)), 65 | S.map(S.unlines)]) 66 | ``` 67 | 68 | A `LazyEither` instance is executed when `value` or `fantasy-land/equals` gets called: 69 | 70 | ```js 71 | catDir(os.homedir()).value(S.either(console.error, console.log)) 72 | ``` 73 | 74 | 75 | ## Reference 76 | 77 | ### Constructors 78 | 79 | #### `LazyEither` 80 | 81 | ```hs 82 | :: ((Either e a -> ()) -> ()) -> LazyEither e a 83 | ``` 84 | 85 | 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. 86 | 87 | #### `LazyEither.Right` 88 | 89 | ```hs 90 | :: a -> LazyEither e a 91 | ``` 92 | 93 | Creates a `LazyEither` instance that resolves to a `Right` with the given value. 94 | 95 | #### `LazyEither.Left` 96 | 97 | ```hs 98 | :: e -> LazyEither e a 99 | ``` 100 | 101 | Creates a `LazyEither` instance that resolves to a `Left` with the given value. 102 | 103 | 104 | ### Static methods 105 | 106 | #### `LazyEither['fantasy-land/of']` 107 | 108 | ```hs 109 | :: a -> LazyEither e a 110 | ``` 111 | 112 | Creates a pure instance that resolves to a `Right` with the given value. 113 | 114 | #### `LazyEither.lift` 115 | 116 | ```hs 117 | :: (a -> b) -> a -> LazyEither e b 118 | ``` 119 | 120 | Lifts a function of arity `1` into one that returns a `LazyEither` instance. 121 | 122 | #### `LazyEither.liftN` 123 | 124 | ```hs 125 | :: n -> (a -> .. -> z) -> a -> .. -> z -> LazyEither e z 126 | ``` 127 | 128 | Lifts a function of arity `n` into one that returns a `LazyEither` instance. 129 | 130 | #### `LazyEither.promote` 131 | 132 | ```hs 133 | :: Either a b -> LazyEither a b 134 | ``` 135 | 136 | Promotes an `Either` type to a `LazyEither` type. 137 | 138 | 139 | ### Instance methods 140 | 141 | #### `lazyEither['fantasy-land/map']` 142 | 143 | ```hs 144 | :: LazyEither e a ~> (a -> b) -> LazyEither e b 145 | ``` 146 | 147 | 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. 148 | 149 | #### `lazyEither['fantasy-land/ap']` 150 | 151 | ```hs 152 | :: LazyEither e a ~> LazyEither e (a -> b) -> LazyEither e b 153 | ``` 154 | 155 | 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. 156 | 157 | #### `lazyEither['fantasy-land/chain']` 158 | 159 | ```hs 160 | :: LazyEither e a ~> (a -> LazyEither e b) -> LazyEither e b 161 | ``` 162 | 163 | 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. 164 | 165 | #### `lazyEither['fantasy-land/bimap']` 166 | 167 | ```hs 168 | :: LazyEither e a ~> (e -> f) -> (a -> b) -> LazyEither f b 169 | ``` 170 | 171 | Uses the provided functions to transform this `LazyEither` instance when it resolves to a `Left` or a `Right` value, respectively. 172 | 173 | #### `lazyEither.value` 174 | 175 | ```hs 176 | :: LazyEither e a ~> (Either e a -> ()) -> () 177 | ``` 178 | 179 | 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. 180 | 181 | #### `lazyEither['fantasy-land/equals']` 182 | 183 | ```hs 184 | :: LazyEither a b ~> LazyEither c d -> (Boolean -> ()) -> () 185 | ``` 186 | 187 | 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. 188 | -------------------------------------------------------------------------------- /test/either.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const expect = require('chai').expect 3 | const LazyEither = require('../index') 4 | const FL = require('fantasy-land') 5 | , R = require('ramda') 6 | , S = require('sanctuary') 7 | 8 | describe('Constructors', function() { 9 | it('should construct a Lazy Left value', function(done) { 10 | LazyEither.Left('bad').value(res => { 11 | expect(res).to.deep.equal(S.Left('bad')) 12 | done() 13 | }) 14 | }) 15 | 16 | it('should construct a Lazy Right value', function(done) { 17 | LazyEither.Right('good').value(res => { 18 | expect(res).to.deep.equal(S.Right('good')) 19 | done() 20 | }) 21 | }) 22 | 23 | it('should construct using LazyEither["fantasy-land/of"]', function(done) { 24 | S.of(LazyEither, 'good').value(res => { 25 | expect(res).to.deep.equal(S.Right('good')) 26 | done() 27 | }) 28 | }) 29 | 30 | it('should handle async delayed values', function(done) { 31 | let startTime = Date.now() 32 | LazyEither(resolve => { 33 | setTimeout(() => { 34 | return resolve(S.Right('hello')) 35 | }, 1000) 36 | }).value(res => { 37 | let endTime = Date.now() 38 | expect(res).to.deep.equal(S.Right('hello')) 39 | expect(endTime - startTime).to.be.closeTo(1000, 100) 40 | done() 41 | }) 42 | }) 43 | }) 44 | 45 | 46 | 47 | describe('Chain', function() { 48 | before(function() { 49 | this.delayed = t => LazyEither(resolve => { 50 | if (t > 1000) 51 | return resolve(S.Left('Delay too long')) 52 | 53 | setTimeout(() => { 54 | return resolve(S.Right('hello')) 55 | }, t) 56 | }) 57 | }) 58 | 59 | it('should not execute the rest of the chain if it fails and should propagate the Left value', function(done) { 60 | let startTime = Date.now() 61 | LazyEither.Left('bad')[FL.chain](this.delayed).value(res => { 62 | let endTime = Date.now() 63 | expect(res).to.deep.equal(S.Left('bad')) 64 | expect(endTime - startTime).to.be.closeTo(0, 100) 65 | done() 66 | }) 67 | }) 68 | 69 | it('should pass the result to the next item in the chain (1)', function(done) { 70 | let startTime = Date.now() 71 | LazyEither.Right(1500)[FL.chain](this.delayed).value(res => { 72 | let endTime = Date.now() 73 | expect(res).to.deep.equal(S.Left('Delay too long')) 74 | expect(endTime - startTime).to.be.closeTo(0, 100) 75 | done() 76 | }) 77 | }) 78 | 79 | it('should pass the result to the next item in the chain (2)', function(done) { 80 | let startTime = Date.now() 81 | LazyEither.Right(800)[FL.chain](this.delayed).value(res => { 82 | let endTime = Date.now() 83 | expect(res).to.deep.equal(S.Right('hello')) 84 | expect(endTime - startTime).to.be.closeTo(800, 100) 85 | done() 86 | }) 87 | }) 88 | 89 | it('should pass the result to the next item in the chain (3)', function(done) { 90 | let startTime = Date.now() 91 | LazyEither.Right(1001)[FL.chain](this.delayed)[FL.chain](LazyEither.lift(R.identity)).value(res => { 92 | let endTime = Date.now() 93 | expect(res).to.deep.equal(S.Left('Delay too long')) 94 | expect(endTime - startTime).to.be.closeTo(0, 100) 95 | done() 96 | }) 97 | }) 98 | 99 | it('should pass the result to the next item in the chain (4)', function(done) { 100 | let startTime = Date.now() 101 | LazyEither.Right(1001)[FL.chain](LazyEither.lift(a => a - 1))[FL.chain](this.delayed).value(res => { 102 | let endTime = Date.now() 103 | expect(res).to.deep.equal(S.Right('hello')) 104 | expect(endTime - startTime).to.be.closeTo(1000, 100) 105 | done() 106 | }) 107 | }) 108 | 109 | it('should pass the result to the next item in the chain (5)', function(done) { 110 | let startTime = Date.now() 111 | LazyEither.Right(200)[FL.chain](this.delayed)[FL.chain](LazyEither.lift(hi => `${hi} world`)).value(res => { 112 | let endTime = Date.now() 113 | expect(res).to.deep.equal(S.Right('hello world')) 114 | expect(endTime - startTime).to.be.closeTo(200, 100) 115 | done() 116 | }) 117 | }) 118 | 119 | it('should satisfy associativity', function(done) { 120 | let val1, val2 121 | let add3 = LazyEither.lift(R.add(3)) 122 | , mult2 = LazyEither.lift(R.multiply(2)) 123 | 124 | let resolveIfDone = _ => { if (val1 && val2) done() } 125 | 126 | LazyEither.Right(5)[FL.chain](add3)[FL.chain](mult2).value(res => { 127 | val1 = res 128 | expect(res).to.deep.equal(S.Right(16)) 129 | resolveIfDone() 130 | }) 131 | 132 | LazyEither.Right(5)[FL.chain](x => add3(x)[FL.chain](mult2)).value(res => { 133 | val2 = res 134 | expect(res).to.deep.equal(S.Right(16)) 135 | resolveIfDone() 136 | }) 137 | }) 138 | }) 139 | 140 | 141 | 142 | describe('Functor', function() { 143 | it('should apply the map to a Right value (1)', function(done) { 144 | LazyEither.Right(1)[FL.map](R.add(7)).value(res => { 145 | expect(res).to.deep.equal(S.Right(8)) 146 | done() 147 | }) 148 | }) 149 | 150 | it('should apply the map to a Right value (2)', function(done) { 151 | LazyEither.Right('foo')[FL.map](a => `${a} bar`)[FL.map](a => `${a} baz`).value(res => { 152 | expect(res).to.deep.equal(S.Right('foo bar baz')) 153 | done() 154 | }) 155 | }) 156 | 157 | it('should not apply the map function to a Left value (1)', function(done) { 158 | LazyEither.Left('bad')[FL.map](a => `${a} bar`)[FL.map](a => `${a} baz`).value(res => { 159 | expect(res).to.deep.equal(S.Left('bad')) 160 | done() 161 | }) 162 | }) 163 | 164 | it('should satisfy identity (1)', function(done) { 165 | LazyEither.Right('good')[FL.map](R.identity).value(res => { 166 | expect(res).to.deep.equal(S.Right('good')) 167 | done() 168 | }) 169 | }) 170 | 171 | it('should satisfy identity (2)', function(done) { 172 | LazyEither.Left('bad')[FL.map](R.identity).value(res => { 173 | expect(res).to.deep.equal(S.Left('bad')) 174 | done() 175 | }) 176 | }) 177 | 178 | it('should satisfy composition', function(done) { 179 | let val1, val2 180 | let resolveIfDone = _ => { if (val1 && val2) done() } 181 | 182 | LazyEither.Right(5)[FL.map](R.add(3))[FL.map](R.multiply(2)).value(res => { 183 | val1 = res 184 | expect(res).to.deep.equal(S.Right(16)) 185 | resolveIfDone() 186 | }) 187 | 188 | LazyEither.Right(5)[FL.map](x => R.multiply(2)(R.add(3, x))).value(res => { 189 | val2 = res 190 | expect(res).to.deep.equal(S.Right(16)) 191 | resolveIfDone() 192 | }) 193 | }) 194 | }) 195 | 196 | 197 | 198 | describe('Applicative', function() { 199 | it('should be apply the ap function', function(done) { 200 | LazyEither.Right(4)['fantasy-land/ap'](LazyEither.Right(R.multiply(3))).value(res => { 201 | expect(res).to.deep.equal(S.Right(12)) 202 | done() 203 | }) 204 | }) 205 | 206 | it('should propagate a Left value (1)', function(done) { 207 | LazyEither.Right(2)[FL.ap](LazyEither.Left('bad')).value(res => { 208 | expect(res).to.deep.equal(S.Left('bad')) 209 | done() 210 | }) 211 | }) 212 | 213 | it('should propagate a Left value (2)', function(done) { 214 | LazyEither.Left('bad')[FL.ap](LazyEither.Right(R.add(5))).value(res => { 215 | expect(res).to.deep.equal(S.Left('bad')) 216 | done() 217 | }) 218 | }) 219 | 220 | it('should be able to compose multiple ap functions', function(done) { 221 | LazyEither.Right(5)[FL.ap](LazyEither.Right(R.add(3)))[FL.ap](LazyEither.Right(R.multiply(2))).value(res => { 222 | expect(res).to.deep.equal(S.Right(16)) 223 | done() 224 | }) 225 | }) 226 | 227 | it('should satisfy identity', function(done) { 228 | LazyEither.Right('good')[FL.ap](LazyEither.Right(R.identity)).value(res => { 229 | expect(res).to.deep.equal(S.Right('good')) 230 | done() 231 | }) 232 | }) 233 | 234 | it('should satisfy homomorphism', function(done) { 235 | LazyEither.Right(5)[FL.ap](LazyEither.Right(R.add(3))).value(res => { 236 | LazyEither.Right(R.add(3, 5)).value(res2 => { 237 | expect(res).to.deep.equal(res2) 238 | done() 239 | }) 240 | }) 241 | }) 242 | 243 | it('should satisfy interchange', function(done) { 244 | LazyEither.Right(5)[FL.ap](LazyEither.Right(R.add(3))).value(res => { 245 | LazyEither.Right(R.add(3))[FL.ap](LazyEither.Right(f => f(5))).value(res2 => { 246 | expect(res).to.deep.equal(S.Right(8)) 247 | expect(res).to.deep.equal(res2) 248 | done() 249 | }) 250 | }) 251 | }) 252 | }) 253 | 254 | 255 | 256 | describe('Setoid (contitional)', function() { 257 | it('should return true if a and b are equal (reflexivity) (1)', function(done) { 258 | LazyEither.Right('good')[FL.equals](LazyEither.Right('good'), res => { 259 | expect(res).to.be.true 260 | done() 261 | }) 262 | }) 263 | 264 | it('should return true if a and b are equal (reflexivity) (2)', function(done) { 265 | LazyEither.Left('bad')[FL.equals](LazyEither.Left('bad'), res => { 266 | expect(res).to.be.true 267 | done() 268 | }) 269 | }) 270 | 271 | it('should return false if a and b are not equal (1)', function(done) { 272 | LazyEither.Right(1)[FL.equals](LazyEither.Right(2), res => { 273 | expect(res).to.be.false 274 | done() 275 | }) 276 | }) 277 | 278 | it('should return false if a and b are not equal (2)', function(done) { 279 | LazyEither.Left(1)[FL.equals](LazyEither.Left(2), res => { 280 | expect(res).to.be.false 281 | done() 282 | }) 283 | }) 284 | 285 | it('should return false if a and b are equal but Left and Right', function(done) { 286 | LazyEither.Left(1)[FL.equals](LazyEither.Right(1), res => { 287 | expect(res).to.be.false 288 | done() 289 | }) 290 | }) 291 | 292 | it('should satisfy symmetry', function(done) { 293 | LazyEither.Right(7 + 5)[FL.equals](LazyEither.Right(3 * 4), res => { 294 | LazyEither.Right(3 * 4)[FL.equals](LazyEither.Right(7 + 5), res2 => { 295 | expect(res).to.deep.equal(res2) 296 | done() 297 | }) 298 | }) 299 | }) 300 | 301 | it('should satisfy transitivity', function(done) { 302 | LazyEither.Right(7 + 5)[FL.equals](LazyEither.Right(3 * 4), res => { 303 | LazyEither.Right(3 * 4)[FL.equals](LazyEither.Right(72 / 6), res2 => { 304 | LazyEither.Right(7 + 5)[FL.equals](LazyEither.Right(72 / 6), res2 => { 305 | expect(res).to.deep.equal(res2) 306 | done() 307 | }) 308 | }) 309 | }) 310 | }) 311 | }) 312 | 313 | 314 | 315 | describe('Lift', function() { 316 | it('should lift a function of arity 1', function(done) { 317 | let lifted = LazyEither.lift(R.multiply(3)) 318 | 319 | LazyEither.Right(3)[FL.chain](lifted).value(res => { 320 | expect(res).to.deep.equal(S.Right(9)) 321 | done() 322 | }) 323 | }) 324 | 325 | it('should lift a function of arity n', function(done) { 326 | let add = R.curry((a, b, c, d, e) => a + b + c + d + e) 327 | , lifted = LazyEither.liftN(5, add) 328 | 329 | LazyEither.Right(3)[FL.chain](lifted(4, 5, 6, 7)).value(res => { 330 | expect(res).to.deep.equal(S.Right(25)) 331 | done() 332 | }) 333 | }) 334 | }) 335 | 336 | 337 | 338 | describe('Promote', function() { 339 | it('should promote an Either type to a LazyEither type (Left)', function(done) { 340 | LazyEither.promote(S.Left('bad')).value(either => { 341 | expect(either.isLeft).to.be.true 342 | expect(either.value).to.equal('bad') 343 | done() 344 | }) 345 | }) 346 | 347 | it('should promote an Either type to a LazyEither type (Right)', function(done) { 348 | LazyEither.promote(S.Right('good')).value(either => { 349 | expect(either.isRight).to.be.true 350 | expect(either.value).to.equal('good') 351 | done() 352 | }) 353 | }) 354 | }) 355 | --------------------------------------------------------------------------------