diff --git a/index.js b/index.js index c45a6b04..bb970fd5 100644 --- a/index.js +++ b/index.js @@ -126,21 +126,17 @@ //. //. There is a performance cost to run-time type checking. One may wish to //. disable type checking in certain contexts to avoid paying this cost. -//. There are actually two versions of the Sanctuary module: one with type -//. checking; one without. The latter is accessible via the `unchecked` -//. property of the former. +//. [`create`](#create) facilitates the creation of a Sanctuary module which +//. does not perform type checking. //. -//. When application of `S.unchecked.` honours the function's type -//. signature the result will be the same as if `S.` had been used -//. instead. Otherwise, the behaviour is unspecified. -//. -//. In Node, one could use an environment variable to determine which version -//. of the Sanctuary module to use: +//. In Node, one could use an environment variable to determine whether to +//. perform type checking: //. //. ```javascript -//. const S = process.env.NODE_ENV === 'production' ? -//. require('sanctuary').unchecked : -//. require('sanctuary'); +//. const sanctuary = require('sanctuary'); +//. +//. const checkTypes = process.env.NODE_ENV !== 'production'; +//. const S = sanctuary.create({checkTypes: checkTypes, env: sanctuary.env}); //. ``` //. //. ## API @@ -281,7 +277,7 @@ // $Either :: Type -> Type -> Type var $Either = $.BinaryType( 'sanctuary/Either', - function(x) { return x != null && x['@@type'] === 'sanctuary/Either'; }, + function(x) { return _type(x) === 'sanctuary/Either'; }, function(either) { return either.isLeft ? [either.value] : []; }, function(either) { return either.isRight ? [either.value] : []; } ); @@ -303,7 +299,7 @@ // $Maybe :: Type -> Type var $Maybe = $.UnaryType( 'sanctuary/Maybe', - function(x) { return x != null && x['@@type'] === 'sanctuary/Maybe'; }, + function(x) { return _type(x) === 'sanctuary/Maybe'; }, function(maybe) { return maybe.isJust ? [maybe.value] : []; } ); @@ -318,8 +314,8 @@ } ); - // env :: Array Type - var env = $.env.concat([ + // defaultEnv :: Array Type + var defaultEnv = $.env.concat([ $.FiniteNumber, $.NonZeroFiniteNumber, $Either, @@ -331,14 +327,42 @@ $.ValidNumber ]); - // createSanctuary :: Boolean -> Module - var createSanctuary = function(checkTypes) { + // Options :: Type + var Options = $.RecordType({checkTypes: $.Boolean, env: $.Array($.Any)}); + + // createSanctuary :: Options -> Module + var createSanctuary = function createSanctuary(opts) { /* eslint-disable indent */ var S = {EitherType: $Either, MaybeType: $Maybe}; - var def = $.create(checkTypes, env); + //# create :: { checkTypes :: Boolean, env :: Array Type } -> Module + //. + //. Takes an options record and returns a Sanctuary module. `checkTypes` + //. specifies whether to enable type checking. The module's polymorphic + //. functions (such as [`I`](#I)) require each value associated with a + //. type variable to be a member of at least one type in the environment. + //. + //. A well-typed application of a Sanctuary function will produce the same + //. result regardless of whether type checking is enabled. If type checking + //. is enabled, a badly typed application will produce an exception with a + //. descriptive error message. + //. + //. See also [`env`](#env). + S.create = + $.create(opts.checkTypes, [])('create', + {}, + [Options, $.Object], + createSanctuary); + + //# env :: Array Type + //. + //. The default environment, which may be used as is or as the basis of a + //. custom environment in conjunction with [`create`](#create). + S.env = defaultEnv; + + var def = $.create(opts.checkTypes, opts.env); // Note: Type checking of method arguments takes place once all arguments // have been provided (whereas function arguments are checked as early as @@ -3309,11 +3333,7 @@ }; - // Export two versions of the Sanctuary module: one with type checking; - // one without. - var S = createSanctuary(true); - S.unchecked = createSanctuary(false); - return S; + return createSanctuary({checkTypes: true, env: defaultEnv}); })); diff --git a/test/create.js b/test/create.js new file mode 100644 index 00000000..9c4055f1 --- /dev/null +++ b/test/create.js @@ -0,0 +1,84 @@ +'use strict'; + +var throws = require('assert').throws; + +var R = require('ramda'); +var $ = require('sanctuary-def'); + +var eq = require('./utils').eq; +var errorEq = require('./utils').errorEq; +var S = require('..'); + + +// customEnv :: Array Type +var customEnv = S.env.concat([$.EnumType(['foo', true, 42])]); + +var checkedDefaultEnv = S.create({checkTypes: true, env: S.env}); +var checkedCustomEnv = S.create({checkTypes: true, env: customEnv}); +var uncheckedDefaultEnv = S.create({checkTypes: false, env: S.env}); +var uncheckedCustomEnv = S.create({checkTypes: false, env: customEnv}); + + +describe('create', function() { + + it('is a unary function', function() { + eq(typeof S.create, 'function'); + eq(S.create.length, 1); + }); + + it('type checks its arguments', function() { + throws(function() { S.create({}); }, + errorEq(TypeError, + 'Invalid value\n' + + '\n' + + 'create :: { checkTypes :: Boolean, env :: (Array Any) } -> Object\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + ' 1\n' + + '\n' + + '1) {} :: Object\n' + + '\n' + + 'The value at position 1 is not a member of ‘{ checkTypes :: Boolean, env :: (Array Any) }’.\n')); + + throws(function() { S.create({checkTypes: 'true', env: []}); }, + errorEq(TypeError, + 'Invalid value\n' + + '\n' + + 'create :: { checkTypes :: Boolean, env :: (Array Any) } -> Object\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n' + + ' 1\n' + + '\n' + + '1) {"checkTypes": "true", "env": []} :: Object\n' + + '\n' + + 'The value at position 1 is not a member of ‘{ checkTypes :: Boolean, env :: (Array Any) }’.\n')); + }); + + it('returns a Sanctuary module', function() { + var expected = R.keys(S).sort(); + eq(R.keys(checkedDefaultEnv).sort(), expected); + eq(R.keys(checkedCustomEnv).sort(), expected); + eq(R.keys(uncheckedDefaultEnv).sort(), expected); + eq(R.keys(uncheckedCustomEnv).sort(), expected); + }); + + it('can create a module which does not perform type checking', function() { + eq(uncheckedDefaultEnv.inc(42), S.inc(42)); + eq(uncheckedDefaultEnv.inc('XXX'), 'XXX1'); + }); + + it('can create a module with a custom environment', function() { + throws(function() { S.I(['foo', 'foo', 42]); }, + errorEq(TypeError, + 'Type-variable constraint violation\n' + + '\n' + + 'I :: a -> a\n' + + ' ^\n' + + ' 1\n' + + '\n' + + '1) ["foo", "foo", 42] :: Array ???\n' + + '\n' + + 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); + + eq(checkedCustomEnv.I(['foo', 'foo', 42]), ['foo', 'foo', 42]); + }); + +}); diff --git a/test/unchecked.js b/test/unchecked.js deleted file mode 100644 index 83909d70..00000000 --- a/test/unchecked.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -var R = require('ramda'); - -var eq = require('./utils').eq; -var S = require('..'); - -describe('unchecked', function() { - - it('has the same properties as the top-level module', function() { - eq(R.sortBy(S.I, R.keys(S.unchecked)), - R.sortBy(S.I, R.without(['unchecked'], R.keys(S)))); - }); - - it('provides functions which do not perform type checking', function() { - eq(S.unchecked.inc(42), S.inc(42)); - eq(S.unchecked.inc('XXX'), 'XXX1'); - }); - -});