Repository: plaid/sanctuary
Pull request #148 (2016-02-05 21:51:21)

Add S.reduce function
Those of you that know your Ramda history should appreciate the irony of this one...

davidchambers: Since we define [`Maybe#reduce`][1] (in accordance with the requirements of [Foldable][2]), this function should dispatch to a `reduce` method if present. It'd make sense to update the function's name to match.

We could define `Foldable :: TypeClass` to constrain the relevant argument.


[1]: https://github.com/plaid/sanctuary#Maybe.prototype.reduce
[2]: https://github.com/fantasyland/fantasy-land#foldable

svozza: Dammit, `foldl` is so much cooler as a name. ;) Not sure what to put for the documentation now that the type takes the more genral type `Foldable` rather than `Array`. Any suggestions?
davidchambers: This function seems more at home in the [list][1] section than in the [function][2] section. Do you agree?

Also, you should edit the commit message. ;)


[1]: https://github.com/plaid/sanctuary#list
[2]: https://github.com/plaid/sanctuary#function

svozza: Yeah, I wasn't sure where to put it, `List` seems more appropriate given the standard way the function willl be used.
davidchambers: Let's insert `S.reduce` immediately before `S.unfoldr`. These two functions are related, and beginning the list section with the various slicing functions seems appropriate.

svozza: :zap: x 2
svozza: :zap: x 2

Patch:
diff --git a/index.js b/index.js
index 2a06cac..de4ce97 100644
--- a/index.js
+++ b/index.js
@@ -151,6 +151,14 @@
     }
   );
 
+  //  Foldable :: TypeClass
+  var Foldable = $.TypeClass(
+    'sanctuary/Foldable',
+    function(x) {
+      return _type(x) === 'Array' || hasMethod('reduce');
+    }
+  );
+
   //  Functor :: TypeClass
   var Functor = $.TypeClass(
     'sanctuary/Functor',
@@ -435,10 +443,10 @@
   //. [Apply][]s.
   //.
   //. ```javascript
-  //. > S.lift3(R.reduce, S.Just(R.add), S.Just(0), S.Just([1, 2, 3]))
+  //. > S.lift3(S.reduce, S.Just(S.add), S.Just(0), S.Just([1, 2, 3]))
   //. Just(6)
   //.
-  //. > S.lift3(R.reduce, S.Just(R.add), S.Just(0), S.Nothing())
+  //. > S.lift3(S.reduce, S.Just(S.add), S.Just(0), S.Nothing())
   //. Nothing()
   //. ```
   S.lift3 =
@@ -2171,6 +2179,39 @@
       [TypeRep, $.String, $.Array(a), $.Array($Maybe(b))],
       function(type, key, xs) { return R.map(get(type, key), xs); });
 
+  //# reduce :: Foldable f => (a -> b -> a) -> a -> f b -> a
+  //.
+  //. Takes a binary function, an initial value, and a [Foldable][], and
+  //. applies the function to the initial value and the Foldable's first
+  //. value, then applies the function to the result of the previous
+  //. application and the Foldable's second value. Repeats this process
+  //. until each of the Foldable's values has been used. Returns the initial
+  //. value if the Foldable is empty; the result of the final application
+  //. otherwise.
+  //.
+  //. ```javascript
+  //. > S.reduce(S.add, 0, [1, 2, 3, 4, 5])
+  //. 15
+  //.
+  //. > S.reduce((xs, x) => [x].concat(xs), [], [1, 2, 3, 4, 5])
+  //. [5, 4, 3, 2, 1]
+  //. ```
+  S.reduce =
+  def('reduce',
+      {b: [Foldable]},
+      [$.Function, a, b, a],
+      function(f, initial, foldable) {
+        if (_type(foldable) === 'Array') {
+          var acc = initial;
+          for (var idx = 0; idx < foldable.length; idx += 1) {
+            acc = f(acc, foldable[idx]);
+          }
+          return acc;
+        } else {
+          return foldable.reduce(f, initial);
+        }
+      });
+
   //# unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
   //.
   //. Takes a function and a seed value, and returns a list generated by
@@ -2780,6 +2821,7 @@
 //. [Apply]:          https://github.com/fantasyland/fantasy-land#apply
 //. [BinaryType]:     https://github.com/plaid/sanctuary-def#binarytype
 //. [Extend]:         https://github.com/fantasyland/fantasy-land#extend
+//. [Foldable]:       https://github.com/fantasyland/fantasy-land#foldable
 //. [Functor]:        https://github.com/fantasyland/fantasy-land#functor
 //. [Monad]:          https://github.com/fantasyland/fantasy-land#monad
 //. [Monoid]:         https://github.com/fantasyland/fantasy-land#monoid


Test patch:
diff --git a/test/index.js b/test/index.js
index 472488b..fc2d418 100644
--- a/test/index.js
+++ b/test/index.js
@@ -439,14 +439,14 @@ describe('function', function() {
     });
 
     it('lifts a function into the context of Applys', function() {
-      eq(S.lift3(R.reduce, S.Just(R.add), S.Just(0), S.Just([1, 2, 3])), S.Just(6));
-      eq(S.lift3(R.reduce, S.Just(R.add), S.Just(0), S.Nothing()), S.Nothing());
+      eq(S.lift3(S.reduce, S.Just(R.add), S.Just(0), S.Just([1, 2, 3])), S.Just(6));
+      eq(S.lift3(S.reduce, S.Just(R.add), S.Just(0), S.Nothing()), S.Nothing());
 
-      eq(S.lift3(R.reduce, S.Right(R.add), S.Right(0), S.Right([1, 2, 3])), S.Right(6));
-      eq(S.lift3(R.reduce, S.Right(R.add), S.Right(0), S.Left('WHOOPS')), S.Left('WHOOPS'));
+      eq(S.lift3(S.reduce, S.Right(R.add), S.Right(0), S.Right([1, 2, 3])), S.Right(6));
+      eq(S.lift3(S.reduce, S.Right(R.add), S.Right(0), S.Left('WHOOPS')), S.Left('WHOOPS'));
 
-      eq(S.lift3(R.reduce, [R.add], [0], [[1, 2, 3]]), [6]);
-      eq(S.lift3(R.reduce, [R.add], [0], []), []);
+      eq(S.lift3(S.reduce, [R.add], [0], [[1, 2, 3]]), [6]);
+      eq(S.lift3(S.reduce, [R.add], [0], []), []);
 
       eq(S.lift3(R.curry(area), R.dec, S.I, R.inc)(4), 6);
     });
@@ -3197,6 +3197,40 @@ describe('list', function() {
 
   });
 
+  describe('reduce', function() {
+
+    it('is a ternary function', function() {
+      eq(typeof S.reduce, 'function');
+      eq(S.reduce.length, 3);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.reduce('xxx'); },
+                    errorEq(TypeError,
+                            '‘reduce’ expected a value of type Function ' +
+                            'as its first argument; received "xxx"'));
+    });
+
+    it('folds over lists with the supplied accumulator', function() {
+      eq(S.reduce(S.add, 0, [1, 2, 3, 4, 5]), 15);
+      eq(S.reduce(S.add, 0, []), 0);
+      eq(S.reduce(S.lift2(S.add), S.Just(0),
+                  [S.Just(1), S.Just(2), S.Just(3), S.Just(4), S.Just(5)]),
+         S.Just(15));
+    });
+
+    it('dispatches to a "reduce" method if present', function() {
+      eq(S.reduce(S.add, 10, S.Just(5)), 15);
+    });
+
+    it('is curried', function() {
+      eq(S.reduce(S.add).length, 2);
+      eq(S.reduce(S.add)(0).length, 1);
+      eq(S.reduce(S.add)(0)([1, 2, 3, 4, 5]), 15);
+    });
+
+  });
+
   describe('unfoldr', function() {
 
     it('is a binary function', function() {