diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..aeff0ee --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,36 @@ +version: 2 + +jobs: + build: + docker: + - image: circleci/node:6 + environment: + NPM_CONFIG_COLOR: false + NPM_CONFIG_LOGLEVEL: warn + NPM_CONFIG_PROGRESS: false + NVM_DIR: /home/circleci/.nvm + parallelism: 3 + steps: + - checkout + - restore_cache: + keys: + - nvm-cache-{{ checksum ".circleci/config.yml" }} + - run: curl https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash + - save_cache: + key: nvm-cache-{{ checksum ".circleci/config.yml" }} + paths: + - /home/circleci/.nvm + - run: + name: npm install && npm test + command: | + test_with_version() { + source "$NVM_DIR/nvm.sh" + nvm install $1 + nvm exec $1 npm install + nvm exec $1 npm test + } + case $CIRCLE_NODE_INDEX in + 0) npm install && npm test ;; + 1) test_with_version 8 ;; + 2) test_with_version 10 ;; + esac diff --git a/.config b/.config new file mode 100644 index 0000000..366dbb9 --- /dev/null +++ b/.config @@ -0,0 +1,2 @@ +repo-owner = sanctuary-js +repo-name = sanctuary-either diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..fa09497 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "root": true, + "extends": ["./node_modules/sanctuary-style/eslint-es3.json"], + "overrides": [ + { + "files": ["*.md"], + "globals": {"$": false, "Either": false, "Left": false, "Right": false, "S": false, "Z": false, "show": false, "type": false} + }, + { + "files": ["index.js"], + "rules": { + "no-unused-vars": ["error", {"vars": "all", "varsIgnorePattern": "^S$", "args": "none"}] + } + } + ] +} diff --git a/.gitignore b/.gitignore index e69de29..cba87a3 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,2 @@ +/coverage/ +/node_modules/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dd7edee --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing + +Note: __README.md__ is generated from comments in __index.js__. Do not modify +__README.md__ directly. + +1. Update local master branch: + + $ git checkout master + $ git pull upstream master + +2. Create feature branch: + + $ git checkout -b feature-x + +3. Make one or more atomic commits, and ensure that each commit has a + descriptive commit message. Commit messages should be line wrapped + at 72 characters. + +4. Run `npm test`, and address any errors. Preferably, fix commits in place + using `git rebase` or `git commit --amend` to make the changes easier to + review. + +5. Push: + + $ git push origin feature-x + +6. Open a pull request. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ffd105b --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +The MIT License (MIT) + +Copyright (c) 2018 Sanctuary +Copyright (c) 2016 Plaid Technologies, Inc. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/index.js b/index.js new file mode 100644 index 0000000..0918207 --- /dev/null +++ b/index.js @@ -0,0 +1,527 @@ +/* + _______ ___ _________ ___ ___ _______ ______ + / ____/\ / /\ /__ ___/\ / /\ / /\ / ____/\ / __ \ + / /\___\/ / / / \_/ /\__\/ / /_// / / / /\___\/ / /\/ /\ + / ____/\ / / / / / / / ___ / / / ____/\ / _/ / + / /\___\/ / / / / / / / /\_/ / / / /\___\/ / /| |\\/ + /______/\ /__/ / /__/ / /__/ //__/ / /______/\ /__/ |__| | + \______\/ \__\/ \__\/ \__\/ \__\/ \______\/ \__\/ \__\| + */ + +//. Fantasy Land +//. +//. # sanctuary-either +//. +//. The Either type represents values with two possibilities: a value of type +//. `Either a b` is either a Left whose value is of type `a` or a Right whose +//. value is of type `b`. + +(function(f) { + + 'use strict'; + + /* istanbul ignore else */ + if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = f (require ('sanctuary-show'), + require ('sanctuary-type-classes')); + } else if (typeof define === 'function' && define.amd != null) { + define (['sanctuary-show', 'sanctuary-type-classes'], f); + } else { + self.sanctuaryEither = f (self.sanctuaryShow, self.sanctuaryTypeClasses); + } + +} (function(show, Z) { + + 'use strict'; + + /* istanbul ignore if */ + if (typeof __doctest !== 'undefined') { + var $ = __doctest.require ('sanctuary-def'); + var type = __doctest.require ('sanctuary-type-identifiers'); + var S = (function() { + var S = __doctest.require ('sanctuary'); + var EitherType = $.BinaryType + ('sanctuary-either/Either') + ('') + (function(x) { return type (x) === Either['@@type']; }) + (function(e) { return e.isLeft ? [e.value] : []; }) + (function(e) { return e.isLeft ? [] : [e.value]; }); + var env = Z.concat (S.env, + [$.TypeClass, EitherType ($.Unknown) ($.Unknown)]); + return S.create ({checkTypes: true, env: env}); + } ()); + } + + var Either = {}; + + var Left$prototype = { + /* eslint-disable key-spacing */ + 'constructor': Either, + 'isLeft': true, + 'isRight': false, + '@@show': Left$prototype$show, + 'fantasy-land/map': Left$prototype$map, + 'fantasy-land/bimap': Left$prototype$bimap, + 'fantasy-land/ap': Left$prototype$ap, + 'fantasy-land/chain': Left$prototype$chain, + 'fantasy-land/alt': Left$prototype$alt, + 'fantasy-land/reduce': Left$prototype$reduce, + 'fantasy-land/traverse': Left$prototype$traverse, + 'fantasy-land/extend': Left$prototype$extend + /* eslint-enable key-spacing */ + }; + + var Right$prototype = { + /* eslint-disable key-spacing */ + 'constructor': Either, + 'isLeft': false, + 'isRight': true, + '@@show': Right$prototype$show, + 'fantasy-land/map': Right$prototype$map, + 'fantasy-land/bimap': Right$prototype$bimap, + 'fantasy-land/ap': Right$prototype$ap, + 'fantasy-land/chain': Right$prototype$chain, + 'fantasy-land/alt': Right$prototype$alt, + 'fantasy-land/reduce': Right$prototype$reduce, + 'fantasy-land/traverse': Right$prototype$traverse, + 'fantasy-land/extend': Right$prototype$extend + /* eslint-enable key-spacing */ + }; + + var util = + typeof module === 'object' && typeof module.exports === 'object' ? + require ('util') : + /* istanbul ignore next */ {}; + var inspect = + util.inspect != null && typeof util.inspect.custom === 'symbol' ? + /* istanbul ignore next */ util.inspect.custom : + /* istanbul ignore next */ 'inspect'; + Left$prototype[inspect] = Left$prototype$show; + Right$prototype[inspect] = Right$prototype$show; + + //. `Either a b` satisfies the following [Fantasy Land][] specifications: + //. + //. ```javascript + //. > const Useless = require ('sanctuary-useless') + //. + //. > S.map (k => k + ' '.repeat (16 - k.length) + + //. . (Z[k].test (Right (Useless)) ? '\u2705 ' : + //. . Z[k].test (Right (['foo'])) ? '\u2705 * ' : + //. . /* otherwise */ '\u274C ')) + //. . (S.keys (Z.filter ($.test ([]) ($.TypeClass), Z))) + //. [ 'Setoid ✅ * ', // if ‘a’ and ‘b’ satisfy Setoid + //. . 'Ord ✅ * ', // if ‘a’ and ‘b’ satisfy Ord + //. . 'Semigroupoid ❌ ', + //. . 'Category ❌ ', + //. . 'Semigroup ✅ * ', // if ‘a’ and ‘b’ satisfy Semigroup + //. . 'Monoid ❌ ', + //. . 'Group ❌ ', + //. . 'Filterable ❌ ', + //. . 'Functor ✅ ', + //. . 'Bifunctor ✅ ', + //. . 'Profunctor ❌ ', + //. . 'Apply ✅ ', + //. . 'Applicative ✅ ', + //. . 'Chain ✅ ', + //. . 'ChainRec ✅ ', + //. . 'Monad ✅ ', + //. . 'Alt ✅ ', + //. . 'Plus ❌ ', + //. . 'Alternative ❌ ', + //. . 'Foldable ✅ ', + //. . 'Traversable ✅ ', + //. . 'Extend ✅ ', + //. . 'Comonad ❌ ', + //. . 'Contravariant ❌ ' ] + //. ``` + + //# Either :: TypeRep Either + //. + //. Either [type representative][]. + + //# Either.Left :: a -> Either a b + //. + //. Constructs a value of type `Either a b` from a value of type `a`. + //. + //. ```javascript + //. > Left ('sqrt undefined for -1') + //. Left ('sqrt undefined for -1') + //. ``` + var Left = Either.Left = function(value) { + var left = Object.create (Left$prototype); + if (Z.Setoid.test (value)) { + left['fantasy-land/equals'] = Left$prototype$equals; + if (Z.Ord.test (value)) { + left['fantasy-land/lte'] = Left$prototype$lte; + } + } + if (Z.Semigroup.test (value)) { + left['fantasy-land/concat'] = Left$prototype$concat; + } + left.value = value; + return left; + }; + + //# Either.Right :: b -> Either a b + //. + //. Constructs a value of type `Either a b` from a value of type `b`. + //. + //. ```javascript + //. > Right (42) + //. Right (42) + //. ``` + var Right = Either.Right = function Right(value) { + var right = Object.create (Right$prototype); + if (Z.Setoid.test (value)) { + right['fantasy-land/equals'] = Right$prototype$equals; + if (Z.Ord.test (value)) { + right['fantasy-land/lte'] = Right$prototype$lte; + } + } + if (Z.Semigroup.test (value)) { + right['fantasy-land/concat'] = Right$prototype$concat; + } + right.value = value; + return right; + }; + + //# Either.@@type :: String + //. + //. Either [type identifier][]. + //. + //. ```javascript + //. > type (Right (42)) + //. 'sanctuary-either/Either@1' + //. + //. > type.parse (type (Right (42))) + //. {namespace: 'sanctuary-either', name: 'Either', version: 1} + //. ``` + Either['@@type'] = 'sanctuary-either/Either@1'; + + //# Either.fantasy-land/of :: b -> Either a b + //. + //. - `of (Either) (x)` is equivalent to `Right (x)` + //. + //. ```javascript + //. > S.of (Either) (42) + //. Right (42) + //. ``` + Either['fantasy-land/of'] = Right; + + function next(x) { return {tag: next, value: x}; } + function done(x) { return {tag: done, value: x}; } + + //# Either.fantasy-land/chainRec :: ((a -> c, b -> c, a) -> Either d c, a) -> Either d b + //. + //. ```javascript + //. > Z.chainRec ( + //. . Either, + //. . (next, done, x) => + //. . x <= 1 ? Left ('!!') : Right (x >= 1000 ? done (x) : next (x * x)), + //. . 1 + //. . ) + //. Left ('!!') + //. + //. > Z.chainRec ( + //. . Either, + //. . (next, done, x) => + //. . x <= 1 ? Left ('!!') : Right (x >= 1000 ? done (x) : next (x * x)), + //. . 2 + //. . ) + //. Right (65536) + //. ``` + Either['fantasy-land/chainRec'] = function(f, x) { + var r = next (x); + while (r.tag === next) { + var either = f (next, done, r.value); + if (either.isLeft) return either; + r = either.value; + } + return Right (r.value); + }; + + //# Either#@@show :: (Showable a, Showable b) => Either a b ~> () -> String + //. + //. - `show (Left (x))` is equivalent to `'Left (' + show (x) + ')'` + //. - `show (Right (x))` is equivalent to `'Right (' + show (x) + ')'` + //. + //. ```javascript + //. > show (Left ('sqrt undefined for -1')) + //. 'Left ("sqrt undefined for -1")' + //. + //. > show (Right ([1, 2, 3])) + //. 'Right ([1, 2, 3])' + //. ``` + function Left$prototype$show() { + return 'Left (' + show (this.value) + ')'; + } + function Right$prototype$show() { + return 'Right (' + show (this.value) + ')'; + } + + //# Either#fantasy-land/equals :: (Setoid a, Setoid b) => Either a b ~> Either a b -> Boolean + //. + //. - `Left (x)` is equal to `Left (y)` [iff][] `x` is equal to `y` + //. according to [`Z.equals`][] + //. - `Right (x)` is equal to `Right (y)` [iff][] `x` is equal to `y` + //. according to [`Z.equals`][] + //. - `Left (x)` is never equal to `Right (y)` + //. + //. ```javascript + //. > S.equals (Left ([1, 2, 3])) (Left ([1, 2, 3])) + //. true + //. + //. > S.equals (Right ([1, 2, 3])) (Right ([1, 2, 3])) + //. true + //. + //. > S.equals (Left ([1, 2, 3])) (Right ([1, 2, 3])) + //. false + //. ``` + function Left$prototype$equals(other) { + return other.isLeft && Z.equals (this.value, other.value); + } + function Right$prototype$equals(other) { + return other.isRight && Z.equals (this.value, other.value); + } + + //# Either#fantasy-land/lte :: (Ord a, Ord b) => Either a b ~> Either a b -> Boolean + //. + //. - `Left (x)` is less than or equal to `Left (y)` [iff][] `x` is less + //. than or equal to `y` according to [`Z.lte`][] + //. - `Right (x)` is less than or equal to `Right (y)` [iff][] `x` is less + //. than or equal to `y` according to [`Z.lte`][] + //. - `Left (x)` is always less than `Right (y)` + //. + //. ```javascript + //. > S.filter (S.lte (Left (1))) ([Left (0), Left (1), Left (2)]) + //. [Left (0), Left (1)] + //. + //. > S.filter (S.lte (Right (1))) ([Right (0), Right (1), Right (2)]) + //. [Right (0), Right (1)] + //. + //. > S.filter (S.lte (Left (1))) ([Right (0), Right (1), Right (2)]) + //. [] + //. + //. > S.filter (S.lte (Right (1))) ([Left (0), Left (1), Left (2)]) + //. [Left (0), Left (1), Left (2)] + //. ``` + function Left$prototype$lte(other) { + return other.isRight || Z.lte (this.value, other.value); + } + function Right$prototype$lte(other) { + return other.isRight && Z.lte (this.value, other.value); + } + + //# Either#fantasy-land/concat :: (Semigroup a, Semigroup b) => Either a b ~> Either a b -> Either a b + //. + //. - `concat (Left (x)) (Left (y))` is equivalent to + //. `Left (concat (x) (y))` + //. - `concat (Right (x)) (Right (y))` is equivalent to + //. `Right (concat (x) (y))` + //. - `concat (Left (x)) (Right (y))` is equivalent to `Right (y)` + //. - `concat (Right (x)) (Left (y))` is equivalent to `Right (x)` + //. + //. ```javascript + //. > S.concat (Left ('abc')) (Left ('def')) + //. Left ('abcdef') + //. + //. > S.concat (Right ([1, 2, 3])) (Right ([4, 5, 6])) + //. Right ([1, 2, 3, 4, 5, 6]) + //. + //. > S.concat (Left ('abc')) (Right ([1, 2, 3])) + //. Right ([1, 2, 3]) + //. + //. > S.concat (Right ([1, 2, 3])) (Left ('abc')) + //. Right ([1, 2, 3]) + //. ``` + function Left$prototype$concat(other) { + return other.isLeft ? Left (Z.concat (this.value, other.value)) : other; + } + function Right$prototype$concat(other) { + return other.isRight ? Right (Z.concat (this.value, other.value)) : this; + } + + //# Either#fantasy-land/map :: Either a b ~> (b -> c) -> Either a c + //. + //. - `map (f) (Left (x))` is equivalent to `Left (x)` + //. - `map (f) (Right (x))` is equivalent to `Right (f (x))` + //. + //. ```javascript + //. > S.map (S.add (1)) (Left ('sqrt undefined for -1')) + //. Left ('sqrt undefined for -1') + //. + //. > S.map (S.add (1)) (Right (99)) + //. Right (100) + //. ``` + function Left$prototype$map(f) { + return this; + } + function Right$prototype$map(f) { + return Right (f (this.value)); + } + + //# Either#fantasy-land/bimap :: Either a c ~> (a -> b, c -> d) -> Either b d + //. + //. - `bimap (f) (g) (Left (x))` is equivalent to `Left (f (x))` + //. - `bimap (f) (g) (Right (x))` is equivalent to `Right (g (x))` + //. + //. ```javascript + //. > S.bimap (S.toUpper) (S.add (1)) (Left ('abc')) + //. Left ('ABC') + //. + //. > S.bimap (S.toUpper) (S.add (1)) (Right (99)) + //. Right (100) + //. ``` + function Left$prototype$bimap(f, g) { + return Left (f (this.value)); + } + function Right$prototype$bimap(f, g) { + return Right (g (this.value)); + } + + //# Either#fantasy-land/ap :: Either a b ~> Either a (b -> c) -> Either a c + //. + //. - `ap (Left (x)) (Left (y))` is equivalent to `Left (x)` + //. - `ap (Left (x)) (Right (y))` is equivalent to `Left (x)` + //. - `ap (Right (f)) (Left (x))` is equivalent to `Left (x)` + //. - `ap (Right (f)) (Right (x))` is equivalent to `Right (f (x))` + //. + //. ```javascript + //. > S.ap (Left ('div undefined for 0')) (Left ('sqrt undefined for -1')) + //. Left ('div undefined for 0') + //. + //. > S.ap (Left ('div undefined for 0')) (Right (99)) + //. Left ('div undefined for 0') + //. + //. > S.ap (Right (S.add (1))) (Left ('sqrt undefined for -1')) + //. Left ('sqrt undefined for -1') + //. + //. > S.ap (Right (S.add (1))) (Right (99)) + //. Right (100) + //. ``` + function Left$prototype$ap(other) { + return other.isLeft ? other : this; + } + function Right$prototype$ap(other) { + return other.isLeft ? other : Right (other.value (this.value)); + } + + //# Either#fantasy-land/chain :: Either a b ~> (b -> Either a c) -> Either a c + //. + //. - `chain (f) (Left (x))` is equivalent to `Left (x)` + //. - `chain (f) (Right (x))` is equivalent to `f (x)` + //. + //. ```javascript + //. > const sqrt = n => n < 0 ? Left ('sqrt undefined for ' + show (n)) + //. . : Right (Math.sqrt (n)) + //. + //. > S.chain (sqrt) (Left ('div undefined for 0')) + //. Left ('div undefined for 0') + //. + //. > S.chain (sqrt) (Right (-1)) + //. Left ('sqrt undefined for -1') + //. + //. > S.chain (sqrt) (Right (25)) + //. Right (5) + //. ``` + function Left$prototype$chain(f) { + return this; + } + function Right$prototype$chain(f) { + return f (this.value); + } + + //# Either#fantasy-land/alt :: Either a b ~> Either a b -> Either a b + //. + //. - `alt (Left (x)) (Left (y))` is equivalent to `Left (y)` + //. - `alt (Left (x)) (Right (y))` is equivalent to `Right (y)` + //. - `alt (Right (x)) (Left (y))` is equivalent to `Right (x)` + //. - `alt (Right (x)) (Right (y))` is equivalent to `Right (x)` + //. + //. ```javascript + //. > S.alt (Left ('A')) (Left ('B')) + //. Left ('B') + //. + //. > S.alt (Left ('C')) (Right (1)) + //. Right (1) + //. + //. > S.alt (Right (2)) (Left ('D')) + //. Right (2) + //. + //. > S.alt (Right (3)) (Right (4)) + //. Right (3) + //. ``` + function Left$prototype$alt(other) { + return other; + } + function Right$prototype$alt(other) { + return this; + } + + //# Either#fantasy-land/reduce :: Either a b ~> ((c, b) -> c, c) -> c + //. + //. - `reduce (f) (x) (Left (y))` is equivalent to `x` + //. - `reduce (f) (x) (Right (y))` is equivalent to `f (x) (y)` + //. + //. ```javascript + //. > S.reduce (S.concat) ([1]) (Left ('sqrt undefined for -1')) + //. [1] + //. + //. > S.reduce (S.concat) ([1]) (Right ([2])) + //. [1, 2] + //. ``` + function Left$prototype$reduce(f, x) { + return x; + } + function Right$prototype$reduce(f, x) { + return f (x, this.value); + } + + //# Either#fantasy-land/traverse :: Applicative f => Either a b ~> (TypeRep f, b -> f c) -> f (Either a c) + //. + //. - `traverse (A) (f) (Left (x))` is equivalent to `of (A) (Left (x))` + //. - `traverse (A) (f) (Right (x))` is equivalent to `map (Right) (f (x))` + //. + //. ```javascript + //. > S.traverse (Array) (S.words) (Left ('sqrt undefined for -1')) + //. [Left ('sqrt undefined for -1')] + //. + //. > S.traverse (Array) (S.words) (Right ('foo bar baz')) + //. [Right ('foo'), Right ('bar'), Right ('baz')] + //. ``` + function Left$prototype$traverse(typeRep, f) { + return Z.of (typeRep, this); + } + function Right$prototype$traverse(typeRep, f) { + return Z.map (Right, f (this.value)); + } + + //# Either#fantasy-land/extend :: Either a b ~> (Either a b -> c) -> Either a c + //. + //. - `extend (f) (Left (x))` is equivalent to `Left (x)` + //. - `extend (f) (Right (x))` is equivalent to `Right (f (Right (x)))` + //. + //. ```javascript + //. > S.extend (S.reduce (S.add) (1)) (Left ('sqrt undefined for -1')) + //. Left ('sqrt undefined for -1') + //. + //. > S.extend (S.reduce (S.add) (1)) (Right (99)) + //. Right (100) + //. ``` + function Left$prototype$extend(f) { + return this; + } + function Right$prototype$extend(f) { + return Right (f (this)); + } + + return Either; + +})); + +//. [Fantasy Land]: v:fantasyland/fantasy-land +//. [`Z.equals`]: v:sanctuary-js/sanctuary-type-classes#equals +//. [`Z.lte`]: v:sanctuary-js/sanctuary-type-classes#lte +//. [iff]: https://en.wikipedia.org/wiki/If_and_only_if +//. [type identifier]: v:sanctuary-js/sanctuary-type-identifiers +//. [type representative]: v:fantasyland/fantasy-land#type-representatives diff --git a/package.json b/package.json new file mode 100644 index 0000000..922cd2a --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "sanctuary-either", + "version": "0.0.0", + "description": "Fantasy Land -compliant Either type", + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/sanctuary-js/sanctuary-either.git" + }, + "files": [ + "/LICENSE", + "/README.md", + "/index.js", + "/package.json" + ], + "dependencies": { + "sanctuary-show": "1.0.x", + "sanctuary-type-classes": "9.0.0" + }, + "devDependencies": { + "fantasy-land": "3.5.0", + "fantasy-laws": "1.0.x", + "jsverify": "0.8.x", + "sanctuary": "0.14.1", + "sanctuary-def": "0.17.x", + "sanctuary-identity": "1.0.x", + "sanctuary-scripts": "2.0.x", + "sanctuary-type-identifiers": "2.0.1", + "sanctuary-useless": "1.0.x" + }, + "scripts": { + "doctest": "sanctuary-doctest", + "lint": "sanctuary-lint", + "release": "sanctuary-release", + "test": "npm run lint && sanctuary-test && npm run doctest" + } +} diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 0000000..249d948 --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "root": true, + "extends": ["../node_modules/sanctuary-style/eslint-es6.json"], + "env": {"node": true}, + "globals": {"suite": false, "test": false} +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..57e4f56 --- /dev/null +++ b/test/index.js @@ -0,0 +1,411 @@ +'use strict'; + +const assert = require ('assert'); + +const laws = require ('fantasy-laws'); +const jsc = require ('jsverify'); +const Identity = require ('sanctuary-identity'); +const show = require ('sanctuary-show'); +const Z = require ('sanctuary-type-classes'); +const type = require ('sanctuary-type-identifiers'); +const Useless = require ('sanctuary-useless'); + +const Either = require ('..'); + + +const {Left, Right} = Either; + + +// EitherArb :: Arbitrary a -> Arbitrary b -> Arbitrary (Either a b) +const EitherArb = arbL => arbR => + jsc.oneof (arbL.smap (Left, left => left.value, show), + arbR.smap (Right, right => right.value, show)); + +// IdentityArb :: Arbitrary a -> Arbitrary (Identity a) +const IdentityArb = arb => arb.smap (Identity, Z.extract, show); + +// head :: Array a -> Either String a +const head = xs => + xs.length === 0 ? Left ('head undefined for ' + show (xs)) : Right (xs[0]); + +// sqrt :: Number -> Either String Number +const sqrt = n => + n < 0 ? Left ('sqrt undefined for ' + show (n)) : Right (Math.sqrt (n)); + +// testLaws :: Object -> Object -> Undefined +const testLaws = laws => arbs => { + (Object.keys (laws)).forEach (name => { + test (name.replace (/[A-Z]/g, c => ' ' + c.toLowerCase ()), + laws[name] (...arbs[name])); + }); +}; + +// eq :: a -> b -> Undefined ! +function eq(actual) { + assert.strictEqual (arguments.length, eq.length); + return function eq$1(expected) { + assert.strictEqual (arguments.length, eq$1.length); + assert.strictEqual (show (actual), show (expected)); + assert.strictEqual (Z.equals (actual, expected), true); + }; +} + + +suite ('Either', () => { + + test ('metadata', () => { + eq (typeof Left) ('function'); + eq (typeof Right) ('function'); + eq (Left.length) (1); + eq (Right.length) (1); + }); + + test ('tags', () => { + const left = Left (0); + const right = Right (0); + eq (left.isLeft) (true); + eq (left.isRight) (false); + eq (right.isLeft) (false); + eq (right.isRight) (true); + }); + + test ('@@type', () => { + eq (type (Left (0))) ('sanctuary-either/Either@1'); + eq (type (Right (0))) ('sanctuary-either/Either@1'); + eq (type.parse (type (Right (0)))) + ({namespace: 'sanctuary-either', name: 'Either', version: 1}); + }); + + test ('@@show', () => { + eq (show (Left (['foo', 'bar', 'baz']))) + ('Left (["foo", "bar", "baz"])'); + eq (show (Right (['foo', 'bar', 'baz']))) + ('Right (["foo", "bar", "baz"])'); + eq (show (Left (Right (Left (Right (-0)))))) + ('Left (Right (Left (Right (-0))))'); + }); + +}); + +suite ('type-class predicates', () => { + + test ('Setoid', () => { + eq (Z.Setoid.test (Left (Useless))) (false); + eq (Z.Setoid.test (Left (/(?:)/))) (true); + eq (Z.Setoid.test (Right (Useless))) (false); + eq (Z.Setoid.test (Right (/(?:)/))) (true); + }); + + test ('Ord', () => { + eq (Z.Ord.test (Left (Useless))) (false); + eq (Z.Ord.test (Left (/(?:)/))) (false); + eq (Z.Ord.test (Left (0))) (true); + eq (Z.Ord.test (Right (Useless))) (false); + eq (Z.Ord.test (Right (/(?:)/))) (false); + eq (Z.Ord.test (Right (0))) (true); + }); + + test ('Semigroupoid', () => { + eq (Z.Semigroupoid.test (Left ([]))) (false); + eq (Z.Semigroupoid.test (Right ([]))) (false); + }); + + test ('Category', () => { + eq (Z.Category.test (Left ([]))) (false); + eq (Z.Category.test (Right ([]))) (false); + }); + + test ('Semigroup', () => { + eq (Z.Semigroup.test (Left (Useless))) (false); + eq (Z.Semigroup.test (Left (0))) (false); + eq (Z.Semigroup.test (Left ([]))) (true); + eq (Z.Semigroup.test (Right (Useless))) (false); + eq (Z.Semigroup.test (Right (0))) (false); + eq (Z.Semigroup.test (Right ([]))) (true); + }); + + test ('Monoid', () => { + eq (Z.Monoid.test (Left ([]))) (false); + eq (Z.Monoid.test (Right ([]))) (false); + }); + + test ('Group', () => { + eq (Z.Group.test (Left ([]))) (false); + eq (Z.Group.test (Right ([]))) (false); + }); + + test ('Filterable', () => { + eq (Z.Filterable.test (Left ([]))) (false); + eq (Z.Filterable.test (Right ([]))) (false); + }); + + test ('Functor', () => { + eq (Z.Functor.test (Left (Useless))) (true); + eq (Z.Functor.test (Right (Useless))) (true); + }); + + test ('Bifunctor', () => { + eq (Z.Bifunctor.test (Left (Useless))) (true); + eq (Z.Bifunctor.test (Right (Useless))) (true); + }); + + test ('Profunctor', () => { + eq (Z.Profunctor.test (Left (Math.sqrt))) (false); + eq (Z.Profunctor.test (Right (Math.sqrt))) (false); + }); + + test ('Apply', () => { + eq (Z.Apply.test (Left (Useless))) (true); + eq (Z.Apply.test (Right (Useless))) (true); + }); + + test ('Applicative', () => { + eq (Z.Applicative.test (Left (Useless))) (true); + eq (Z.Applicative.test (Right (Useless))) (true); + }); + + test ('Chain', () => { + eq (Z.Chain.test (Left (Useless))) (true); + eq (Z.Chain.test (Right (Useless))) (true); + }); + + test ('ChainRec', () => { + eq (Z.ChainRec.test (Left (Useless))) (true); + eq (Z.ChainRec.test (Right (Useless))) (true); + }); + + test ('Monad', () => { + eq (Z.Monad.test (Left (Useless))) (true); + eq (Z.Monad.test (Right (Useless))) (true); + }); + + test ('Alt', () => { + eq (Z.Alt.test (Left (Useless))) (true); + eq (Z.Alt.test (Right (Useless))) (true); + }); + + test ('Plus', () => { + eq (Z.Plus.test (Left ([]))) (false); + eq (Z.Plus.test (Right ([]))) (false); + }); + + test ('Alternative', () => { + eq (Z.Alternative.test (Left ([]))) (false); + eq (Z.Alternative.test (Right ([]))) (false); + }); + + test ('Foldable', () => { + eq (Z.Foldable.test (Left (Useless))) (true); + eq (Z.Foldable.test (Right (Useless))) (true); + }); + + test ('Traversable', () => { + eq (Z.Traversable.test (Left (Useless))) (true); + eq (Z.Traversable.test (Right (Useless))) (true); + }); + + test ('Extend', () => { + eq (Z.Extend.test (Left (Useless))) (true); + eq (Z.Extend.test (Right (Useless))) (true); + }); + + test ('Comonad', () => { + eq (Z.Comonad.test (Left (Identity (0)))) (false); + eq (Z.Comonad.test (Right (Identity (0)))) (false); + }); + + test ('Contravariant', () => { + eq (Z.Contravariant.test (Left (Math.sqrt))) (false); + eq (Z.Contravariant.test (Right (Math.sqrt))) (false); + }); + +}); + +suite ('Setoid laws', () => { + testLaws (laws.Setoid) ({ + reflexivity: [ + EitherArb (jsc.string) (jsc.falsy), + ], + symmetry: [ + EitherArb (jsc.bool) (jsc.bool), + EitherArb (jsc.bool) (jsc.bool), + ], + transitivity: [ + EitherArb (jsc.bool) (jsc.bool), + EitherArb (jsc.bool) (jsc.bool), + EitherArb (jsc.bool) (jsc.bool), + ], + }); +}); + +suite ('Ord laws', () => { + testLaws (laws.Ord) ({ + totality: [ + EitherArb (jsc.string) (jsc.number), + EitherArb (jsc.string) (jsc.number), + ], + antisymmetry: [ + EitherArb (jsc.string) (jsc.number), + EitherArb (jsc.string) (jsc.number), + EitherArb (jsc.string) (jsc.number), + ], + transitivity: [ + EitherArb (jsc.string) (jsc.number), + EitherArb (jsc.string) (jsc.number), + EitherArb (jsc.string) (jsc.number), + ], + }); +}); + +suite ('Semigroup laws', () => { + testLaws (laws.Semigroup (Z.equals)) ({ + associativity: [ + EitherArb (jsc.string) (jsc.string), + EitherArb (jsc.string) (jsc.string), + EitherArb (jsc.string) (jsc.string), + ], + }); +}); + +suite ('Functor laws', () => { + testLaws (laws.Functor (Z.equals)) ({ + identity: [ + EitherArb (jsc.string) (jsc.number), + ], + composition: [ + EitherArb (jsc.string) (jsc.number), + jsc.constant (Math.sqrt), + jsc.constant (Math.abs), + ], + }); +}); + +suite ('Bifunctor laws', () => { + testLaws (laws.Bifunctor (Z.equals)) ({ + identity: [ + EitherArb (jsc.string) (jsc.number), + ], + composition: [ + EitherArb (jsc.string) (jsc.number), + jsc.constant (Math.sqrt), + jsc.constant (s => s.length), + jsc.constant (Math.sqrt), + jsc.constant (Math.abs), + ], + }); +}); + +suite ('Apply laws', () => { + testLaws (laws.Apply (Z.equals)) ({ + composition: [ + EitherArb (jsc.string) (jsc.constant (Math.sqrt)), + EitherArb (jsc.string) (jsc.constant (Math.abs)), + EitherArb (jsc.string) (jsc.number), + ], + }); +}); + +suite ('Applicative laws', () => { + testLaws (laws.Applicative (Z.equals, Either)) ({ + identity: [ + EitherArb (jsc.string) (jsc.number), + ], + homomorphism: [ + jsc.constant (Math.abs), + jsc.number, + ], + interchange: [ + EitherArb (jsc.string) (jsc.constant (Math.abs)), + jsc.number, + ], + }); +}); + +suite ('Chain laws', () => { + testLaws (laws.Chain (Z.equals)) ({ + associativity: [ + EitherArb (jsc.string) (jsc.array (jsc.number)), + jsc.constant (head), + jsc.constant (sqrt), + ], + }); +}); + +suite ('ChainRec laws', () => { + testLaws (laws.ChainRec (Z.equals, Either)) ({ + equivalence: [ + jsc.constant (x => x >= 1000), + jsc.constant (x => x <= 1 ? Left (show (x) + ' <= 1') : Right (x * x)), + jsc.constant (Right), + jsc.integer, + ], + }); +}); + +suite ('Monad laws', () => { + testLaws (laws.Monad (Z.equals, Either)) ({ + leftIdentity: [ + jsc.constant (sqrt), + jsc.number, + ], + rightIdentity: [ + EitherArb (jsc.string) (jsc.number), + ], + }); +}); + +suite ('Alt laws', () => { + testLaws (laws.Alt (Z.equals)) ({ + associativity: [ + EitherArb (jsc.string) (jsc.number), + EitherArb (jsc.string) (jsc.number), + EitherArb (jsc.string) (jsc.number), + ], + distributivity: [ + EitherArb (jsc.string) (jsc.number), + EitherArb (jsc.string) (jsc.number), + jsc.constant (Math.sqrt), + ], + }); +}); + +suite ('Foldable laws', () => { + testLaws (laws.Foldable (Z.equals)) ({ + associativity: [ + jsc.constant ((x, y) => x + y), + jsc.number, + EitherArb (jsc.string) (jsc.number), + ], + }); +}); + +suite ('Traversable laws', () => { + testLaws (laws.Traversable (Z.equals)) ({ + naturality: [ + jsc.constant (Identity), + jsc.constant (Array), + jsc.constant (identity => [Z.extract (identity)]), + EitherArb (jsc.string) (IdentityArb (jsc.number)), + ], + identity: [ + jsc.constant (Identity), + EitherArb (jsc.string) (jsc.number), + ], + composition: [ + jsc.constant (Identity), + jsc.constant (Either), + EitherArb (jsc.string) + (IdentityArb (EitherArb (jsc.string) (jsc.number))), + ], + }); +}); + +suite ('Extend laws', () => { + testLaws (laws.Extend (Z.equals)) ({ + associativity: [ + EitherArb (jsc.string) (jsc.integer), + jsc.constant (either => Z.reduce ((x, y) => x + y, 1, either)), + jsc.constant (either => Z.reduce ((x, y) => y * y, 1, either)), + ], + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..5efaf24 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--ui tdd