From ce5499fd88ae0bc49741ff125366856ce79d5305 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 | 101 ++++++++++++++++++++++++++++++++++++---------- test/create.js | 83 +++++++++++++++++++++++++++++++++++++ test/unchecked.js | 20 --------- 3 files changed, 162 insertions(+), 42 deletions(-) create mode 100644 test/create.js delete mode 100644 test/unchecked.js diff --git a/index.js b/index.js index 06035070..dd3c0864 100644 --- a/index.js +++ b/index.js @@ -128,21 +128,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 @@ -319,8 +315,8 @@ } ); - // env :: Array Type - var env = $.env.concat([ + // defaultEnv :: Array Type + var defaultEnv = $.env.concat([ $.FiniteNumber, $.NonZeroFiniteNumber, $Either, @@ -333,14 +329,79 @@ $.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. + //. + //. The following snippet demonstrates defining a custom type and using + //. `create` to produce a Sanctuary module which is aware of that type: + //. + //. ```javascript + //. const {create, env} = require('sanctuary'); + //. const $ = require('sanctuary-def'); + //. + //. // identityTypeName :: String + //. const identityTypeName = 'my-package/Identity'; + //. + //. // Identity :: a -> Identity a + //. const Identity = function Identity(x) { + //. return { + //. '@@type': identityTypeName, + //. map: f => Identity(f(x)), + //. chain: f => f(x), + //. // ... + //. value: x, + //. }; + //. }; + //. + //. // isIdentity :: a -> Boolean + //. const isIdentity = x => x != null && x['@@type'] === identityTypeName; + //. + //. // identityToArray :: Identity a -> Array a + //. const identityToArray = identity => [identity.value]; + //. + //. // IdentityType :: Type + //. const IdentityType = + //. $.UnaryType(identityTypeName, isIdentity, identityToArray); + //. + //. const S = create({ + //. checkTypes: process.env.NODE_ENV !== 'production', + //. env: env.concat([IdentityType]), + //. }); + //. ``` + //. + //. 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 @@ -3446,11 +3507,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..a2b1a281 --- /dev/null +++ b/test/create.js @@ -0,0 +1,83 @@ +'use strict'; + +var throws = require('assert').throws; + +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 = Object.keys(S).sort(); + eq(Object.keys(checkedDefaultEnv).sort(), expected); + eq(Object.keys(checkedCustomEnv).sort(), expected); + eq(Object.keys(uncheckedDefaultEnv).sort(), expected); + eq(Object.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'); - }); - -});