Repository: sanctuary-js/sanctuary
Pull request #182 (2016-04-19 00:00:21)

Changes higher order functions to expect curried functions
Opened this to restart the conversation in #163 

Does not currently expose additional functions that are uncurry-safe

rjmk: Thanks for the through review, @davidchambers! 👍 
davidchambers: Looking good, @rjmk! Thank you for all your work on this. There are two more things I'd like you to do:

  - add "see also" links from each unsuffixed function to its corresponding suffixed function (e.g. from `reduce` to `reduce_`) in the style used elsewhere; and
  - `git rebase master` and squash the commits.

rjmk: Awesome! Just to clarify, shall I squash all the commits and force push onto my branch?
davidchambers: > Just to clarify, shall I squash all the commits and force push onto my branch?

That would be wonderful. GitHub has a fancy new auto-squashing feature, but I prefer to let the author squash the commits and adjust the combined commit message as he or she sees fit.


Patch:
diff --git a/index.js b/index.js
index 44023d8..1206056 100644
--- a/index.js
+++ b/index.js
@@ -530,7 +530,7 @@
 
   //. ### Function
 
-  //# flip :: (a -> b -> c) -> b -> a -> c
+  //# flip :: ((a, b) -> c) -> b -> a -> c
   //.
   //. Takes a binary function and two values and returns the result of
   //. applying the function - with its argument order reversed - to the
@@ -1286,33 +1286,71 @@
   //# encase2 :: (a -> b -> c) -> a -> b -> Maybe c
   //.
   //. Binary version of [`encase`](#encase).
+  //.
+  //. See also [`encase2_`](#encase2_).
   S.encase2 =
   def('encase2',
       {},
       [$.Function, a, b, $Maybe(c)],
       function(f, x, y) {
         try {
-          return Just(f(x, y));
+          return Just(f(x)(y));
         } catch (err) {
           return Nothing();
         }
       });
 
+  //# encase2_ :: ((a, b) -> c) -> a -> b -> Maybe c
+  //.
+  //. Version of [`encase2`](#encase2) accepting uncurried functions.
+  S.encase2_ =
+  def('encase2_',
+      {},
+      [$.Function, a, b, $Maybe(c)],
+      function(f_, x, y) {
+        var f = function(x) {
+          return function(y) {
+            return f_(x, y);
+          };
+        };
+        return S.encase2(f, x, y);
+      });
+
   //# encase3 :: (a -> b -> c -> d) -> a -> b -> c -> Maybe d
   //.
   //. Ternary version of [`encase`](#encase).
+  //.
+  //. See also [`encase3_`](#encase3_).
   S.encase3 =
   def('encase3',
       {},
       [$.Function, a, b, c, $Maybe(d)],
       function(f, x, y, z) {
         try {
-          return Just(f(x, y, z));
+          return Just(f(x)(y)(z));
         } catch (err) {
           return Nothing();
         }
       });
 
+  //# encase3_ :: ((a, b, c) -> d) -> a -> b -> c -> Maybe d
+  //.
+  //. Version of [`encase3`](#encase3) accepting uncurried functions.
+  S.encase3_ =
+  def('encase3_',
+      {},
+      [$.Function, a, b, c, $Maybe(d)],
+      function(f_, x, y, z) {
+        var f = function(x) {
+          return function(y) {
+            return function(z) {
+              return f_(x, y, z);
+            };
+          };
+        };
+        return S.encase3(f, x, y, z);
+      });
+
   //. ### Either type
   //.
   //. The Either type represents values with two possibilities: a value of type
@@ -1771,33 +1809,72 @@
   //# encaseEither2 :: (Error -> l) -> (a -> b -> r) -> a -> b -> Either l r
   //.
   //. Binary version of [`encaseEither`](#encaseEither).
+  //.
+  //. See also [`encaseEither2_`](#encaseEither2_).
   S.encaseEither2 =
   def('encaseEither2',
       {},
       [$.Function, $.Function, a, b, $Either(l, r)],
       function(f, g, x, y) {
         try {
-          return Right(g(x, y));
+          return Right(g(x)(y));
         } catch (err) {
           return Left(f(err));
         }
       });
 
+  //# encaseEither2_ :: (Error -> l) -> ((a, b) -> r) -> a -> b -> Either l r
+  //.
+  //. Version of [`encaseEither2`](#encaseEither2) accepting uncurried
+  //. functions.
+  S.encaseEither2_ =
+  def('encaseEither2_',
+      {},
+      [$.Function, $.Function, a, b, $Either(l, r)],
+      function(f, g_, x, y) {
+        var g = function(x) {
+          return function(y) {
+            return g_(x, y);
+          };
+        };
+        return S.encaseEither2(f, g, x, y);
+      });
+
   //# encaseEither3 :: (Error -> l) -> (a -> b -> c -> r) -> a -> b -> c -> Either l r
   //.
   //. Ternary version of [`encaseEither`](#encaseEither).
+  //.
+  //. See also [`encaseEither3_`](#encaseEither3_).
   S.encaseEither3 =
   def('encaseEither3',
       {},
       [$.Function, $.Function, a, b, c, $Either(l, r)],
       function(f, g, x, y, z) {
         try {
-          return Right(g(x, y, z));
+          return Right(g(x)(y)(z));
         } catch (err) {
           return Left(f(err));
         }
       });
 
+  //# encaseEither3_ :: (Error -> l) -> ((a, b, c) -> r) -> a -> b -> c -> Either l r
+  //.
+  //. Version of [`encaseEither3`](#encaseEither3) accepting uncurried functions.
+  S.encaseEither3_ =
+  def('encaseEither3',
+      {},
+      [$.Function, $.Function, a, b, c, $Either(l, r)],
+      function(f, g_, x, y, z) {
+        var g = function(x) {
+          return function(y) {
+            return function(z) {
+              return g_(x, y, z);
+            };
+          };
+        };
+        return S.encaseEither3(f, g, x, y, z);
+      });
+
   //# maybeToEither :: a -> Maybe b -> Either a b
   //.
   //. Takes a value of any type and a Maybe, and returns an Either.
@@ -2392,23 +2469,39 @@
 
   //# 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
+  //. Takes a curried 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.
   //.
+  //. See also [`reduce_`](#reduce_).
+  //.
   //. ```javascript
   //. > S.reduce(S.add, 0, [1, 2, 3, 4, 5])
   //. 15
   //.
-  //. > S.reduce((xs, x) => [x].concat(xs), [], [1, 2, 3, 4, 5])
+  //. > 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) {
+        var f = function(a, b) {
+          return f_(a)(b);
+        };
+        return reduce_(f, initial, foldable);
+      });
+
+  //# reduce_ :: Foldable f => ((a, b) -> a) -> a -> f b -> a
+  //.
+  //. Version of [`reduce`](#reduce) accepting uncurried functions.
+  var reduce_ = S.reduce_ =
+  def('reduce_',
       {b: [Foldable]},
       [$.Function, a, b, a],
       function(f, initial, foldable) {


Test patch:
diff --git a/test/encase2.js b/test/encase2.js
index 835e982..69efdfe 100644
--- a/test/encase2.js
+++ b/test/encase2.js
@@ -5,6 +5,7 @@ var throws = assert.throws;
 var eq = require('./utils').eq;
 var errorEq = require('./utils').errorEq;
 var rem = require('./utils').rem;
+var highArity = require('./utils').highArity;
 var S = require('..');
 
 
@@ -38,7 +39,7 @@ describe('encase2', function() {
   });
 
   it('can be applied to a function of arbitrary arity', function() {
-    eq(S.encase2(function(a, b, c, d) { return b; }, 0, 42), S.Just(42));
+    eq(S.encase2(highArity, 0, 42), S.Just(42));
   });
 
   it('is curried', function() {
diff --git a/test/encase2_.js b/test/encase2_.js
new file mode 100644
index 0000000..aad19ff
--- /dev/null
+++ b/test/encase2_.js
@@ -0,0 +1,22 @@
+'use strict';
+
+var R = require('ramda');
+
+var utils = require('./utils');
+
+var eq = utils.eq;
+var rem = R.uncurryN(2, utils.rem);
+var S = require('..');
+
+
+describe('encase2_', function() {
+
+  it('returns a Just on success', function() {
+    eq(S.encase2_(rem, 42, 5), S.Just(2));
+  });
+
+  it('returns a Nothing on failure', function() {
+    eq(S.encase2_(rem, 42, 0), S.Nothing());
+  });
+
+});
diff --git a/test/encase3.js b/test/encase3.js
index b2f48d2..86c01cf 100644
--- a/test/encase3.js
+++ b/test/encase3.js
@@ -6,6 +6,7 @@ var throws = assert.throws;
 var area = require('./utils').area;
 var eq = require('./utils').eq;
 var errorEq = require('./utils').errorEq;
+var highArity = require('./utils').highArity;
 var S = require('..');
 
 
@@ -39,7 +40,7 @@ describe('encase3', function() {
   });
 
   it('can be applied to a function of arbitrary arity', function() {
-    eq(S.encase3(function(a, b, c, d) { return c; }, 0, 0, 42), S.Just(42));
+    eq(S.encase3(S.K(highArity), 0, 0, 42), S.Just(42));
   });
 
   it('is curried', function() {
diff --git a/test/encase3_.js b/test/encase3_.js
new file mode 100644
index 0000000..46e1e91
--- /dev/null
+++ b/test/encase3_.js
@@ -0,0 +1,21 @@
+'use strict';
+
+var R = require('ramda');
+var utils = require('./utils');
+
+var area = R.uncurryN(3, utils.area);
+var eq = utils.eq;
+var S = require('..');
+
+
+describe('encase3_', function() {
+
+  it('returns a Just on success', function() {
+    eq(S.encase3_(area, 3, 4, 5), S.Just(6));
+  });
+
+  it('returns a Nothing on failure', function() {
+    eq(S.encase3_(area, 2, 2, 5), S.Nothing());
+  });
+
+});
diff --git a/test/encaseEither2.js b/test/encaseEither2.js
index 80a5433..650bc48 100644
--- a/test/encaseEither2.js
+++ b/test/encaseEither2.js
@@ -5,6 +5,7 @@ var throws = require('assert').throws;
 var eq = require('./utils').eq;
 var errorEq = require('./utils').errorEq;
 var rem = require('./utils').rem;
+var highArity = require('./utils').highArity;
 var S = require('..');
 
 
@@ -56,7 +57,7 @@ describe('encaseEither2', function() {
   });
 
   it('can be applied to a function of arbitrary arity', function() {
-    eq(S.encaseEither2(S.I, function(a, b, c, d) { return b; }, 0, 42),
+    eq(S.encaseEither2(S.I, highArity, 0, 42),
        S.Right(42));
   });
 
diff --git a/test/encaseEither2_.js b/test/encaseEither2_.js
new file mode 100644
index 0000000..837cf7c
--- /dev/null
+++ b/test/encaseEither2_.js
@@ -0,0 +1,27 @@
+'use strict';
+
+var R = require('ramda');
+var utils = require('./utils');
+
+var eq = utils.eq;
+var rem = R.uncurryN(2, utils.rem);
+var S = require('..');
+
+
+describe('encaseEither2_', function() {
+
+  it('returns a Right on success', function() {
+    eq(S.encaseEither2_(S.I, rem, 42, 5), S.Right(2));
+  });
+
+  it('returns a Left on failure', function() {
+    eq(S.encaseEither2_(S.I, rem, 42, 0),
+       S.Left(new Error('Cannot divide by zero')));
+  });
+
+  it('applies the first argument to the Error', function() {
+    eq(S.encaseEither2_(S.prop('message'), rem, 42, 0),
+       S.Left('Cannot divide by zero'));
+  });
+
+});
diff --git a/test/encaseEither3.js b/test/encaseEither3.js
index d20070b..2230908 100644
--- a/test/encaseEither3.js
+++ b/test/encaseEither3.js
@@ -5,6 +5,7 @@ var throws = require('assert').throws;
 var area = require('./utils').area;
 var eq = require('./utils').eq;
 var errorEq = require('./utils').errorEq;
+var highArity = require('./utils').highArity;
 var S = require('..');
 
 
@@ -56,7 +57,7 @@ describe('encaseEither3', function() {
   });
 
   it('can be applied to a function of arbitrary arity', function() {
-    eq(S.encaseEither3(S.I, function(a, b, c, d) { return c; }, 0, 0, 42),
+    eq(S.encaseEither3(S.I, S.K(highArity), 0, 0, 42),
        S.Right(42));
   });
 
diff --git a/test/encaseEither3_.js b/test/encaseEither3_.js
new file mode 100644
index 0000000..f16ab0a
--- /dev/null
+++ b/test/encaseEither3_.js
@@ -0,0 +1,27 @@
+'use strict';
+
+var R = require('ramda');
+var utils = require('./utils');
+
+var area = R.uncurryN(3, utils.area);
+var eq = utils.eq;
+var S = require('..');
+
+
+describe('encaseEither3_', function() {
+
+  it('returns a Right on success', function() {
+    eq(S.encaseEither3_(S.I, area, 3, 4, 5), S.Right(6));
+  });
+
+  it('returns a Left on failure', function() {
+    eq(S.encaseEither3_(S.I, area, 2, 2, 5),
+       S.Left(new Error('Impossible triangle')));
+  });
+
+  it('applies the first argument to the Error', function() {
+    eq(S.encaseEither3_(S.prop('message'), area, 2, 2, 5),
+       S.Left('Impossible triangle'));
+  });
+
+});
diff --git a/test/reduce.js b/test/reduce.js
index f939531..4c0cfcf 100644
--- a/test/reduce.js
+++ b/test/reduce.js
@@ -6,6 +6,12 @@ var eq = require('./utils').eq;
 var errorEq = require('./utils').errorEq;
 var S = require('..');
 
+var unaryAdd = function(a) { return function(b) { return a + b; }; };
+var UnaryFoldable = {
+  reduce: function(f, a) {
+    return f(a, 1);
+  }
+};
 
 describe('reduce', function() {
 
@@ -40,6 +46,10 @@ describe('reduce', function() {
     eq(S.reduce(S.add, 10, S.Just(5)), 15);
   });
 
+  it('works with manually curried functions when dispatching', function() {
+    eq(S.reduce(unaryAdd, 10, UnaryFoldable), 11);
+  });
+
   it('is curried', function() {
     eq(S.reduce(S.add).length, 2);
     eq(S.reduce(S.add)(0).length, 1);
diff --git a/test/reduce_.js b/test/reduce_.js
new file mode 100644
index 0000000..d2916a5
--- /dev/null
+++ b/test/reduce_.js
@@ -0,0 +1,19 @@
+'use strict';
+
+var eq = require('./utils').eq;
+var S = require('..');
+
+var add = function(a, b) { return a + b; };
+
+describe('reduce_', function() {
+
+  it('folds over lists with the supplied uncurried accumulator', function() {
+    eq(S.reduce_(add, 0, [1, 2, 3, 4, 5]), 15);
+    eq(S.reduce_(add, 0, []), 0);
+  });
+
+  it('dispatches to a "reduce" method if present', function() {
+    eq(S.reduce_(add, 10, S.Just(5)), 15);
+  });
+
+});
diff --git a/test/utils.js b/test/utils.js
index 38e9305..5ce0210 100644
--- a/test/utils.js
+++ b/test/utils.js
@@ -7,14 +7,18 @@ var R = require('ramda');
 var S = require('../index.js');
 
 
-//      area :: (Number, Number, Number) -> Number !
-exports.area = function(a, b, c) {
-  if (Math.max(a, b, c) < (a + b + c) / 2) {
-    var s = (a + b + c) / 2;
-    return Math.sqrt(s * (s - a) * (s - b) * (s - c));
-  } else {
-    throw new Error('Impossible triangle');
-  }
+//      area :: Number -> Number -> Number -> Number !
+exports.area = function(a) {
+  return function(b) {
+    return function(c) {
+      if (Math.max(a, b, c) < (a + b + c) / 2) {
+        var s = (a + b + c) / 2;
+        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
+      } else {
+        throw new Error('Impossible triangle');
+      }
+    };
+  };
 };
 
 var eq = exports.eq = function(actual, expected) {
@@ -44,13 +48,22 @@ exports.parseHex = function(s) {
   return n !== n ? S.Left('Invalid hexadecimal string') : S.Right(n);
 };
 
-//      rem :: (Number, Number) -> Number !
-exports.rem = function(x, y) {
-  if (y === 0) {
-    throw new Error('Cannot divide by zero');
-  } else {
-    return x % y;
-  }
+//      rem :: Number -> Number -> Number !
+exports.rem = function(x) {
+  return function(y) {
+    if (y === 0) {
+      throw new Error('Cannot divide by zero');
+    } else {
+      return x % y;
+    }
+  };
+};
+
+//      highArity :: a -> ((b, c, d) -> b)
+exports.highArity = function(a) {
+  return function(b, c, d) {
+    return b;
+  };
 };
 
 exports.runCompositionTests = function(compose) {