Repository: plaid/sanctuary (js)

Issue:
integer type
This may or may not be useful:

```javascript
> S.Int(7).add(S.Int(2))
Int(9)
> S.Int(7).divideBy(S.Int(2))
Int(3)
> S.Int(7).valueOf()
7
```

An integer type would allow us to specify types more precisely. [`S.at`][1], for example, could have type `Int -> [a] -> Maybe a`.


[1]: https://github.com/plaid/sanctuary#at


Pull request:
add Integer pseudotype
Closes #45

This pull request, like #62, makes the types of several functions more restrictive by requiring integer indices, radices, and slice lengths. It differs from #62 in two significant ways:

  - it does not add any functions; and
  - it introduces a pseudotype for (-2^53 .. 2^53) rather than [-2^31 .. 2^31).<sup>†</sup>

When I opened #45, I had two goals in mind: integer-specific functions, and stricter types for existing functions. My first solution was an Int container type. Others rightly pointed out that `S.at(S.Int(3))` is much less convenient than `S.at(3)`. Interacting with boxed numbers returned by functions such as `S.indexOf` would have been similarly awkward.

In #62 I proposed an Int pseudotype rather than an Int container type, obviating the need for boxing and unboxing. I haven't merged that pull request because I'm not happy with the fact that

  - <code>and :: Int -> Int -> Int</code>,
  - <code>or&nbsp; :: Int -> Int -> Int</code>, and
  - <code>xor :: Int -> Int -> Int</code>

share names with existing Sanctuary functions, necessitating a namespace for Int-specific functions. This would be inconsistent given that functions such as [`S.either`][1] and [`S.fromMaybe`][2] are exported at the top level of the module.

I suggest we create a separate package for the functions in #62:

```javascript
const Int = require('sanctuary-int');

Int.quot(42, 5);  // => 8
```

I'm particularly interested in hearing from @CrossEye and @paldepind, since your comments in the earlier discussion greatly influenced my thoughts on this matter.

---

† Since this pull request does not introduce bitwise functions, there's no reason to limit the range to that of signed 32-bit integers.


[1]: https://github.com/plaid/sanctuary#either
[2]: https://github.com/plaid/sanctuary#fromMaybe


Patch:
diff --git a/index.js b/index.js
index fb5f400..066a38d 100644
--- a/index.js
+++ b/index.js
@@ -69,6 +69,13 @@
 //. Sanctuary uses the Accessible pseudotype to represent the set of values
 //. which support property access.
 //.
+//. ### Integer pseudotype
+//.
+//. The Integer pseudotype represents integers in the range (-2^53 .. 2^53).
+//. It is a pseudotype because each Integer is represented by a Number value.
+//. Sanctuary's run-time type checking asserts that a valid Number value is
+//. provided wherever an Integer value is required.
+//.
 //. ### Type representatives
 //.
 //. What is the type of `Number`? One answer is `a -> Number`, since it's a
@@ -101,6 +108,9 @@
     this.sanctuary = S;
   }
 
+  var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;
+  var MIN_SAFE_INTEGER = -MAX_SAFE_INTEGER;
+
   var _ = R.__;
 
   //  placeholder :: a -> Boolean
@@ -125,13 +135,25 @@
   });
 
   var Accessible = /* istanbul ignore next */ function Accessible() {};
+  var Integer = /* istanbul ignore next */ function Integer() {};
   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 && (type === Accessible || Object(x) instanceof type);
+    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 TypeRep:
+        return _is(Function, x);
+      default:
+        return Object(x) instanceof type;
+    }
   };
 
   var arity = function(n, f) {
@@ -185,7 +207,7 @@
               [paramIndex, name]
             ));
           }
-        } else if (!_is(type === TypeRep ? Function : type, arg)) {
+        } else if (!_is(type, arg)) {
           throw new TypeError(format(
             '{quote} requires a value of type {type} as its {ord} argument; ' +
             'received {repr}',
@@ -1054,7 +1076,7 @@
 
   //. ### List
 
-  //# slice :: Number -> Number -> [a] -> Maybe [a]
+  //# slice :: Integer -> Integer -> [a] -> Maybe [a]
   //.
   //. Returns Just a list containing the elements from the supplied list
   //. from a beginning index (inclusive) to an end index (exclusive).
@@ -1083,7 +1105,7 @@
   //. Just("nana")
   //. ```
   var slice = S.slice =
-  def('slice', [Number, Number, Accessible], function(start, end, xs) {
+  def('slice', [Integer, Integer, Accessible], function(start, end, xs) {
     var len = xs.length;
     var startIdx = negativeZero(start) ? len : start < 0 ? start + len : start;
     var endIdx = negativeZero(end) ? len : end < 0 ? end + len : end;
@@ -1093,7 +1115,7 @@
       Nothing();
   });
 
-  //# at :: Number -> [a] -> Maybe a
+  //# at :: Integer -> [a] -> Maybe a
   //.
   //. Takes an index and a list and returns Just the element of the list at
   //. the index if the index is within the list's bounds; Nothing otherwise.
@@ -1109,7 +1131,7 @@
   //. > S.at(-2, ['a', 'b', 'c', 'd', 'e'])
   //. Just("d")
   //. ```
-  var at = S.at = def('at', [Number, Accessible], function(n, xs) {
+  var at = S.at = def('at', [Integer, Accessible], function(n, xs) {
     return R.map(R.head, slice(n, n === -1 ? -0 : n + 1, xs));
   });
 
@@ -1171,7 +1193,7 @@
   //. ```
   S.init = def('init', [Accessible], slice(0, -1));
 
-  //# take :: Number -> [a] -> Maybe [a]
+  //# take :: Integer -> [a] -> Maybe [a]
   //.
   //. Returns Just the first N elements of the given collection if N is
   //. greater than or equal to zero and less than or equal to the length
@@ -1188,11 +1210,11 @@
   //. > S.take(4, ['a', 'b', 'c'])
   //. Nothing()
   //. ```
-  S.take = def('take', [Number, Accessible], function(n, xs) {
+  S.take = def('take', [Integer, Accessible], function(n, xs) {
     return n < 0 || negativeZero(n) ? Nothing() : slice(0, n, xs);
   });
 
-  //# takeLast :: Number -> [a] -> Maybe [a]
+  //# takeLast :: Integer -> [a] -> Maybe [a]
   //.
   //. Returns Just the last N elements of the given collection if N is
   //. greater than or equal to zero and less than or equal to the length
@@ -1209,11 +1231,11 @@
   //. > S.takeLast(4, ['a', 'b', 'c'])
   //. Nothing()
   //. ```
-  S.takeLast = def('takeLast', [Number, Accessible], function(n, xs) {
+  S.takeLast = def('takeLast', [Integer, Accessible], function(n, xs) {
     return n < 0 || negativeZero(n) ? Nothing() : slice(-n, -0, xs);
   });
 
-  //# drop :: Number -> [a] -> Maybe [a]
+  //# drop :: Integer -> [a] -> Maybe [a]
   //.
   //. Returns Just all but the first N elements of the given collection
   //. if N is greater than or equal to zero and less than or equal to the
@@ -1230,11 +1252,11 @@
   //. > S.drop(4, 'abc')
   //. Nothing()
   //. ```
-  S.drop = def('drop', [Number, Accessible], function(n, xs) {
+  S.drop = def('drop', [Integer, Accessible], function(n, xs) {
     return n < 0 || negativeZero(n) ? Nothing() : slice(n, -0, xs);
   });
 
-  //# dropLast :: Number -> [a] -> Maybe [a]
+  //# dropLast :: Integer -> [a] -> Maybe [a]
   //.
   //. Returns Just all but the last N elements of the given collection
   //. if N is greater than or equal to zero and less than or equal to the
@@ -1251,7 +1273,7 @@
   //. > S.dropLast(4, 'abc')
   //. Nothing()
   //. ```
-  S.dropLast = def('dropLast', [Number, Accessible], function(n, xs) {
+  S.dropLast = def('dropLast', [Integer, Accessible], function(n, xs) {
     return n < 0 || negativeZero(n) ? Nothing() : slice(0, -n, xs);
   });
 
@@ -1282,14 +1304,14 @@
                R.pipe(R[name], Just, R.filter(R.gte(_, 0))));
   };
 
-  //# indexOf :: a -> [a] -> Maybe Number
+  //# indexOf :: a -> [a] -> Maybe Integer
   //.
   //. Takes a value of any type and a list, and returns Just the index
   //. of the first occurrence of the value in the list, if applicable;
   //. Nothing otherwise.
   //.
   //. Dispatches to its second argument's `indexOf` method if present.
-  //. As a result, `String -> String -> Maybe Number` is an alternative
+  //. As a result, `String -> String -> Maybe Integer` is an alternative
   //. type signature.
   //.
   //. ```javascript
@@ -1307,14 +1329,14 @@
   //. ```
   S.indexOf = sanctifyIndexOf('indexOf');
 
-  //# lastIndexOf :: a -> [a] -> Maybe Number
+  //# lastIndexOf :: a -> [a] -> Maybe Integer
   //.
   //. Takes a value of any type and a list, and returns Just the index
   //. of the last occurrence of the value in the list, if applicable;
   //. Nothing otherwise.
   //.
   //. Dispatches to its second argument's `lastIndexOf` method if present.
-  //. As a result, `String -> String -> Maybe Number` is an alternative
+  //. As a result, `String -> String -> Maybe Integer` is an alternative
   //. type signature.
   //.
   //. ```javascript
@@ -1441,7 +1463,7 @@
     return n === n ? Just(n) : Nothing();
   });
 
-  //# parseInt :: Number -> String -> Maybe Number
+  //# parseInt :: Integer -> String -> Maybe Integer
   //.
   //. Takes a radix (an integer between 2 and 36 inclusive) and a string,
   //. and returns Just the number represented by the string if it does in
@@ -1462,7 +1484,7 @@
   //. > S.parseInt(16, '0xGG')
   //. Nothing()
   //. ```
-  S.parseInt = def('parseInt', [Number, String], function(radix, s) {
+  S.parseInt = def('parseInt', [Integer, String], function(radix, s) {
     if (radix < 2 || radix > 36) {
       throw new RangeError('Radix not in [2 .. 36]');
     }
@@ -1477,7 +1499,8 @@
                       R.all(R.pipe(R.toUpper,
                                    R.indexOf(_, charset),
                                    R.gte(_, 0))))),
-      R.map(R.partialRight(parseInt, radix))
+      R.map(R.partialRight(parseInt, radix)),
+      R.filter(is(Integer))
     )(s);
   });
 


Test patch:
diff --git a/test/index.js b/test/index.js
index c3ebef9..dcd8037 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1397,10 +1397,10 @@ describe('list', function() {
     });
 
     it('type checks its arguments', function() {
-      assert.throws(function() { S.at([1, 2, 3]); },
+      assert.throws(function() { S.at(0.5); },
                     errorEq(TypeError,
-                            '‘at’ requires a value of type Number ' +
-                            'as its first argument; received [1, 2, 3]'));
+                            '‘at’ requires a value of type Integer ' +
+                            'as its first argument; received 0.5'));
 
       assert.throws(function() { S.at(0, null); },
                     errorEq(TypeError,
@@ -1437,14 +1437,14 @@ describe('list', function() {
     });
 
     it('type checks its arguments', function() {
-      assert.throws(function() { S.slice([1, 2, 3]); },
+      assert.throws(function() { S.slice(0.5); },
                     errorEq(TypeError,
-                            '‘slice’ requires a value of type Number ' +
-                            'as its first argument; received [1, 2, 3]'));
+                            '‘slice’ requires a value of type Integer ' +
+                            'as its first argument; received 0.5'));
 
       assert.throws(function() { S.slice(0, [1, 2, 3]); },
                     errorEq(TypeError,
-                            '‘slice’ requires a value of type Number ' +
+                            '‘slice’ requires a value of type Integer ' +
                             'as its second argument; received [1, 2, 3]'));
 
       assert.throws(function() { S.slice(0, 0, null); },
@@ -1624,10 +1624,10 @@ describe('list', function() {
     });
 
     it('type checks its arguments', function() {
-      assert.throws(function() { S.take([1, 2, 3]); },
+      assert.throws(function() { S.take(0.5); },
                     errorEq(TypeError,
-                            '‘take’ requires a value of type Number ' +
-                            'as its first argument; received [1, 2, 3]'));
+                            '‘take’ requires a value of type Integer ' +
+                            'as its first argument; received 0.5'));
 
       assert.throws(function() { S.take(0, null); },
                     errorEq(TypeError,
@@ -1680,10 +1680,10 @@ describe('list', function() {
     });
 
     it('type checks its arguments', function() {
-      assert.throws(function() { S.takeLast([1, 2, 3]); },
+      assert.throws(function() { S.takeLast(0.5); },
                     errorEq(TypeError,
-                            '‘takeLast’ requires a value of type Number ' +
-                            'as its first argument; received [1, 2, 3]'));
+                            '‘takeLast’ requires a value of type Integer ' +
+                            'as its first argument; received 0.5'));
 
       assert.throws(function() { S.takeLast(0, null); },
                     errorEq(TypeError,
@@ -1729,10 +1729,10 @@ describe('list', function() {
     });
 
     it('type checks its arguments', function() {
-      assert.throws(function() { S.drop([1, 2, 3]); },
+      assert.throws(function() { S.drop(0.5); },
                     errorEq(TypeError,
-                            '‘drop’ requires a value of type Number ' +
-                            'as its first argument; received [1, 2, 3]'));
+                            '‘drop’ requires a value of type Integer ' +
+                            'as its first argument; received 0.5'));
 
       assert.throws(function() { S.drop(0, null); },
                     errorEq(TypeError,
@@ -1785,10 +1785,10 @@ describe('list', function() {
     });
 
     it('type checks its arguments', function() {
-      assert.throws(function() { S.dropLast([1, 2, 3]); },
+      assert.throws(function() { S.dropLast(0.5); },
                     errorEq(TypeError,
-                            '‘dropLast’ requires a value of type Number ' +
-                            'as its first argument; received [1, 2, 3]'));
+                            '‘dropLast’ requires a value of type Integer ' +
+                            'as its first argument; received 0.5'));
 
       assert.throws(function() { S.dropLast(0, null); },
                     errorEq(TypeError,
@@ -2111,6 +2111,8 @@ describe('parse', function() {
 
     it('returns a Maybe', function() {
       eq(S.parseFloat('12.34'), S.Just(12.34));
+      eq(S.parseFloat('Infinity'), S.Just(Infinity));
+      eq(S.parseFloat('-Infinity'), S.Just(-Infinity));
       eq(S.parseFloat('xxx'), S.Nothing());
     });
 
@@ -2124,10 +2126,10 @@ describe('parse', function() {
     });
 
     it('type checks its arguments', function() {
-      assert.throws(function() { S.parseInt('10'); },
+      assert.throws(function() { S.parseInt(0.5); },
                     errorEq(TypeError,
-                            '‘parseInt’ requires a value of type Number ' +
-                            'as its first argument; received "10"'));
+                            '‘parseInt’ requires a value of type Integer ' +
+                            'as its first argument; received 0.5'));
 
       assert.throws(function() { S.parseInt(10, 42); },
                     errorEq(TypeError,
@@ -2248,9 +2250,19 @@ describe('parse', function() {
     });
 
     it('returns a Nothing if one or more characters are invalid', function() {
+      eq(S.parseInt(10, '12.34'), S.Nothing());  // parseInt('12.34', 10) == 12
       eq(S.parseInt(16, 'alice'), S.Nothing());  // parseInt('alice', 16) == 10
     });
 
+    it('restricts to exactly representable range (-2^53 .. 2^53)', function() {
+      eq(S.parseInt(10,  '9007199254740991'), S.Just(9007199254740991));
+      eq(S.parseInt(10, '-9007199254740991'), S.Just(-9007199254740991));
+      eq(S.parseInt(10,  '9007199254740992'), S.Nothing());
+      eq(S.parseInt(10, '-9007199254740992'), S.Nothing());
+      eq(S.parseInt(10,  'Infinity'), S.Nothing());
+      eq(S.parseInt(10, '-Infinity'), S.Nothing());
+    });
+
     it('is curried', function() {
       eq(S.parseInt(10).length, 1);
       eq(S.parseInt(10)('42'), S.Just(42));