From 52918bda1ef048b6a260b213236275c7d3b6823b Mon Sep 17 00:00:00 2001 From: David Chambers Date: Sat, 30 Apr 2016 20:37:49 -0700 Subject: [PATCH] add S.create for creating Sanctuary modules with custom environments --- index.js | 68 ++++++++++++++++++++++++-------------- test/create.js | 84 +++++++++++++++++++++++++++++++++++++++++++++++ test/unchecked.js | 20 ----------- 3 files changed, 128 insertions(+), 44 deletions(-) create mode 100644 test/create.js delete mode 100644 test/unchecked.js diff --git a/index.js b/index.js index 930caf8b..220e6a78 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 @@ -280,7 +276,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] : []; } ); @@ -302,7 +298,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] : []; } ); @@ -317,8 +313,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: checkTypes, env: 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({checkTypes: opts.checkTypes, env: defaultEnv})('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); // 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..b5727ee6 --- /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, StrMap ???\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) "true" :: String\n' + + '\n' + + 'The value at position 1 is not a member of ‘Boolean’.\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'); - }); - -});