diff --git a/index.js b/index.js index 64d6b0bc..761cd7b5 100644 --- a/index.js +++ b/index.js @@ -59,6 +59,30 @@ //. (for any type `a`) with an argument of type `a -> b` (for any type `b`), //. it returns a value of type `Maybe b`._ //. +//. ### Accessible pseudotype +//. +//. What is the type of values which support property access? In other words, +//. what is the type of which every value except `null` and `undefined` is a +//. member? Object is close, but `Object.create(null)` produces a value which +//. supports property access but which is not a member of the Object type. +//. +//. Sanctuary uses the Accessible pseudotype to represent the set of values +//. which support property access. +//. +//. ### Type representatives +//. +//. What is the type of `Number`? One answer is `a -> Number`, since it's a +//. function which takes an argument of any type and returns a Number value. +//. When provided as the first argument to [`is`](#is), though, `Number` is +//. really the value-level representative of the Number type. +//. +//. Sanctuary uses the TypeRep pseudotype to describe type representatives. +//. For example: +//. +//. Number :: TypeRep Number +//. +//. `Number` is the sole inhabitant of the TypeRep Number type. +//. //. ## API ;(function() { @@ -101,13 +125,13 @@ }); var Accessible = /* istanbul ignore next */ function Accessible() {}; - var Type = /* istanbul ignore next */ function Type() {}; + var TypeRep = /* istanbul ignore next */ function TypeRep() {}; var a = {name: 'a'}; var b = {name: 'b'}; var c = {name: 'c'}; var _is = function(type, x) { - return x != null && Object(x) instanceof type; + return x != null && (type === Accessible || Object(x) instanceof type); }; var arity = function(n, f) { @@ -119,7 +143,7 @@ } }; - // curry :: (String, [Type], [*], Function) -> Function + // curry :: (String, [TypeRep], [*], Function) -> Function var curry = function(name, types, _values, f) { return arity(R.filter(placeholder, _values).length, function() { var values = _values; // Locally scoped variable to update. @@ -161,7 +185,7 @@ [paramIndex, name] )); } - } else if (!_is(type === Type ? Function : type, arg)) { + } else if (!_is(type === TypeRep ? Function : type, arg)) { throw new TypeError(format( '{quote} requires a value of type {type} as its {ord} argument; ' + 'received {repr}', @@ -210,11 +234,11 @@ //. ### Classify - //# is :: Type -> a -> Boolean + //# is :: TypeRep a -> b -> Boolean //. - //. Takes a type and a value of any type and returns `true` if the given - //. value is of the specified type (either directly or via the prototype - //. chain); `false` otherwise. + //. Takes a [type representative](#type-representatives) and a value of + //. any type and returns `true` if the given value is of the specified + //. type (either directly or via the prototype chain); `false` otherwise. //. //. Boolean, number, string, and symbol [primitives][] are promoted to //. their object equivalents. `42`, for example, is considered a Number @@ -231,7 +255,7 @@ //. > S.is(String, 42) //. false //. ``` - var is = S.is = def('is', [Type, a], _is); + var is = S.is = def('is', [TypeRep, a], _is); //. ### Combinator @@ -253,7 +277,7 @@ //. ### Maybe type - //# Maybe :: Type + //# Maybe :: TypeRep Maybe //. //. The Maybe type represents optional values: a value of type `Maybe a` is //. either a Just whose value is of type `a` or a Nothing (with no value). @@ -480,7 +504,7 @@ //. "Just([1, 2, 3])" //. ``` - //# Maybe#type :: Type + //# Maybe#type :: TypeRep Maybe //. //. A reference to the Maybe type. Useful for determining whether two //. values such as `S.Nothing()` and `S.Just(42)` are of the same type. @@ -648,7 +672,7 @@ //. ### Either type - //# Either :: Type + //# Either :: TypeRep 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 @@ -822,7 +846,7 @@ //. "Right([1, 2, 3])" //. ``` - //# Either#type :: Type + //# Either#type :: TypeRep Either //. //. A reference to the Either type. Useful for determining whether two //. values such as `S.Left('Cannot divide by zero')` and `S.Right(42)` @@ -1308,59 +1332,77 @@ //. ``` S.lastIndexOf = sanctifyIndexOf('lastIndexOf'); - //# pluck :: String -> [{String: *}] -> [Maybe *] + //# pluck :: TypeRep a -> String -> [Accessible] -> [Maybe a] //. - //. Takes a list of objects and plucks the value of the specified key - //. for each object in the list. Returns the value wrapped in a Just - //. if an object has the key and a Nothing if it does not. + //. Takes a [type representative](#type-representatives), a property name, + //. and a list of objects and returns a list of equal length. Each element + //. of the output list is Just the value of the specified property of the + //. corresponding object if the value is of the specified type (according + //. to [`is`](#is)); Nothing otherwise. //. - //. ```javascript - //. > S.pluck('a', [{a: 1, b: 2}, {a: 4, b: 5}, {b: 3, c: 7}]) - //. [Just(1), Just(4), Nothing()] + //. See also [`get`](#get). //. - //. > S.pluck('x', [{x: 1}, {x: 2}, {x: undefined}]) - //. [Just(1), Just(2), Just(undefined)] + //. ```javascript + //. > S.pluck(Number, 'x', [{x: 1}, {x: 2}, {x: '3'}, {x: null}, {}]) + //. [Just(1), Just(2), Nothing(), Nothing(), Nothing()] //. ``` - S.pluck = def('pluck', [String, Accessible], function(key, xs) { - return R.map(get(key), xs); + S.pluck = + def('pluck', [TypeRep, String, Accessible], function(type, key, xs) { + return R.map(get(type, key), xs); }); //. ### Object - //# get :: String -> Object -> Maybe * + //# get :: TypeRep a -> String -> Accessible -> Maybe a //. - //. Takes a property name and an object and returns Just the value of - //. the specified property of the object if the object has such an own - //. property; Nothing otherwise. + //. Takes a [type representative](#type-representatives), a property + //. name, and an object and returns Just the value of the specified object + //. property if it is of the specified type (according to [`is`](#is)); + //. Nothing otherwise. + //. + //. The `Object` type representative may be used as a catch-all since almost + //. every value has `Object.prototype` in its prototype chain. + //. + //. See also [`gets`](#gets). //. //. ```javascript - //. > S.get('x', {x: 1, y: 2}) + //. > S.get(Number, 'x', {x: 1, y: 2}) //. Just(1) //. - //. > S.get('toString', {x: 1, y: 2}) + //. > S.get(Number, 'x', {x: '1', y: '2'}) + //. Nothing() + //. + //. > S.get(Number, 'x', {}) //. Nothing() //. ``` var get = S.get = - def('get', [String, Accessible], - R.ifElse(R.has, R.compose(Just, R.prop), Nothing)); + def('get', [TypeRep, String, Accessible], function(type, key, obj) { + return filter(is(type), Just(obj[key])); + }); - //# gets :: [String] -> Object -> Maybe * + //# gets :: TypeRep a -> [String] -> Accessible -> Maybe a + //. + //. Takes a [type representative](#type-representatives), a list of property + //. names, and an object and returns Just the value at the path specified by + //. the list of property names if such a path exists and the value is of the + //. specified type; Nothing otherwise. //. - //. Takes a list of property names and an object and returns Just the - //. value at the path specified by the list of property names if such - //. a path exists; Nothing otherwise. + //. See also [`get`](#get). //. //. ```javascript - //. > S.gets(['a', 'b', 'c'], {a: {b: {c: 42}}}) + //. > S.gets(Number, ['a', 'b', 'c'], {a: {b: {c: 42}}}) //. Just(42) //. - //. > S.gets(['a', 'b', 'c'], {}) + //. > S.gets(Number, ['a', 'b', 'c'], {a: {b: {c: '42'}}}) + //. Nothing() + //. + //. > S.gets(Number, ['a', 'b', 'c'], {}) //. Nothing() //. ``` - S.gets = def('gets', [Accessible, Accessible], function(keys, obj) { - return R.reduce(function(acc, key) { - return R.chain(get(key), acc); - }, Just(obj), keys); + S.gets = + def('gets', [TypeRep, Accessible, Accessible], function(type, keys, obj) { + var f = function(m, k) { return R.chain(get(Accessible, k), m); }; + return filter(is(type), R.reduce(f, Just(obj), keys)); }); //. ### Parse diff --git a/test/index.js b/test/index.js index f2a3cad2..c3ebef9e 100644 --- a/test/index.js +++ b/test/index.js @@ -70,7 +70,7 @@ describe('classify', function() { it('type checks its arguments', function() { assert.throws(function() { S.is([1, 2, 3]); }, errorEq(TypeError, - '‘is’ requires a value of type Type ' + + '‘is’ requires a value of type TypeRep ' + 'as its first argument; received [1, 2, 3]')); }); @@ -1940,48 +1940,41 @@ describe('list', function() { describe('pluck', function() { - it('is a binary function', function() { + it('is a ternary function', function() { eq(typeof S.pluck, 'function'); - eq(S.pluck.length, 2); + eq(S.pluck.length, 3); }); it('type checks its arguments', function() { assert.throws(function() { S.pluck([1, 2, 3]); }, errorEq(TypeError, - '‘pluck’ requires a value of type String ' + + '‘pluck’ requires a value of type TypeRep ' + 'as its first argument; received [1, 2, 3]')); - assert.throws(function() { S.pluck('x', null); }, + assert.throws(function() { S.pluck(Number, [1, 2, 3]); }, errorEq(TypeError, - 'The second argument to ‘pluck’ ' + - 'cannot be null or undefined')); - }); - - it('returns array of Justs for found keys', function() { - var xs = [{a: 1, b: 3}, {a: 2, b: 4}, {a: 3, b: 5}]; - eq(S.pluck('a', xs), [S.Just(1), S.Just(2), S.Just(3)]); - }); - - it('returns array of Nothings for keys not found', function() { - var xs = [{a: 1, b: 3}, {a: 2, b: 4}, {a: 3, b: 5}]; - eq(S.pluck('c', xs), [S.Nothing(), S.Nothing(), S.Nothing()]); - }); + '‘pluck’ requires a value of type String ' + + 'as its second argument; received [1, 2, 3]')); - it('returns Just(undefined) for defined key with no value', function() { - var xs = [{a: 1, b: 3}, {a: void 0, b: 4}, {a: 3, b: 5}]; - eq(S.pluck('a', xs), [S.Just(1), S.Just(undefined), S.Just(3)]); + assert.throws(function() { S.pluck(Number, 'x', null); }, + errorEq(TypeError, + 'The third argument to ‘pluck’ ' + + 'cannot be null or undefined')); }); - it('returns an array of Maybes for various values', function() { - var xs = [{a: 1}, {a: void 0}, {a: 4}, {b: 1}]; - eq(S.pluck('a', xs), - [S.Just(1), S.Just(undefined), S.Just(4), S.Nothing()]); + it('returns a list of satisfactory plucked values', function() { + var xs = [{x: '1'}, {x: 2}, {x: null}, {x: undefined}, {}]; + eq(S.pluck(Number, 'x', []), []); + eq(S.pluck(Number, 'x', xs), + [S.Nothing(), S.Just(2), S.Nothing(), S.Nothing(), S.Nothing()]); + eq(S.pluck(Object, 'x', xs), + [S.Just('1'), S.Just(2), S.Nothing(), S.Nothing(), S.Nothing()]); }); it('is curried', function() { - var xs = [{x: 1}, {x: 2}, {x: 3}]; - eq(S.pluck('x').length, 1); - eq(S.pluck('x')(xs), [S.Just(1), S.Just(2), S.Just(3)]); + eq(S.pluck(Number).length, 2); + eq(S.pluck(Number)('x').length, 1); + eq(S.pluck(Number)('x')([{x: 42}]), [S.Just(42)]); }); }); @@ -1992,67 +1985,83 @@ describe('object', function() { describe('get', function() { - it('is a binary function', function() { + it('is a ternary function', function() { eq(typeof S.get, 'function'); - eq(S.get.length, 2); + eq(S.get.length, 3); }); it('type checks its arguments', function() { assert.throws(function() { S.get([1, 2, 3]); }, errorEq(TypeError, - '‘get’ requires a value of type String ' + + '‘get’ requires a value of type TypeRep ' + 'as its first argument; received [1, 2, 3]')); - assert.throws(function() { S.get('x', null); }, + assert.throws(function() { S.get(Number, [1, 2, 3]); }, errorEq(TypeError, - 'The second argument to ‘get’ ' + + '‘get’ requires a value of type String ' + + 'as its second argument; received [1, 2, 3]')); + + assert.throws(function() { S.get(Number, 'x', null); }, + errorEq(TypeError, + 'The third argument to ‘get’ ' + 'cannot be null or undefined')); }); it('returns a Maybe', function() { var obj = {x: 0, y: 42}; - eq(S.get('x', obj), S.Just(0)); - eq(S.get('y', obj), S.Just(42)); - eq(S.get('z', obj), S.Nothing()); + eq(S.get(Number, 'x', obj), S.Just(0)); + eq(S.get(Number, 'y', obj), S.Just(42)); + eq(S.get(Number, 'z', obj), S.Nothing()); + eq(S.get(String, 'x', obj), S.Nothing()); + eq(S.get(Object, 'x', obj), S.Just(0)); }); it('is curried', function() { - eq(S.get('x').length, 1); - eq(S.get('x')({x: 42}), S.Just(42)); + eq(S.get(Number).length, 2); + eq(S.get(Number)('x').length, 1); + eq(S.get(Number)('x')({x: 42}), S.Just(42)); }); }); describe('gets', function() { - it('is a binary function', function() { + it('is a ternary function', function() { eq(typeof S.gets, 'function'); - eq(S.gets.length, 2); + eq(S.gets.length, 3); }); it('type checks its arguments', function() { - assert.throws(function() { S.gets(null); }, + assert.throws(function() { S.gets([1, 2, 3]); }, errorEq(TypeError, - 'The first argument to ‘gets’ ' + - 'cannot be null or undefined')); + '‘gets’ requires a value of type TypeRep ' + + 'as its first argument; received [1, 2, 3]')); - assert.throws(function() { S.gets([], null); }, + assert.throws(function() { S.gets(Number, null); }, errorEq(TypeError, 'The second argument to ‘gets’ ' + 'cannot be null or undefined')); + + assert.throws(function() { S.gets(Number, [], null); }, + errorEq(TypeError, + 'The third argument to ‘gets’ ' + + 'cannot be null or undefined')); }); it('returns a Maybe', function() { var obj = {x: {z: 0}, y: 42}; - eq(S.gets([], obj), S.Just({x: {z: 0}, y: 42})); - eq(S.gets(['y'], obj), S.Just(42)); - eq(S.gets(['z'], obj), S.Nothing()); - eq(S.gets(['x', 'z'], obj), S.Just(0)); + eq(S.gets(Number, ['x'], obj), S.Nothing()); + eq(S.gets(Number, ['y'], obj), S.Just(42)); + eq(S.gets(Number, ['z'], obj), S.Nothing()); + eq(S.gets(Number, ['x', 'z'], obj), S.Just(0)); + eq(S.gets(Number, [], obj), S.Nothing()); + eq(S.gets(Object, [], obj), S.Just({x: {z: 0}, y: 42})); }); it('is curried', function() { - eq(S.gets(['x']).length, 1); - eq(S.gets(['x'])({x: 42}), S.Just(42)); + eq(S.gets(Number).length, 2); + eq(S.gets(Number)(['x']).length, 1); + eq(S.gets(Number)(['x'])({x: 42}), S.Just(42)); }); });