Repository: sanctuary-js/sanctuary
Pull request #192 (2016-04-23 18:09:35)

Add S.sum and S.product
I had to move a few functions to the top of the file so that we could replace the Ramda versions used throughout the code.

svozza: Anything else left on this?
davidchambers: > Anything else left on this?

This looks great, Stefano. This has motivated me to resolve #119. I plan to do so tomorrow. Once that pull request is merged, you'll be able to rebase this one and remove the `.skip`s. :)


Patch:
diff --git a/index.js b/index.js
index dc8e743..774c038 100644
--- a/index.js
+++ b/index.js
@@ -228,7 +228,7 @@
   var Foldable = $.TypeClass(
     'sanctuary/Foldable',
     function(x) {
-      return _type(x) === 'Array' || hasMethod('reduce');
+      return _type(x) === 'Array' || hasMethod('reduce')(x);
     }
   );
 
@@ -266,6 +266,7 @@
   var b = $.TypeVariable('b');
   var c = $.TypeVariable('c');
   var d = $.TypeVariable('d');
+  var f = $.TypeVariable('f');
   var l = $.TypeVariable('l');
   var r = $.TypeVariable('r');
 
@@ -678,12 +679,12 @@
   //. > S.meld([Math.pow, S.sub])(3)(4)(5)
   //. 76
   //. ```
-  var meld = S.meld =
+  S.meld =
   def('meld',
       {},
       [$.Array($.Function), $.Function],
       function(fs) {
-        var n = 1 + R.sum(R.map(R.length, fs)) - fs.length;
+        var n = 1 + sum(R.map(R.length, fs)) - fs.length;
         return R.curryN(n, function() {
           var args = Array.prototype.slice.call(arguments);
           for (var idx = 0; idx < fs.length; idx += 1) {
@@ -948,7 +949,7 @@
   //. > S.Nothing().map(S.inc)
   //. Nothing()
   //.
-  //. > S.Just([1, 2, 3]).map(R.sum)
+  //. > S.Just([1, 2, 3]).map(S.sum)
   //. Just(6)
   //. ```
   Maybe.prototype.map =
@@ -1253,7 +1254,7 @@
   def('mapMaybe',
       {},
       [$.Function, $.Array(a), $.Array(b)],
-      meld([R.map, justs]));
+      function(f, xs) { return justs(R.map(f, xs)); });
 
   //# encase :: (a -> b) -> a -> Maybe b
   //.
@@ -1566,7 +1567,7 @@
   //. > S.Left('Cannot divide by zero').map(S.inc)
   //. Left('Cannot divide by zero')
   //.
-  //. > S.Right([1, 2, 3]).map(R.sum)
+  //. > S.Right([1, 2, 3]).map(S.sum)
   //. Right(6)
   //. ```
   Either.prototype.map =
@@ -2528,7 +2529,7 @@
   //. > S.reduce(xs => x => [x].concat(xs), [], [1, 2, 3, 4, 5])
   //. [5, 4, 3, 2, 1]
   //. ```
-  S.reduce =
+  var reduce = S.reduce =
   def('reduce',
       {b: [Foldable]},
       [$.Function, a, b, a],
@@ -2699,6 +2700,29 @@
       [$.FiniteNumber, $.FiniteNumber, $.FiniteNumber],
       function(a, b) { return a + b; });
 
+  //# sum :: Foldable f => f FiniteNumber -> FiniteNumber
+  //.
+  //. Returns the sum of the given array of (finite) numbers.
+  //.
+  //. ```javascript
+  //. > S.sum([1, 2, 3, 4, 5])
+  //. 15
+  //.
+  //. > S.sum([])
+  //. 0
+  //.
+  //. > S.sum(S.Just(42))
+  //. 42
+  //.
+  //. > S.sum(S.Nothing())
+  //. 0
+  //. ```
+  var sum = S.sum =
+  def('sum',
+      {f: [Foldable]},
+      [f, $.FiniteNumber],
+      reduce(function(a) { return function(b) { return a + b; }; }, 0));
+
   //# sub :: FiniteNumber -> FiniteNumber -> FiniteNumber
   //.
   //. Returns the difference between two (finite) numbers.
@@ -2755,6 +2779,29 @@
       [$.FiniteNumber, $.FiniteNumber, $.FiniteNumber],
       function(a, b) { return a * b; });
 
+  //# product :: Foldable f => f FiniteNumber -> FiniteNumber
+  //.
+  //. Returns the product of the given array of (finite) numbers.
+  //.
+  //. ```javascript
+  //. > S.product([1, 2, 3, 4, 5])
+  //. 120
+  //.
+  //. > S.product([])
+  //. 1
+  //.
+  //. > S.product(S.Just(42))
+  //. 42
+  //.
+  //. > S.product(S.Nothing())
+  //. 1
+  //. ```
+  S.product =
+  def('product',
+      {f: [Foldable]},
+      [f, $.FiniteNumber],
+      reduce(function(a) { return function(b) { return a * b; }; }, 1));
+
   //# div :: FiniteNumber -> NonZeroFiniteNumber -> FiniteNumber
   //.
   //. Returns the result of dividing its first argument (a finite number) by


Test patch:
diff --git a/test/product.js b/test/product.js
new file mode 100644
index 0000000..9cb34ee
--- /dev/null
+++ b/test/product.js
@@ -0,0 +1,85 @@
+'use strict';
+
+var throws = require('assert').throws;
+
+var eq = require('./utils').eq;
+var errorEq = require('./utils').errorEq;
+var S = require('..');
+
+
+describe('product', function() {
+
+  it('is a unary function', function() {
+    eq(typeof S.product, 'function');
+    eq(S.product.length, 1);
+  });
+
+  it('type checks its arguments', function() {
+    throws(function() { S.product('xxx'); },
+           errorEq(TypeError,
+                   'Type-class constraint violation\n' +
+                   '\n' +
+                   'product :: Foldable f => f -> FiniteNumber\n' +
+                   '           ^^^^^^^^^^    ^\n' +
+                   '                         1\n' +
+                   '\n' +
+                   '1)  "xxx" :: String\n' +
+                   '\n' +
+                   '‘product’ requires ‘f’ to satisfy the Foldable type-class constraint; the value at position 1 does not.\n'));
+
+    throws(function() { S.product([1, 'xxx']); },
+           errorEq(TypeError,
+                   'Type-variable constraint violation\n' +
+                   '\n' +
+                   'product :: Foldable f => f -> FiniteNumber\n' +
+                   '                         ^\n' +
+                   '                         1\n' +
+                   '\n' +
+                   '1)  [1, "xxx"] :: Array ???\n' +
+                   '\n' +
+                   'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n'));
+
+    throws(function() { S.product([1, Infinity]); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'product :: Foldable f => f -> FiniteNumber\n' +
+                   '                              ^^^^^^^^^^^^\n' +
+                   '                                   1\n' +
+                   '\n' +
+                   '1)  Infinity :: Number, ValidNumber\n' +
+                   '\n' +
+                   'The value at position 1 is not a member of ‘FiniteNumber’.\n'));
+
+    throws(function() { S.product([1, -Infinity]); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'product :: Foldable f => f -> FiniteNumber\n' +
+                   '                              ^^^^^^^^^^^^\n' +
+                   '                                   1\n' +
+                   '\n' +
+                   '1)  -Infinity :: Number, ValidNumber\n' +
+                   '\n' +
+                   'The value at position 1 is not a member of ‘FiniteNumber’.\n'));
+  });
+
+  it('returns the product of an array of numbers', function() {
+    eq(S.product([]), 1);
+    eq(S.product([0, 1, 2, 3]), 0);
+    eq(S.product([-0, 1, 2, 3]), -0);
+    eq(S.product([1, 2, 3, 4, 5]), 120);
+    eq(S.product([1, 2, 3, 4, -5]), -120);
+  });
+
+  it('can be applied to Maybes', function() {
+    eq(S.product(S.Nothing()), 1);
+    eq(S.product(S.Just(42)), 42);
+  });
+
+  it('can be applied to Eithers', function() {
+    eq(S.product(S.Left('xxx')), 1);
+    eq(S.product(S.Right(42)), 42);
+  });
+
+});
diff --git a/test/sum.js b/test/sum.js
new file mode 100644
index 0000000..cafa4e9
--- /dev/null
+++ b/test/sum.js
@@ -0,0 +1,85 @@
+'use strict';
+
+var throws = require('assert').throws;
+
+var eq = require('./utils').eq;
+var errorEq = require('./utils').errorEq;
+var S = require('..');
+
+
+describe('sum', function() {
+
+  it('is a unary function', function() {
+    eq(typeof S.sum, 'function');
+    eq(S.sum.length, 1);
+  });
+
+  it('type checks its arguments', function() {
+    throws(function() { S.sum('xxx'); },
+           errorEq(TypeError,
+                   'Type-class constraint violation\n' +
+                   '\n' +
+                   'sum :: Foldable f => f -> FiniteNumber\n' +
+                   '       ^^^^^^^^^^    ^\n' +
+                   '                     1\n' +
+                   '\n' +
+                   '1)  "xxx" :: String\n' +
+                   '\n' +
+                   '‘sum’ requires ‘f’ to satisfy the Foldable type-class constraint; the value at position 1 does not.\n'));
+
+    throws(function() { S.sum([1, 'xxx']); },
+           errorEq(TypeError,
+                   'Type-variable constraint violation\n' +
+                   '\n' +
+                   'sum :: Foldable f => f -> FiniteNumber\n' +
+                   '                     ^\n' +
+                   '                     1\n' +
+                   '\n' +
+                   '1)  [1, "xxx"] :: Array ???\n' +
+                   '\n' +
+                   'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n'));
+
+    throws(function() { S.sum([1, Infinity]); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'sum :: Foldable f => f -> FiniteNumber\n' +
+                   '                          ^^^^^^^^^^^^\n' +
+                   '                               1\n' +
+                   '\n' +
+                   '1)  Infinity :: Number, ValidNumber\n' +
+                   '\n' +
+                   'The value at position 1 is not a member of ‘FiniteNumber’.\n'));
+
+    throws(function() { S.sum([1, -Infinity]); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'sum :: Foldable f => f -> FiniteNumber\n' +
+                   '                          ^^^^^^^^^^^^\n' +
+                   '                               1\n' +
+                   '\n' +
+                   '1)  -Infinity :: Number, ValidNumber\n' +
+                   '\n' +
+                   'The value at position 1 is not a member of ‘FiniteNumber’.\n'));
+  });
+
+  it('sums an array of numbers together', function() {
+    eq(S.sum([]), 0);
+    eq(S.sum([0, 1, 2, 3]), 6);
+    eq(S.sum([-0, 1, 2, 3]), 6);
+    eq(S.sum([1, 2, 3, 4, 5]), 15);
+    eq(S.sum([1, 2, 3, 4, -5]), 5);
+  });
+
+  it('can be applied to Maybes', function() {
+    eq(S.sum(S.Nothing()), 0);
+    eq(S.sum(S.Just(42)), 42);
+  });
+
+  it('can be applied to Eithers', function() {
+    eq(S.sum(S.Left('xxx')), 0);
+    eq(S.sum(S.Right(42)), 42);
+  });
+
+});