Repository: sanctuary-js/sanctuary (js)

Issue:
Type validation using predicates rather than TypeRep's
Since most type-checking libraries provide convenient ways to get the predicate for a type, this would allow for much greater interoperability with run-time type systems.

With Sanctuary:

``` js
const isInt = allPass([is(Number), x => x % 1 === 0]);

get(isInt, 'x', {x: 0.5}); //-> Nothing
```

With TComb:

``` js
const Int = t.refinement(t.Number, x => x % 1 === 0, 'Int');

get(Int.is, 'x', {x: 0.5}); //-> Nothing
```

With Sanctuary Def:

``` js
const Int = $.NullaryType('my/Int', allPass([is(Number), x => x % 1 === 0]));

get(Int.test, 'x', {x: 0.5}); //-> Nothing
```

The possibilities are _endless!_ Another option is to accept Sanctuary Def types, which would make the code a little more concise, but the possibilities slightly less endless.


Pull request:
Change type validators to use predicate instead of TypeRep
Commit description:

> This commit will close #149 by changing all functions
> that have built-in type validation to use predicates
> instead of TypeReps to perform their validations.
> 
> The functions affected are:
> - get()
> - gets()
> - parseJson()
> 
> The only function untouched is `is()`, for it is used
> to turn a TypeRep into a predicate.


Patch:
diff --git a/index.js b/index.js
index 0f65b7f..302c76e 100644
--- a/index.js
+++ b/index.js
@@ -2787,64 +2787,62 @@
   }
   S.prop = def('prop', {a: [Accessible]}, [$.String, a, b], prop);
 
-  //# get :: Accessible a => TypeRep b -> String -> a -> Maybe b
+  //# get :: Accessible a => (b -> Boolean) -> String -> a -> Maybe c
   //.
-  //. 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 most
-  //. values have `Object.prototype` in their prototype chains.
+  //. Takes a predicate, a property name, and an object and returns Just the
+  //. value of the specified object property if it exists and the value
+  //. satisfies the given predicate; Nothing otherwise.
   //.
   //. See also [`gets`](#gets) and [`prop`](#prop).
   //.
   //. ```javascript
-  //. > S.get(Number, 'x', {x: 1, y: 2})
+  //. > S.get(S.is(Number), 'x', {x: 1, y: 2})
   //. Just(1)
   //.
-  //. > S.get(Number, 'x', {x: '1', y: '2'})
+  //. > S.get(S.is(Number), 'x', {x: '1', y: '2'})
   //. Nothing
   //.
-  //. > S.get(Number, 'x', {})
+  //. > S.get(S.is(Number), 'x', {})
   //. Nothing
   //. ```
-  function get(typeRep, key, obj) {
-    var x = obj[key];
-    return is(typeRep, x) ? Just(x) : Nothing;
+  function get(pred, key, obj) {
+    var x = null;
+    return key in obj && pred(x = obj[key]) ? Just(x) : Nothing;
   }
   S.get =
-  def('get', {a: [Accessible]}, [TypeRep, $.String, a, $Maybe(b)], get);
+  def('get', {a: [Accessible]}, [$.Function, $.String, a, $Maybe(c)], get);
 
-  //# gets :: Accessible a => TypeRep b -> Array String -> a -> Maybe b
+  //# gets :: Accessible a => (b -> Boolean) -> Array String -> a -> Maybe c
   //.
-  //. Takes a [type representative](#type-representatives), an array of
-  //. property names, and an object and returns Just the value at the path
-  //. specified by the array of property names if such a path exists and
-  //. the value is of the specified type; Nothing otherwise.
+  //. Takes a predicate, an array of property names, and an object and returns
+  //. Just the value at the path specified by the array of property names if
+  //. such a path exists and the value satisfies the given predicate; Nothing
+  //. otherwise.
   //.
   //. See also [`get`](#get).
   //.
   //. ```javascript
-  //. > S.gets(Number, ['a', 'b', 'c'], {a: {b: {c: 42}}})
+  //. > S.gets(S.is(Number), ['a', 'b', 'c'], {a: {b: {c: 42}}})
   //. Just(42)
   //.
-  //. > S.gets(Number, ['a', 'b', 'c'], {a: {b: {c: '42'}}})
+  //. > S.gets(S.is(Number), ['a', 'b', 'c'], {a: {b: {c: '42'}}})
   //. Nothing
   //.
-  //. > S.gets(Number, ['a', 'b', 'c'], {})
+  //. > S.gets(S.is(Number), ['a', 'b', 'c'], {})
   //. Nothing
   //. ```
-  function gets(typeRep, keys, obj) {
+  function gets(pred, keys, obj) {
     var maybe = keys.reduce(function(m, k) {
-      return m.chain(function(x) { return x == null ? Nothing : Just(x[k]); });
+      return m.chain(function(x) {
+        return x != null && k in x ? Just(x[k]) : Nothing;
+      });
     }, Just(obj));
-    return filter(function(x) { return is(typeRep, x); }, maybe);
+    return filter(pred, maybe);
   }
   S.gets =
   def('gets',
       {a: [Accessible]},
-      [TypeRep, $.Array($.String), a, $Maybe(b)],
+      [$.Function, $.Array($.String), a, $Maybe(c)],
       gets);
 
   //# keys :: StrMap a -> Array String
@@ -3255,29 +3253,27 @@
   S.parseInt =
   def('parseInt', {}, [$.Integer, $.String, $Maybe($.Integer)], parseInt_);
 
-  //# parseJson :: TypeRep a -> String -> Maybe a
+  //# parseJson :: (a -> Boolean) -> String -> Maybe b
   //.
-  //. Takes a [type representative](#type-representatives) and a string which
-  //. may or may not be valid JSON, and returns Just the result of applying
-  //. `JSON.parse` to the string *if* the result is of the specified type
-  //. (according to [`is`](#is)); Nothing otherwise.
+  //. Takes a predicate and a string which may or may not be valid JSON, and
+  //. returns Just the result of applying `JSON.parse` to the string *if* the
+  //. result satisfies the predicate; Nothing otherwise.
   //.
   //. ```javascript
-  //. > S.parseJson(Array, '["foo","bar","baz"]')
+  //. > S.parseJson(S.is(Array), '["foo","bar","baz"]')
   //. Just(['foo', 'bar', 'baz'])
   //.
-  //. > S.parseJson(Array, '[')
+  //. > S.parseJson(S.is(Array), '[')
   //. Nothing
   //.
-  //. > S.parseJson(Object, '["foo","bar","baz"]')
+  //. > S.parseJson(S.is(Object), '["foo","bar","baz"]')
   //. Nothing
   //. ```
-  function parseJson(typeRep, s) {
-    return filter(function(x) { return is(typeRep, x); },
-                  encase(JSON.parse, s));
+  function parseJson(pred, s) {
+    return filter(pred, encase(JSON.parse, s));
   }
   S.parseJson =
-  def('parseJson', {}, [TypeRep, $.String, $Maybe(a)], parseJson);
+  def('parseJson', {}, [$.Function, $.String, $Maybe(b)], parseJson);
 
   //. ### RegExp
 


Test patch:
diff --git a/test/get.js b/test/get.js
index a52cf62..dabe643 100644
--- a/test/get.js
+++ b/test/get.js
@@ -17,44 +17,45 @@ test('get', function() {
          TypeError,
          'Invalid value\n' +
          '\n' +
-         'get :: Accessible a => TypeRep -> String -> a -> Maybe b\n' +
-         '                       ^^^^^^^\n' +
+         'get :: Accessible a => Function -> String -> a -> Maybe c\n' +
+         '                       ^^^^^^^^\n' +
          '                          1\n' +
          '\n' +
          '1)  [1, 2, 3] :: Array Number, Array FiniteNumber, Array NonZeroFiniteNumber, Array Integer, Array ValidNumber\n' +
          '\n' +
-         'The value at position 1 is not a member of ‘TypeRep’.\n');
+         'The value at position 1 is not a member of ‘Function’.\n');
 
-  throws(function() { S.get(Number, [1, 2, 3]); },
+  throws(function() { S.get(S.is(Number), [1, 2, 3]); },
          TypeError,
          'Invalid value\n' +
          '\n' +
-         'get :: Accessible a => TypeRep -> String -> a -> Maybe b\n' +
-         '                                  ^^^^^^\n' +
-         '                                    1\n' +
+         'get :: Accessible a => Function -> String -> a -> Maybe c\n' +
+         '                                   ^^^^^^\n' +
+         '                                     1\n' +
          '\n' +
          '1)  [1, 2, 3] :: Array Number, Array FiniteNumber, Array NonZeroFiniteNumber, Array Integer, Array ValidNumber\n' +
          '\n' +
          'The value at position 1 is not a member of ‘String’.\n');
 
-  throws(function() { S.get(Number, 'x', null); },
+  throws(function() { S.get(S.is(Number), 'x', null); },
          TypeError,
          'Type-class constraint violation\n' +
          '\n' +
-         'get :: Accessible a => TypeRep -> String -> a -> Maybe b\n' +
-         '       ^^^^^^^^^^^^                         ^\n' +
-         '                                            1\n' +
+         'get :: Accessible a => Function -> String -> a -> Maybe c\n' +
+         '       ^^^^^^^^^^^^                          ^\n' +
+         '                                             1\n' +
          '\n' +
          '1)  null :: Null\n' +
          '\n' +
          '‘get’ requires ‘a’ to satisfy the Accessible type-class constraint; the value at position 1 does not.\n');
 
-  eq(S.get(Number, 'x', {x: 0, y: 42}), S.Just(0));
-  eq(S.get(Number, 'y', {x: 0, y: 42}), S.Just(42));
-  eq(S.get(Number, 'z', {x: 0, y: 42}), S.Nothing);
-  eq(S.get(String, 'x', {x: 0, y: 42}), S.Nothing);
+  eq(S.get(S.is(Number), 'x', {x: 0, y: 42}), S.Just(0));
+  eq(S.get(S.is(Number), 'y', {x: 0, y: 42}), S.Just(42));
+  eq(S.get(S.is(Number), 'z', {x: 0, y: 42}), S.Nothing);
+  eq(S.get(S.is(String), 'z', {x: 0, y: 42}), S.Nothing);
+  eq(S.get(S.is(String), 'x', {x: 0, y: 42}), S.Nothing);
 
-  eq(S.get(RegExp, 'x', {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
-  eq(S.get(vm.runInNewContext('RegExp'), 'x', {x: /.*/}), S.Just(/.*/));
+  eq(S.get(S.is(RegExp), 'x', {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
+  eq(S.get(S.is(vm.runInNewContext('RegExp')), 'x', {x: /.*/}), S.Just(/.*/));
 
 });
diff --git a/test/gets.js b/test/gets.js
index 6fe9b1c..0bd49f4 100644
--- a/test/gets.js
+++ b/test/gets.js
@@ -17,47 +17,47 @@ test('gets', function() {
          TypeError,
          'Invalid value\n' +
          '\n' +
-         'gets :: Accessible a => TypeRep -> Array String -> a -> Maybe b\n' +
-         '                        ^^^^^^^\n' +
+         'gets :: Accessible a => Function -> Array String -> a -> Maybe c\n' +
+         '                        ^^^^^^^^\n' +
          '                           1\n' +
          '\n' +
          '1)  [1, 2, 3] :: Array Number, Array FiniteNumber, Array NonZeroFiniteNumber, Array Integer, Array ValidNumber\n' +
          '\n' +
-         'The value at position 1 is not a member of ‘TypeRep’.\n');
+         'The value at position 1 is not a member of ‘Function’.\n');
 
-  throws(function() { S.gets(Number, null); },
+  throws(function() { S.gets(S.is(Number), null); },
          TypeError,
          'Invalid value\n' +
          '\n' +
-         'gets :: Accessible a => TypeRep -> Array String -> a -> Maybe b\n' +
-         '                                   ^^^^^^^^^^^^\n' +
-         '                                        1\n' +
+         'gets :: Accessible a => Function -> Array String -> a -> Maybe c\n' +
+         '                                    ^^^^^^^^^^^^\n' +
+         '                                         1\n' +
          '\n' +
          '1)  null :: Null\n' +
          '\n' +
          'The value at position 1 is not a member of ‘Array String’.\n');
 
-  throws(function() { S.gets(Number, [], null); },
+  throws(function() { S.gets(S.is(Number), [], null); },
          TypeError,
          'Type-class constraint violation\n' +
          '\n' +
-         'gets :: Accessible a => TypeRep -> Array String -> a -> Maybe b\n' +
-         '        ^^^^^^^^^^^^                               ^\n' +
-         '                                                   1\n' +
+         'gets :: Accessible a => Function -> Array String -> a -> Maybe c\n' +
+         '        ^^^^^^^^^^^^                                ^\n' +
+         '                                                    1\n' +
          '\n' +
          '1)  null :: Null\n' +
          '\n' +
          '‘gets’ requires ‘a’ to satisfy the Accessible type-class constraint; the value at position 1 does not.\n');
 
-  eq(S.gets(Number, ['x'], {x: {z: 0}, y: 42}), S.Nothing);
-  eq(S.gets(Number, ['y'], {x: {z: 0}, y: 42}), S.Just(42));
-  eq(S.gets(Number, ['z'], {x: {z: 0}, y: 42}), S.Nothing);
-  eq(S.gets(Number, ['x', 'z'], {x: {z: 0}, y: 42}), S.Just(0));
-  eq(S.gets(Number, ['a', 'b', 'c'], {x: {z: 0}, y: 42}), S.Nothing);
-  eq(S.gets(Number, [], {x: {z: 0}, y: 42}), S.Nothing);
-  eq(S.gets(Object, [], {x: {z: 0}, y: 42}), S.Just({x: {z: 0}, y: 42}));
+  eq(S.gets(S.is(Number), ['x'], {x: {z: 0}, y: 42}), S.Nothing);
+  eq(S.gets(S.is(Number), ['y'], {x: {z: 0}, y: 42}), S.Just(42));
+  eq(S.gets(S.is(Number), ['z'], {x: {z: 0}, y: 42}), S.Nothing);
+  eq(S.gets(S.is(Number), ['x', 'z'], {x: {z: 0}, y: 42}), S.Just(0));
+  eq(S.gets(S.is(Number), ['a', 'b', 'c'], {x: {z: 0}, y: 42}), S.Nothing);
+  eq(S.gets(S.is(Number), [], {x: {z: 0}, y: 42}), S.Nothing);
+  eq(S.gets(S.is(Object), [], {x: {z: 0}, y: 42}), S.Just({x: {z: 0}, y: 42}));
 
-  eq(S.gets(RegExp, ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
-  eq(S.gets(vm.runInNewContext('RegExp'), ['x'], {x: /.*/}), S.Just(/.*/));
+  eq(S.gets(S.is(RegExp), ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
+  eq(S.gets(S.is(vm.runInNewContext('RegExp')), ['x'], {x: /.*/}), S.Just(/.*/));
 
 });
diff --git a/test/parseJson.js b/test/parseJson.js
index dd03304..6bf452c 100644
--- a/test/parseJson.js
+++ b/test/parseJson.js
@@ -15,28 +15,28 @@ test('parseJson', function() {
          TypeError,
          'Invalid value\n' +
          '\n' +
-         'parseJson :: TypeRep -> String -> Maybe a\n' +
-         '             ^^^^^^^\n' +
+         'parseJson :: Function -> String -> Maybe b\n' +
+         '             ^^^^^^^^\n' +
          '                1\n' +
          '\n' +
          '1)  "String" :: String\n' +
          '\n' +
-         'The value at position 1 is not a member of ‘TypeRep’.\n');
+         'The value at position 1 is not a member of ‘Function’.\n');
 
-  throws(function() { S.parseJson(Array, [1, 2, 3]); },
+  throws(function() { S.parseJson(S.is(Array), [1, 2, 3]); },
          TypeError,
          'Invalid value\n' +
          '\n' +
-         'parseJson :: TypeRep -> String -> Maybe a\n' +
-         '                        ^^^^^^\n' +
-         '                          1\n' +
+         'parseJson :: Function -> String -> Maybe b\n' +
+         '                         ^^^^^^\n' +
+         '                           1\n' +
          '\n' +
          '1)  [1, 2, 3] :: Array Number, Array FiniteNumber, Array NonZeroFiniteNumber, Array Integer, Array ValidNumber\n' +
          '\n' +
          'The value at position 1 is not a member of ‘String’.\n');
 
-  eq(S.parseJson(Object, '[Invalid JSON]'), S.Nothing);
-  eq(S.parseJson(Array, '{"foo":"bar"}'), S.Nothing);
-  eq(S.parseJson(Array, '["foo","bar"]'), S.Just(['foo', 'bar']));
+  eq(S.parseJson(S.is(Object), '[Invalid JSON]'), S.Nothing);
+  eq(S.parseJson(S.is(Array), '{"foo":"bar"}'), S.Nothing);
+  eq(S.parseJson(S.is(Array), '["foo","bar"]'), S.Just(['foo', 'bar']));
 
 });