Repository: sanctuary-js/sanctuary (js)

Issue:
`is` can be fooled
```js
S.is(function Number () {}, 4)
// -> true
```

While documentation says:

`is :: TypeRep a -⁠> Any -⁠> Boolean`

Takes a type representative and a value of any
type and returns `true` iff the given value is of the specified type.
Subtyping is not respected.

Pull request:
classify: change S.is to take a sanctuary-def Type value
Closes #502

This pull request greatly increases the expressive power of `S.is` by allowing us to specify types such as `Array (Maybe Integer)` rather than `Array ???`.

If this pull request is accepted, #481 will no longer be warranted.

/cc @futpib, @gautaz, @miwillhite, @najlachamseddine, @tomkel


Patch:
diff --git a/index.js b/index.js
index 5484619..488142c 100644
--- a/index.js
+++ b/index.js
@@ -451,32 +451,22 @@
         return r;
       });
 
-  //# is :: TypeRep a -> Any -> Boolean
+  //# is :: Type -> Any -> Boolean
   //.
-  //. Takes a [type representative](#type-representatives) and a value of any
-  //. type and returns `true` [iff][] the given value is of the specified type.
-  //. Subtyping is not respected.
+  //. Returns `true` [iff][] the given value is a member of the specified type.
+  //. See [`$.test`][] for details.
   //.
   //. ```javascript
-  //. > S.is(Number, 42)
+  //. > S.is($.Array($.Integer), [1, 2, 3])
   //. true
   //.
-  //. > S.is(Object, 42)
-  //. false
-  //.
-  //. > S.is(String, 42)
+  //. > S.is($.Array($.Integer), [1, 2, 3.14])
   //. false
   //. ```
-  function is(typeRep, x) {
-    var xType = type(x);
-    if ($.String._test(typeRep['@@type'])) {
-      return xType === typeRep['@@type'];
-    } else {
-      var match = /function (\w*)/.exec(typeRep);
-      return match != null && match[1] === xType;
-    }
+  function is(type, x) {
+    return $.test(env, type, x);
   }
-  S.is = def('is', {}, [TypeRep(a), $.Any, $.Boolean], is);
+  S.is = def('is', {}, [$.Type, $.Any, $.Boolean], is);
 
   //. ### Showable
 
@@ -3699,19 +3689,19 @@
   //. See also [`gets`](#gets) and [`prop`](#prop).
   //.
   //. ```javascript
-  //. > S.get(S.is(Number), 'x', {x: 1, y: 2})
+  //. > S.get(S.is($.Number), 'x', {x: 1, y: 2})
   //. Just(1)
   //.
-  //. > S.get(S.is(Number), 'x', {x: '1', y: '2'})
+  //. > S.get(S.is($.Number), 'x', {x: '1', y: '2'})
   //. Nothing
   //.
-  //. > S.get(S.is(Number), 'x', {})
+  //. > S.get(S.is($.Number), 'x', {})
   //. Nothing
   //.
-  //. > S.get($.test([], $.Array($.Number)), 'x', {x: [1, 2, 3]})
+  //. > S.get(S.is($.Array($.Number)), 'x', {x: [1, 2, 3]})
   //. Just([1, 2, 3])
   //.
-  //. > S.get($.test([], $.Array($.Number)), 'x', {x: [1, 2, 3, null]})
+  //. > S.get(S.is($.Array($.Number)), 'x', {x: [1, 2, 3, null]})
   //. Nothing
   //. ```
   function get(pred, key, x) {
@@ -3733,13 +3723,13 @@
   //. See also [`get`](#get).
   //.
   //. ```javascript
-  //. > S.gets(S.is(Number), ['a', 'b', 'c'], {a: {b: {c: 42}}})
+  //. > S.gets(S.is($.Number), ['a', 'b', 'c'], {a: {b: {c: 42}}})
   //. Just(42)
   //.
-  //. > S.gets(S.is(Number), ['a', 'b', 'c'], {a: {b: {c: '42'}}})
+  //. > S.gets(S.is($.Number), ['a', 'b', 'c'], {a: {b: {c: '42'}}})
   //. Nothing
   //.
-  //. > S.gets(S.is(Number), ['a', 'b', 'c'], {})
+  //. > S.gets(S.is($.Number), ['a', 'b', 'c'], {})
   //. Nothing
   //. ```
   function gets(pred, keys, x) {
@@ -4231,16 +4221,16 @@
   //. result satisfies the predicate; Nothing otherwise.
   //.
   //. ```javascript
-  //. > S.parseJson($.test([], $.Array($.Integer)), '[')
+  //. > S.parseJson(S.is($.Array($.Integer)), '[')
   //. Nothing
   //.
-  //. > S.parseJson($.test([], $.Array($.Integer)), '["1","2","3"]')
+  //. > S.parseJson(S.is($.Array($.Integer)), '["1","2","3"]')
   //. Nothing
   //.
-  //. > S.parseJson($.test([], $.Array($.Integer)), '[0,1.5,3,4.5]')
+  //. > S.parseJson(S.is($.Array($.Integer)), '[0,1.5,3,4.5]')
   //. Nothing
   //.
-  //. > S.parseJson($.test([], $.Array($.Integer)), '[1,2,3]')
+  //. > S.parseJson(S.is($.Array($.Integer)), '[1,2,3]')
   //. Just([1, 2, 3])
   //. ```
   function parseJson(pred, s) {
@@ -4631,6 +4621,7 @@
 //. [Semigroupoid]:     v:fantasyland/fantasy-land#semigroupoid
 //. [Traversable]:      v:fantasyland/fantasy-land#traversable
 //. [UnaryType]:        v:sanctuary-js/sanctuary-def#UnaryType
+//. [`$.test`]:         v:sanctuary-js/sanctuary-def#test
 //. [`Z.alt`]:          v:sanctuary-js/sanctuary-type-classes#alt
 //. [`Z.ap`]:           v:sanctuary-js/sanctuary-type-classes#ap
 //. [`Z.apFirst`]:      v:sanctuary-js/sanctuary-type-classes#apFirst


Test patch:
diff --git a/test/get.js b/test/get.js
index a929196..6d22a5d 100644
--- a/test/get.js
+++ b/test/get.js
@@ -15,19 +15,17 @@ test('get', function() {
   eq(S.get.length, 3);
   eq(S.get.toString(), 'get :: (Any -> Boolean) -> String -> a -> Maybe b');
 
-  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(S.is(RegExp), 'x', {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
-  eq(S.get(S.is(vm.runInNewContext('RegExp')), 'x', {x: /.*/}), S.Just(/.*/));
+  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(S.is($.RegExp), 'x', {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
 
   eq(S.get(S.K(true), 'valueOf', null), S.Nothing);
   eq(S.get(S.K(true), 'valueOf', undefined), S.Nothing);
 
-  eq(S.get($.test([], $.Array($.Number)), 'x', {x: [1, 2]}), S.Just([1, 2]));
-  eq(S.get($.test([], $.Array($.Number)), 'x', {x: [1, 2, null]}), S.Nothing);
+  eq(S.get(S.is($.Array($.Number)), 'x', {x: [1, 2]}), S.Just([1, 2]));
+  eq(S.get(S.is($.Array($.Number)), 'x', {x: [1, 2, null]}), S.Nothing);
 
 });
diff --git a/test/gets.js b/test/gets.js
index 26e1dbf..232b764 100644
--- a/test/gets.js
+++ b/test/gets.js
@@ -15,21 +15,19 @@ test('gets', function() {
   eq(S.gets.length, 3);
   eq(S.gets.toString(), 'gets :: (Any -> Boolean) -> Array String -> a -> Maybe b');
 
-  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(S.is(RegExp), ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
-  eq(S.gets(S.is(vm.runInNewContext('RegExp')), ['x'], {x: /.*/}), S.Just(/.*/));
+  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(S.is($.RegExp), ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
 
   eq(S.gets(S.K(true), ['valueOf'], null), S.Nothing);
   eq(S.gets(S.K(true), ['valueOf'], undefined), S.Nothing);
 
-  eq(S.gets($.test([], $.Array($.Number)), ['x'], {x: [1, 2]}), S.Just([1, 2]));
-  eq(S.gets($.test([], $.Array($.Number)), ['x'], {x: [1, 2, null]}), S.Nothing);
+  eq(S.gets(S.is($.Array($.Number)), ['x'], {x: [1, 2]}), S.Just([1, 2]));
+  eq(S.gets(S.is($.Array($.Number)), ['x'], {x: [1, 2, null]}), S.Nothing);
 
 });
diff --git a/test/is.js b/test/is.js
index 1e1006b..0db3225 100644
--- a/test/is.js
+++ b/test/is.js
@@ -1,9 +1,10 @@
 'use strict';
 
-var vm = require('vm');
+var $ = require('sanctuary-def');
 
 var S = require('..');
 
+var Sum = require('./internal/Sum');
 var eq = require('./internal/eq');
 
 
@@ -11,44 +12,43 @@ test('is', function() {
 
   eq(typeof S.is, 'function');
   eq(S.is.length, 2);
-  eq(S.is.toString(), 'is :: TypeRep a -> Any -> Boolean');
-
-  eq(S.is(Array,    []),                  true);
-  eq(S.is(Boolean,  false),               true);
-  eq(S.is(Date,     new Date(0)),         true);
-  eq(S.is(Function, function() {}),       true);
-  eq(S.is(Number,   0),                   true);
-  eq(S.is(Object,   {}),                  true);
-  eq(S.is(RegExp,   /(?:)/),              true);
-  eq(S.is(String,   ''),                  true);
-  eq(S.is(Boolean,  new Boolean(false)),  true);
-  eq(S.is(Number,   new Number(0)),       true);
-  eq(S.is(String,   new String('')),      true);
-  eq(S.is(Array,    null),                false);
-  eq(S.is(Array,    undefined),           false);
-  eq(S.is(Array,    {}),                  false);
-
-  eq(S.is(S.Maybe,  S.Nothing),           true);
-  eq(S.is(S.Maybe,  S.Just(9)),           true);
-  eq(S.is(S.Maybe,  S.Left(9)),           false);
-  eq(S.is(S.Maybe,  S.Right(9)),          false);
-
-  eq(S.is(S.Either, S.Nothing),           false);
-  eq(S.is(S.Either, S.Just(9)),           false);
-  eq(S.is(S.Either, S.Left(9)),           true);
-  eq(S.is(S.Either, S.Right(9)),          true);
-
-  function FooBar() {}
-  FooBar['@@type'] = 'foobar/FooBar';
-  function Foo() {}
-  Foo.prototype = new FooBar();
-  function Bar() {}
-  Bar.prototype = new FooBar();
-
-  eq(S.is(FooBar, new Foo()), true);
-  eq(S.is(FooBar, new Bar()), true);
-
-  eq(S.is(Array, vm.runInNewContext('[1, 2, 3]')), true);
-  eq(S.is(vm.runInNewContext('Array'), [1, 2, 3]), true);
+  eq(S.is.toString(), 'is :: Type -> Any -> Boolean');
+
+  eq(S.is($.Boolean, true), true);
+  eq(S.is($.Boolean, false), true);
+  eq(S.is($.Boolean, new Boolean(true)), false);
+  eq(S.is($.Boolean, new Boolean(false)), false);
+
+  eq(S.is($.Array($.Integer), null), false);
+  eq(S.is($.Array($.Integer), undefined), false);
+  eq(S.is($.Array($.Integer), ['1', '2', '3']), false);
+  eq(S.is($.Array($.Integer), [1, 2, 3.14]), false);
+  eq(S.is($.Array($.Integer), [1, 2, 3]), true);
+  eq(S.is($.Array($.Integer), []), true);
+
+  eq(S.is(S.MaybeType($.Integer), S.Nothing), true);
+  eq(S.is(S.MaybeType($.Integer), S.Just(0)), true);
+  eq(S.is(S.MaybeType($.Integer), S.Left(0)), false);
+  eq(S.is(S.MaybeType($.Integer), S.Right(0)), false);
+
+  eq(S.is(S.EitherType($.String, $.Integer), S.Nothing), false);
+  eq(S.is(S.EitherType($.String, $.Integer), S.Just(0)), false);
+  eq(S.is(S.EitherType($.String, $.Integer), S.Left(0)), false);
+  eq(S.is(S.EitherType($.String, $.Integer), S.Right('')), false);
+  eq(S.is(S.EitherType($.String, $.Integer), S.Left('')), true);
+  eq(S.is(S.EitherType($.String, $.Integer), S.Right(0)), true);
+
+  var a = $.TypeVariable('a');
+
+  eq(S.is($.Array(a), []), true);
+  eq(S.is($.Array(a), [1, 2, 3]), true);
+  eq(S.is($.Array(a), ['foo', 'bar', 'baz']), true);
+  eq(S.is($.Array(a), ['foo', true, 42]), false);
+  eq(S.is($.Array(a), [Sum(1), Sum(2), Sum(3)]), false);
+
+  eq(S.create({checkTypes: true, env: []}).is($.Array(a), []), true);
+  eq(S.create({checkTypes: true, env: []}).is($.Array(a), [1, 2, 3]), false);
+  eq(S.create({checkTypes: true, env: [$.Number]}).is($.Array(a), [1, 2, 3]), true);
+  eq(S.create({checkTypes: true, env: [Sum.Type]}).is($.Array(a), [Sum(1), Sum(2), Sum(3)]), true);
 
 });
diff --git a/test/parseJson.js b/test/parseJson.js
index 2bbddaa..7bad5d9 100644
--- a/test/parseJson.js
+++ b/test/parseJson.js
@@ -13,11 +13,10 @@ test('parseJson', function() {
   eq(S.parseJson.length, 2);
   eq(S.parseJson.toString(), 'parseJson :: (Any -> Boolean) -> String -> Maybe a');
 
-  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']));
-
-  eq(S.parseJson($.test([], $.Array($.Number)), '[1,2]'), S.Just([1, 2]));
-  eq(S.parseJson($.test([], $.Array($.Number)), '[1,2,null]'), S.Nothing);
+  eq(S.parseJson(S.is($.Any), '[Invalid JSON]'), S.Nothing);
+  eq(S.parseJson(S.is($.Array($.Any)), '{"foo":"bar"}'), S.Nothing);
+  eq(S.parseJson(S.is($.Array($.Any)), '["foo","bar"]'), S.Just(['foo', 'bar']));
+  eq(S.parseJson(S.is($.Array($.Number)), '[1,2]'), S.Just([1, 2]));
+  eq(S.parseJson(S.is($.Array($.Number)), '[1,2,null]'), S.Nothing);
 
 });