Repository: plaid/sanctuary (js)

Issue:
remove instanceof checks
Sanctuary's run-time type checking currently relies on `instanceof` checks. This guarantees that if a value passes type checking it will support all the expected operations.

If a function requires an argument of type `Maybe a`, for example, a Maybe created by a *different* version of Sanctuary will fail type checking (with a rather confusing error message). For example:

    TypeError: ‘fromMaybe’ requires a value of type Maybe as its second argument; received Just(42)

The problem with this approach is that it rejects *valid* values too. Requiring the *same* version of Sanctuary from two different paths results in two incompatible Sanctuary modules existing in memory:

```javascript
const S1 = require('./path/to/sanctuary-0.7.0');
const S2 = require('./another/path/to/sanctuary-0.7.0');

S1.fromMaybe(NaN, S2.Just(42));
// ! TypeError: ‘fromMaybe’ requires a value of type Maybe as its second argument; received Just(42)
```

When might this occur?

  - In the browser, when dealing with iframes.

  - In Node, when using the [vm][1] module. @raine and I both discovered this while building REPLs.

  - In Node, when `package-a` depends on both `sanctuary` and `package-b`, and `package-b` also depends on `sanctuary`. If `package-a` applies a `package-b` function which returns a Maybe, the Maybe will not be recognized by the `sanctuary` module required by `package-a`.

    Note that npm performs dependency deduplication, so if `package-a` and `package-b` depend on the same version of `sanctuary`, both will use __package-a/node\_modules/sanctuary__. *Relying* on deduplication is undesirable: it's an optimization which should not effect a program's behaviour.

__The first and second scenarios also affect built-in types such as Array.__ [`R.is`][2] is also affected by this issue, as discussed in ramda/ramda#1140.

Clearly we must avoid `instanceof` checks. What might we do instead? For built-in types we have `Object.prototype.toString` and [`R.type`][3]:

```javascript
> Object.prototype.toString.call([])
'[object Array]'
> R.type([])
'Array'
```

For Sanctuary types (and other custom types) we need a different approach.

We can't use `.constructor.name` because a type may have multiple constructors: Maybe has `Nothing` and `Just`, for example. Furthermore, there's no guarantee that two constructors with the same name are equivalent.

I propose a special property named `'@@type'` or similar, with a [reverse-DNS][4]-ish value such as `'com.github.plaid.sanctuary/Maybe'`. The implementation would be straightforward:

```javascript
Maybe.prototype['@@type'] = 'com.github.plaid.sanctuary/Maybe';
```

We could then write a function to determine the unique identifier for a value's type:

```javascript
//  typeName :: a -> String
var typeName = function(x) {
  return x != null && R.type(x['@@type']) === 'String' ?
    x['@@type'] :
    'org.ecma-international.ecma-262/' + R.type(x);
};

typeName(Just(42));
// => 'com.github.plaid.sanctuary/Maybe'

typeName([1, 2, 3]);
// => 'org.ecma-international.ecma-262/Array'
```

Hat tip to @jethrolarson who has been giving this matter a lot of thought. I got the idea for a `'@@type'` property from his [xface][5] project (though xface uses the property for a slightly different purpose).

Solving the problems associated with `instanceof` checks is the most important issue I'd like to address in v0.8.0. I'd love to know what others think of this proposal. Counterproposals welcome!


[1]: https://nodejs.org/api/vm.html
[2]: http://ramdajs.com/docs/#is
[3]: http://ramdajs.com/docs/#type
[4]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation
[5]: https://gist.github.com/jethrolarson/5eaad6d7b22b231741c9


Pull request description:
do not rely on constructor identity
Closes #100

> `S.is`, `S.get`, `S.gets`, and `S.pluck` now ignore constructor identity, allowing them to operate on objects from other environments, such as iframes and sandboxes. Additionally, packages which export functions which take and/or return values of Sanctuary types no longer rely on npm package deduplication to satisfy the requirements of Sanctuary's run-time type checking.
>
> Since `instanceof` checks are no longer acceptable, `S.is` no longer respects subtyping.
>
> This [pull request] also introduces the following functions:
>
> ```haskell
> type      :: a -> String
> isNothing :: Maybe a -> Boolean
> isJust    :: Maybe a -> Boolean
> isLeft    :: Either a b -> Boolean
> isRight   :: Either a b -> Boolean
> ```


Patch:
diff --git a/index.js b/index.js
index 409b07d..5516be5 100644
--- a/index.js
+++ b/index.js
@@ -141,6 +141,9 @@
     return x != null && x['@@functional/placeholder'] === true;
   };
 
+  //  functionName :: TypeRep a -> String
+  var functionName = R.compose(R.nth(1), R.match(/^function (\w*)/), String);
+
   var formatters = {
     '{}': R.identity,
     '{card}': R.ifElse(R.lte(_, 10),
@@ -150,7 +153,7 @@
     '{ord}': R.nth(_, ['first', 'second', 'third']),
     '{quote}': function(s) { return '\u2018' + s + '\u2019'; },
     '{repr}': R.toString,
-    '{type}': R.pipe(String, R.match(/^function (\w*)/), R.nth(1))
+    '{type}': functionName
   };
 
   //  format :: String -> [*] -> String
@@ -170,23 +173,37 @@
   var b = {name: 'b'};
   var c = {name: 'c'};
 
+  //  _type :: a -> String
+  var _type = function(x) {
+    return x != null && R.type(x['@@type']) === 'String' ? x['@@type']
+                                                         : R.type(x);
+  };
+
+  //  _is :: (TypeRep a, b) -> Boolean
   var _is = function(type, x) {
-    if (x == null) return false;
-    switch (type) {
-      case Accessible:
-        return true;
-      case Integer:
-        return _is(Number, x) && Math.floor(x) === Number(x) &&
-               x >= MIN_SAFE_INTEGER && x <= MAX_SAFE_INTEGER;
-      case List:
-        return !_is(Function, x) && _is(Number, x.length) && x.length >= 0;
-      case RegexFlags:
-        return R.test(/^g?i?m?$/, x);
-      case TypeRep:
-        return _is(Function, x);
-      default:
-        return Object(x) instanceof type;
-    }
+    return (
+      x == null ?
+        false :
+      type === Accessible ?
+        true :
+      type === Integer ?
+        R.type(x) === 'Number' &&
+        Math.floor(x) === Number(x) &&
+        x >= MIN_SAFE_INTEGER &&
+        x <= MAX_SAFE_INTEGER :
+      type === List ?
+        R.type(x) !== 'Function' &&
+        R.type(x.length) === 'Number' &&
+        x.length >= 0 :
+      type === RegexFlags ?
+        R.test(/^g?i?m?$/, x) :
+      type === TypeRep ?
+        R.type(x) === 'Function' :
+      R.type(type.prototype['@@type']) === 'String' ?
+        x['@@type'] === type.prototype['@@type'] :
+      // else
+        R.type(x) === functionName(type)
+    );
   };
 
   var arity = function(n, f) {
@@ -198,7 +215,7 @@
     }
   };
 
-  //  curry :: (String, [TypeRep], [*], 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.
@@ -232,7 +249,7 @@
           for (idx = 0; idx < values.length; idx += 1) {
             var val = values[idx];
             if (types[idx] === type && !placeholder(val) &&
-                !(R.type(val) === R.type(arg) && val.type === arg.type)) {
+                _type(val) !== _type(arg)) {
               throw new TypeError(format(
                 '{quote} requires its {ord} and {ord} arguments ' +
                 'to be of the same type; {repr} and {repr} are not',
@@ -318,23 +335,38 @@
 
   //. ### Classify
 
+  //# type :: a -> String
+  //.
+  //. Takes a value, `x`, of any type and returns its type identifier. If
+  //. `x` has a `'@@type'` property whose value is a string, `x['@@type']`
+  //. is the type identifier. Otherwise, the type identifier is the result
+  //. of applying [`R.type`][R.type] to `x`.
+  //.
+  //. `'@@type'` properties should use the form `'<package-name>/<type-name>'`,
+  //. where `<package-name>` is the name of the npm package in which the type
+  //. is defined.
+  //.
+  //. ```javascript
+  //. > S.type(S.Just(42))
+  //. 'sanctuary/Maybe'
+  //.
+  //. > S.type([1, 2, 3])
+  //. 'Array'
+  //. ```
+  S.type = def('type', [a], _type);
+
   //# is :: TypeRep a -> b -> Boolean
   //.
   //. 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
-  //. and an Object (whereas [`R.is`][R.is] considers it a Number but not
-  //. an Object).
+  //. type; `false` otherwise. Subtyping is not respected.
   //.
   //. ```javascript
   //. > S.is(Number, 42)
   //. true
   //.
   //. > S.is(Object, 42)
-  //. true
+  //. false
   //.
   //. > S.is(String, 42)
   //. false
@@ -366,9 +398,7 @@
   //. > R.map(S.K(42), R.range(0, 5))
   //. [42, 42, 42, 42, 42]
   //. ```
-  var K = S.K = def('K', [a, b], function(x, y) {
-    return x;
-  });
+  S.K = def('K', [a, b], function(x, y) { return x; });
 
   //. ### Composition
 
@@ -484,6 +514,35 @@
     return Just(x);
   });
 
+  //# Maybe#@@type :: String
+  //.
+  //. Maybe type identifier, `'sanctuary/Maybe'`.
+  Maybe.prototype['@@type'] = 'sanctuary/Maybe';
+
+  //# Maybe#isNothing :: Boolean
+  //.
+  //. `true` if `this` is a Nothing; `false` if `this` is a Just.
+  //.
+  //. ```javascript
+  //. > S.Nothing().isNothing
+  //. true
+  //.
+  //. > S.Just(42).isNothing
+  //. false
+  //. ```
+
+  //# Maybe#isJust :: Boolean
+  //.
+  //. `true` if `this` is a Just; `false` if `this` is a Nothing.
+  //.
+  //. ```javascript
+  //. > S.Just(42).isJust
+  //. true
+  //.
+  //. > S.Nothing().isJust
+  //. false
+  //. ```
+
   //# Maybe#ap :: Maybe (a -> b) ~> Maybe a -> Maybe b
   //.
   //. Takes a value of type `Maybe a` and returns a Nothing unless `this`
@@ -678,12 +737,6 @@
   //. 'Just([1, 2, 3])'
   //. ```
 
-  //# 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.
-  Maybe.prototype.type = Maybe;
-
   //# Nothing :: -> Maybe a
   //.
   //. Returns a Nothing. Though this is a constructor function the `new`
@@ -700,6 +753,12 @@
   };
   extend(Nothing, Maybe);
 
+  //  Nothing#isNothing :: Boolean
+  Nothing.prototype.isNothing = true;
+
+  //  Nothing#isJust :: Boolean
+  Nothing.prototype.isJust = false;
+
   //  Nothing#ap :: Maybe (a -> b) ~> Maybe a -> Maybe b
   Nothing.prototype.ap = def('Nothing#ap', [Maybe], self);
 
@@ -708,14 +767,16 @@
 
   //  Nothing#concat :: Maybe a ~> Maybe a -> Maybe a
   Nothing.prototype.concat = def('Nothing#concat', [Maybe], function(maybe) {
-    if (maybe instanceof Just) {
+    if (maybe.isJust) {
       assertMethodExists('concat', maybe.value);
     }
     return maybe;
   });
 
   //  Nothing#equals :: Maybe a ~> b -> Boolean
-  Nothing.prototype.equals = def('Nothing#equals', [a], is(Nothing));
+  Nothing.prototype.equals = def('Nothing#equals', [a], function(x) {
+    return _type(x) === 'sanctuary/Maybe' && x.isNothing;
+  });
 
   //  Nothing#extend :: Maybe a ~> (Maybe a -> a) -> Maybe a
   Nothing.prototype.extend = def('Nothing#extend', [Function], self);
@@ -753,6 +814,12 @@
   };
   extend(Just, Maybe);
 
+  //  Just#isNothing :: Boolean
+  Just.prototype.isNothing = false;
+
+  //  Just#isJust :: Boolean
+  Just.prototype.isJust = true;
+
   //  Just#ap :: Maybe (a -> b) ~> Maybe a -> Maybe b
   Just.prototype.ap = def('Just#ap', [Maybe], function(maybe) {
     return maybe.map(this.value);
@@ -766,7 +833,7 @@
   //  Just#concat :: Maybe a ~> Maybe a -> Maybe a
   Just.prototype.concat = def('Just#concat', [Maybe], function(maybe) {
     assertMethodExists('concat', this.value);
-    if (maybe instanceof Just) {
+    if (maybe.isJust) {
       assertMethodExists('concat', maybe.value);
       return Just(this.value.concat(maybe.value));
     } else {
@@ -776,7 +843,8 @@
 
   //  Just#equals :: Maybe a ~> b -> Boolean
   Just.prototype.equals = def('Just#equals', [a], function(x) {
-    return x instanceof Just && R.eqProps('value', x, this);
+    return _type(x) === 'sanctuary/Maybe' && x.isJust &&
+           R.eqProps('value', x, this);
   });
 
   //  Just#extend :: Maybe a ~> (Maybe a -> a) -> Maybe a
@@ -800,6 +868,32 @@
   //  Just#toString :: Maybe a ~> String
   Just.prototype.toString = toString('Just');
 
+  //# isNothing :: Maybe a -> Boolean
+  //.
+  //. Returns `true` if the given Maybe is a Nothing; `false` if it is a Just.
+  //.
+  //. ```javascript
+  //. > S.isNothing(S.Nothing())
+  //. true
+  //.
+  //. > S.isNothing(S.Just(42))
+  //. false
+  //. ```
+  S.isNothing = def('isNothing', [Maybe], R.prop('isNothing'));
+
+  //# isJust :: Maybe a -> Boolean
+  //.
+  //. Returns `true` if the given Maybe is a Just; `false` if it is a Nothing.
+  //.
+  //. ```javascript
+  //. > S.isJust(S.Just(42))
+  //. true
+  //.
+  //. > S.isJust(S.Nothing())
+  //. false
+  //. ```
+  S.isJust = def('isJust', [Maybe], R.prop('isJust'));
+
   //# fromMaybe :: a -> Maybe a -> a
   //.
   //. Takes a default value and a Maybe, and returns the Maybe's value
@@ -814,7 +908,7 @@
   //. ```
   var fromMaybe = S.fromMaybe =
   def('fromMaybe', [a, Maybe], function(x, maybe) {
-    return maybe instanceof Just ? maybe.value : x;
+    return maybe.isJust ? maybe.value : x;
   });
 
   //# toMaybe :: a? -> Maybe a
@@ -929,6 +1023,35 @@
     return Right(x);
   });
 
+  //# Either#@@type :: String
+  //.
+  //. Either type identifier, `'sanctuary/Either'`.
+  Either.prototype['@@type'] = 'sanctuary/Either';
+
+  //# Either#isLeft :: Boolean
+  //.
+  //. `true` if `this` is a Left; `false` if `this` is a Right.
+  //.
+  //. ```javascript
+  //. > S.Left('Cannot divide by zero').isLeft
+  //. true
+  //.
+  //. > S.Right(42).isLeft
+  //. false
+  //. ```
+
+  //# Either#isRight :: Boolean
+  //.
+  //. `true` if `this` is a Right; `false` if `this` is a Left.
+  //.
+  //. ```javascript
+  //. > S.Right(42).isRight
+  //. true
+  //.
+  //. > S.Left('Cannot divide by zero').isRight
+  //. false
+  //. ```
+
   //# Either#ap :: Either a (b -> c) ~> Either a b -> Either a c
   //.
   //. Takes a value of type `Either a b` and returns a Left unless `this`
@@ -1081,13 +1204,6 @@
   //. 'Right([1, 2, 3])'
   //. ```
 
-  //# 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)`
-  //. are of the same type.
-  Either.prototype.type = Either;
-
   //# Left :: a -> Either a b
   //.
   //. Takes a value of any type and returns a Left with the given value.
@@ -1106,6 +1222,12 @@
   };
   extend(Left, Either);
 
+  //  Left#isLeft :: Boolean
+  Left.prototype.isLeft = true;
+
+  //  Left#isRight :: Boolean
+  Left.prototype.isRight = false;
+
   //  Left#ap :: Either a (b -> c) ~> Either a b -> Either a c
   Left.prototype.ap = def('Left#ap', [Either], self);
 
@@ -1116,12 +1238,13 @@
   Left.prototype.concat = def('Left#concat', [Either], function(either) {
     assertMethodExists('concat', this.value);
     assertMethodExists('concat', either.value);
-    return is(Left, either) ? Left(this.value.concat(either.value)) : either;
+    return either.isLeft ? Left(this.value.concat(either.value)) : either;
   });
 
   //  Left#equals :: Either a b ~> c -> Boolean
   Left.prototype.equals = def('Left#equals', [a], function(x) {
-    return x instanceof Left && R.eqProps('value', x, this);
+    return _type(x) === 'sanctuary/Either' && x.isLeft &&
+           R.eqProps('value', x, this);
   });
 
   //  Left#extend :: Either a b ~> (Either a b -> b) -> Either a b
@@ -1154,6 +1277,12 @@
   };
   extend(Right, Either);
 
+  //  Right#isLeft :: Boolean
+  Right.prototype.isLeft = false;
+
+  //  Right#isRight :: Boolean
+  Right.prototype.isRight = true;
+
   //  Right#ap :: Either a (b -> c) ~> Either a b -> Either a c
   Right.prototype.ap = def('Right#ap', [Either], function(either) {
     return either.map(this.value);
@@ -1168,12 +1297,13 @@
   Right.prototype.concat = def('Right#concat', [Either], function(either) {
     assertMethodExists('concat', this.value);
     assertMethodExists('concat', either.value);
-    return is(Right, either) ? Right(this.value.concat(either.value)) : this;
+    return either.isRight ? Right(this.value.concat(either.value)) : this;
   });
 
   //  Right#equals :: Either a b ~> c -> Boolean
   Right.prototype.equals = def('Right#equals', [a], function(x) {
-    return x instanceof Right && R.eqProps('value', x, this);
+    return _type(x) === 'sanctuary/Either' && x.isRight &&
+           R.eqProps('value', x, this);
   });
 
   //  Right#extend :: Either a b ~> (Either a b -> b) -> Either a b
@@ -1192,6 +1322,32 @@
   //  Right#toString :: Either a b ~> String
   Right.prototype.toString = toString('Right');
 
+  //# isLeft :: Either a b -> Boolean
+  //.
+  //. Returns `true` if the given Either is a Left; `false` if it is a Right.
+  //.
+  //. ```javascript
+  //. > S.isLeft(S.Left('Cannot divide by zero'))
+  //. true
+  //.
+  //. > S.isLeft(S.Right(42))
+  //. false
+  //. ```
+  S.isLeft = def('isLeft', [Either], R.prop('isLeft'));
+
+  //# isRight :: Either a b -> Boolean
+  //.
+  //. Returns `true` if the given Either is a Right; `false` if it is a Left.
+  //.
+  //. ```javascript
+  //. > S.isRight(S.Right(42))
+  //. true
+  //.
+  //. > S.isRight(S.Left('Cannot divide by zero'))
+  //. false
+  //. ```
+  S.isRight = def('isRight', [Either], R.prop('isRight'));
+
   //# either :: (a -> c) -> (b -> c) -> Either a b -> c
   //.
   //. Takes two functions and an Either, and returns the result of
@@ -1208,7 +1364,7 @@
   //. ```
   S.either =
   def('either', [Function, Function, Either], function(l, r, either) {
-    return either instanceof Left ? l(either.value) : r(either.value);
+    return either.isLeft ? l(either.value) : r(either.value);
   });
 
   //# encaseEither :: (Error -> a) -> (* -> b) -> (* -> Either a b)
@@ -1258,24 +1414,28 @@
   //. Right(42)
   //. ```
   S.maybeToEither = def('maybeToEither', [a, Maybe], function(x, maybe) {
-    return maybe instanceof Nothing ? Left(x) : Right(maybe.value);
+    return maybe.isNothing ? Left(x) : Right(maybe.value);
   });
 
   //. ### Control
 
   //  toBoolean :: * -> Boolean
-  var toBoolean = R.cond([
-    [is(Array),   R.complement(R.isEmpty)],
-    [is(Boolean), I],
-    [R.T,         invoke('toBoolean', [])]
-  ]);
+  var toBoolean = function(x) {
+    switch (R.type(x)) {
+      case 'Array':     return x.length > 0;
+      case 'Boolean':   return x.valueOf();
+      default:          return invoke('toBoolean', [], x);
+    }
+  };
 
   //  empty :: a -> a
-  var empty = R.cond([
-    [is(Array),   function() { return []; }],
-    [is(Boolean), K(false)],
-    [R.T,         invoke('empty', [])]
-  ]);
+  var empty = function(x) {
+    switch (R.type(x)) {
+      case 'Array':     return [];
+      case 'Boolean':   return false;
+      default:          return invoke('empty', [], x);
+    }
+  };
 
   //# and :: a -> a -> a
   //.
@@ -1653,7 +1813,7 @@
   S.unfoldr = def('unfoldr', [Function, a], function(f, x) {
     var result = [];
     var m = f(x);
-    while (is(Just, m)) {
+    while (m.isJust) {
       result.push(m.value[0]);
       m = f(m.value[1]);
     }
@@ -1941,10 +2101,9 @@
 //. [Monad]:        https://github.com/fantasyland/fantasy-land#monad
 //. [Monoid]:       https://github.com/fantasyland/fantasy-land#monoid
 //. [R.equals]:     http://ramdajs.com/docs/#equals
-//. [R.is]:         http://ramdajs.com/docs/#is
 //. [R.map]:        http://ramdajs.com/docs/#map
+//. [R.type]:       http://ramdajs.com/docs/#type
 //. [Ramda]:        http://ramdajs.com/
 //. [RegExp]:       https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
 //. [Semigroup]:    https://github.com/fantasyland/fantasy-land#semigroup
 //. [parseInt]:     https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt
-//. [primitives]:   https://developer.mozilla.org/en-US/docs/Glossary/Primitive


Test patch:
diff --git a/test/index.js b/test/index.js
index 8faea06..21a9e2c 100644
--- a/test/index.js
+++ b/test/index.js
@@ -3,6 +3,7 @@
 /* global describe, it */
 
 var assert = require('assert');
+var vm = require('vm');
 
 var R = require('ramda');
 
@@ -10,6 +11,7 @@ var S = require('..');
 
 
 var eq = function(actual, expected) {
+  assert.strictEqual(arguments.length, 2);
   assert.strictEqual(R.toString(actual), R.toString(expected));
 };
 
@@ -98,6 +100,54 @@ describe('invariants', function() {
 
 describe('classify', function() {
 
+  describe('type', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.type, 'function');
+      eq(S.type.length, 1);
+    });
+
+    it('operates on values of built-in types', function() {
+      eq(S.type((function() { return arguments; }())),
+                                      'Arguments');
+      eq(S.type([]),                  'Array');
+      eq(S.type(false),               'Boolean');
+      eq(S.type(new Date(0)),         'Date');
+      eq(S.type(new TypeError()),     'Error');
+      eq(S.type(function() {}),       'Function');
+      eq(S.type(null),                'Null');
+      eq(S.type(0),                   'Number');
+      eq(S.type(/(?:)/),              'RegExp');
+      eq(S.type(''),                  'String');
+      eq(S.type(undefined),           'Undefined');
+      // jshint -W053
+      eq(S.type(new Boolean(false)),  'Boolean');
+      eq(S.type(new Number(0)),       'Number');
+      eq(S.type(new String('')),      'String');
+      // jshint +W053
+    });
+
+    it('operates on values of Sanctuary types', function() {
+      eq(S.type(S.Left(42)),  'sanctuary/Either');
+      eq(S.type(S.Right(42)), 'sanctuary/Either');
+      eq(S.type(S.Nothing()), 'sanctuary/Maybe');
+      eq(S.type(S.Just(42)),  'sanctuary/Maybe');
+    });
+
+    it('operates on values of user-defined types', function() {
+      function Gizmo() {}
+      Gizmo.prototype['@@type'] = 'gadgets/Gizmo';
+
+      eq(S.type(new Gizmo()), 'gadgets/Gizmo');
+      eq(S.type({'@@type': 'foobar/FooBar'}), 'foobar/FooBar');
+    });
+
+    it('does not rely on constructor identity', function() {
+      eq(S.type(vm.runInNewContext('[1, 2, 3]')), 'Array');
+    });
+
+  });
+
   describe('is', function() {
 
     it('is a binary function', function() {
@@ -113,14 +163,17 @@ describe('classify', function() {
     });
 
     it('works for built-in type', function() {
-      // jshint -W053
       eq(S.is(Array,    []),                  true);
-      eq(S.is(Boolean,  new Boolean(false)),  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,   new Number(0)),       true);
+      eq(S.is(Number,   0),                   true);
       eq(S.is(Object,   {}),                  true);
       eq(S.is(RegExp,   /(?:)/),              true);
+      eq(S.is(String,   ''),                  true);
+      // jshint -W053
+      eq(S.is(Boolean,  new Boolean(false)),  true);
+      eq(S.is(Number,   new Number(0)),       true);
       eq(S.is(String,   new String('')),      true);
       // jshint +W053
       eq(S.is(Array,    null),                false);
@@ -128,35 +181,21 @@ describe('classify', function() {
       eq(S.is(Array,    {}),                  false);
     });
 
-    it('promotes primitive to its object equivalent', function() {
-      eq(S.is(Boolean,  false),               true);
-      eq(S.is(Number,   0),                   true);
-      eq(S.is(String,   ''),                  true);
-    });
-
-    it('respects inheritance', function() {
-      eq(S.is(Object,   []),                  true);
-      eq(S.is(Object,   false),               true);
-      eq(S.is(Object,   new Date(0)),         true);
-      eq(S.is(Object,   function() {}),       true);
-      eq(S.is(Object,   0),                   true);
-      eq(S.is(Object,   /(?:)/),              true);
-      eq(S.is(Object,   ''),                  true);
-      eq(S.is(Error,    new TypeError()),     true);
-    });
-
     it('works for user-defined type', function() {
+      function FooBar() {}
+      FooBar.prototype['@@type'] = 'foobar/FooBar';
       function Foo() {}
+      Foo.prototype = new FooBar();
       function Bar() {}
-      Bar.prototype = new Foo();
+      Bar.prototype = new FooBar();
 
-      var foo = new Foo();
-      var bar = new Bar();
+      eq(S.is(FooBar, new Foo()), true);
+      eq(S.is(FooBar, new Bar()), true);
+    });
 
-      eq(S.is(Foo, foo), true);
-      eq(S.is(Bar, bar), true);
-      eq(S.is(Foo, bar), true);
-      eq(S.is(Bar, foo), false);
+    it('does not rely on constructor identity', function() {
+      eq(S.is(Array, vm.runInNewContext('[1, 2, 3]')), true);
+      eq(S.is(vm.runInNewContext('Array'), [1, 2, 3]), true);
     });
 
     it('is curried', function() {
@@ -331,9 +370,17 @@ describe('maybe', function() {
       assert(S.Nothing() instanceof S.Nothing);
     });
 
+    it('is a Nothing', function() {
+      eq(S.Nothing().isNothing, true);
+    });
+
+    it('is not a Just', function() {
+      eq(S.Nothing().isJust, false);
+    });
+
     it('is a subtype of Maybe', function() {
       assert(S.Nothing() instanceof S.Maybe);
-      eq(S.Nothing().type, S.Maybe);
+      eq(S.Nothing()['@@type'], 'sanctuary/Maybe');
     });
 
     it('provides an "ap" method', function() {
@@ -548,9 +595,17 @@ describe('maybe', function() {
       assert(S.Just(42) instanceof S.Just);
     });
 
+    it('is a Just', function() {
+      eq(S.Just(42).isJust, true);
+    });
+
+    it('is not a Nothing', function() {
+      eq(S.Just(42).isNothing, false);
+    });
+
     it('is a subtype of Maybe', function() {
       assert(S.Just(42) instanceof S.Maybe);
-      eq(S.Just(42).type, S.Maybe);
+      eq(S.Just(42)['@@type'], 'sanctuary/Maybe');
     });
 
     it('provides an "ap" method', function() {
@@ -771,6 +826,54 @@ describe('maybe', function() {
 
   });
 
+  describe('isNothing', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.isNothing, 'function');
+      eq(S.isNothing.length, 1);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.isNothing([1, 2, 3]); },
+                    errorEq(TypeError,
+                            '‘isNothing’ requires a value of type Maybe ' +
+                            'as its first argument; received [1, 2, 3]'));
+    });
+
+    it('returns true when applied to a Nothing', function() {
+      eq(S.isNothing(S.Nothing()), true);
+    });
+
+    it('returns false when applied to a Just', function() {
+      eq(S.isNothing(S.Just(42)), false);
+    });
+
+  });
+
+  describe('isJust', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.isJust, 'function');
+      eq(S.isJust.length, 1);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.isJust([1, 2, 3]); },
+                    errorEq(TypeError,
+                            '‘isJust’ requires a value of type Maybe ' +
+                            'as its first argument; received [1, 2, 3]'));
+    });
+
+    it('returns true when applied to a Just', function() {
+      eq(S.isJust(S.Just(42)), true);
+    });
+
+    it('returns false when applied to a Nothing', function() {
+      eq(S.isJust(S.Nothing()), false);
+    });
+
+  });
+
   describe('fromMaybe', function() {
 
     it('is a binary function', function() {
@@ -979,9 +1082,17 @@ describe('either', function() {
       assert(S.Left(42) instanceof S.Left);
     });
 
+    it('is a Left', function() {
+      eq(S.Left(42).isLeft, true);
+    });
+
+    it('is not a Right', function() {
+      eq(S.Left(42).isRight, false);
+    });
+
     it('is a subtype of Either', function() {
       assert(S.Left(42) instanceof S.Either);
-      eq(S.Left(42).type, S.Either);
+      eq(S.Left(42)['@@type'], 'sanctuary/Either');
     });
 
     it('provides an "ap" method', function() {
@@ -1186,9 +1297,17 @@ describe('either', function() {
       assert(S.Right(42) instanceof S.Right);
     });
 
+    it('is a Right', function() {
+      eq(S.Right(42).isRight, true);
+    });
+
+    it('is not a Left', function() {
+      eq(S.Right(42).isLeft, false);
+    });
+
     it('is a subtype of Either', function() {
       assert(S.Right(42) instanceof S.Either);
-      eq(S.Right(42).type, S.Either);
+      eq(S.Right(42)['@@type'], 'sanctuary/Either');
     });
 
     it('provides an "ap" method', function() {
@@ -1378,6 +1497,54 @@ describe('either', function() {
 
   });
 
+  describe('isLeft', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.isLeft, 'function');
+      eq(S.isLeft.length, 1);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.isLeft([1, 2, 3]); },
+                    errorEq(TypeError,
+                            '‘isLeft’ requires a value of type Either ' +
+                            'as its first argument; received [1, 2, 3]'));
+    });
+
+    it('returns true when applied to a Left', function() {
+      eq(S.isLeft(S.Left(42)), true);
+    });
+
+    it('returns false when applied to a Right', function() {
+      eq(S.isLeft(S.Right(42)), false);
+    });
+
+  });
+
+  describe('isRight', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.isRight, 'function');
+      eq(S.isRight.length, 1);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.isRight([1, 2, 3]); },
+                    errorEq(TypeError,
+                            '‘isRight’ requires a value of type Either ' +
+                            'as its first argument; received [1, 2, 3]'));
+    });
+
+    it('returns true when applied to a Right', function() {
+      eq(S.isRight(S.Right(42)), true);
+    });
+
+    it('returns false when applied to a Left', function() {
+      eq(S.isRight(S.Left(42)), false);
+    });
+
+  });
+
   describe('either', function() {
 
     it('is a ternary function', function() {
@@ -1592,20 +1759,11 @@ describe('control', function() {
     });
 
     it('throws if applied to values of different types', function() {
-      function Foo() {}
-      Foo.prototype.type = Foo;
-      var foo = new Foo();
-
       assert.throws(function() { S.and([], false); },
                     errorEq(TypeError,
                             '‘and’ requires its first and second arguments ' +
                             'to be of the same type; [] and false are not'));
 
-      assert.throws(function() { S.and(false, foo); },
-                    errorEq(TypeError,
-                            '‘and’ requires its first and second arguments ' +
-                            'to be of the same type; false and {} are not'));
-
       assert.throws(function() { S.and(R.__, false)([]); },
                     errorEq(TypeError,
                             '‘and’ requires its first and second arguments ' +
@@ -1661,20 +1819,11 @@ describe('control', function() {
     });
 
     it('throws if applied to values of different types', function() {
-      function Foo() {}
-      Foo.prototype.type = Foo;
-      var foo = new Foo();
-
       assert.throws(function() { S.or([], false); },
                     errorEq(TypeError,
                             '‘or’ requires its first and second arguments ' +
                             'to be of the same type; [] and false are not'));
 
-      assert.throws(function() { S.or(false, foo); },
-                    errorEq(TypeError,
-                            '‘or’ requires its first and second arguments ' +
-                            'to be of the same type; false and {} are not'));
-
       assert.throws(function() { S.or(R.__, false)([]); },
                     errorEq(TypeError,
                             '‘or’ requires its first and second arguments ' +
@@ -1741,20 +1890,11 @@ describe('control', function() {
     });
 
     it('throws if applied to values of different types', function() {
-      function Foo() {}
-      Foo.prototype.type = Foo;
-      var foo = new Foo();
-
       assert.throws(function() { S.xor([], false); },
                     errorEq(TypeError,
                             '‘xor’ requires its first and second arguments ' +
                             'to be of the same type; [] and false are not'));
 
-      assert.throws(function() { S.xor(false, foo); },
-                    errorEq(TypeError,
-                            '‘xor’ requires its first and second arguments ' +
-                            'to be of the same type; false and {} are not'));
-
       assert.throws(function() { S.xor(R.__, false)([]); },
                     errorEq(TypeError,
                             '‘xor’ requires its first and second arguments ' +
@@ -2356,8 +2496,11 @@ describe('list', function() {
       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('does not rely on constructor identity', function() {
+      eq(S.pluck(Array, 'x', [{x: vm.runInNewContext('[0]')}]), [S.Just([0])]);
+      eq(S.pluck(vm.runInNewContext('Array'), 'x', [{x: [0]}]), [S.Just([0])]);
     });
 
     it('is curried', function() {
@@ -2427,7 +2570,11 @@ describe('object', function() {
       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('does not rely on constructor identity', function() {
+      eq(S.get(RegExp, 'x', {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
+      eq(S.get(vm.runInNewContext('RegExp'), 'x', {x: /.*/}), S.Just(/.*/));
     });
 
     it('is curried', function() {
@@ -2472,6 +2619,11 @@ describe('object', function() {
       eq(S.gets(Object, [], obj), S.Just({x: {z: 0}, y: 42}));
     });
 
+    it('does not rely on constructor identity', function() {
+      eq(S.gets(RegExp, ['x'], {x: vm.runInNewContext('/.*/')}), S.Just(/.*/));
+      eq(S.gets(vm.runInNewContext('RegExp'), ['x'], {x: /.*/}), S.Just(/.*/));
+    });
+
     it('is curried', function() {
       eq(S.gets(Number).length, 2);
       eq(S.gets(Number)(['x']).length, 1);