Repository: plaid/sanctuary
Pull request #126 (2016-01-05 04:42:23)

add lift, lift2, lift3
@davidchambers, Wanted to see you what thought of `S.lift` before I go ahead and throw in `S.lift2` and `S.lift3`.

`S.lift` is pretty much just a wrapper for `R.map`, but `S.lift2` and `S.lift3` also use `R.ap`.

davidchambers: I'll add a few in-line comments, but this looks great! I'm really excited about these three functions. :)

benperez: > I'll add a few in-line comments

I had a hunch :stuck_out_tongue_winking_eye: 
benperez: Addressed comments and added `S.lift2` and `S.lift3`. :muscle: :muscle: :muscle: 
svozza: Nice. This means we can close #117 too.
benperez: @davidchambers regarding `Apply` vs `Applicative`: I did some research and here is what I figured out—According to the Fantasy-Land spec it seems like the Haskell idea of `Applicative` is split into two parts in fantasy-land. 

Haskell's Applicative (see [minimal complete definition](https://hackage.haskell.org/package/base-4.8.1.0/docs/Control-Applicative.html))) calls for `pure` and `<*>` (or `ap`).

FL has `Apply`—which calls for `ap`—and `Applicative`—which calls for `of` (aka `pure`).

I suppose lift really only needs `ap` so we could just use `Apply` here. It jives with FL but it might confuse some people coming from Haskell.
davidchambers: > I suppose lift really only needs `ap` so we could just use `Apply` here. It jives with FL but it might confuse some people coming from Haskell.

Aligning with Fantasy Land is more important than aligning with Haskell. We can link to the relevant section of the FL spec to explain what we mean by "Apply".

benperez: > Aligning with Fantasy Land is more important than aligning with Haskell.

:+1: will :zap: now
benperez: :zap: 

Patch:
diff --git a/index.js b/index.js
index 835b930..346b0b5 100644
--- a/index.js
+++ b/index.js
@@ -419,6 +419,57 @@
     return f(y, x);
   });
 
+  //# lift :: Functor f => (a -> b) -> f a -> f b
+  //.
+  //. Promotes a unary function to a function which operates on a [Functor][].
+  //.
+  //. ```javascript
+  //. > S.lift(R.inc, S.Just(2))
+  //. S.Just(3)
+  //.
+  //. > S.lift(R.inc, S.Nothing())
+  //. S.Nothing()
+  //. ```
+  S.lift = def('lift', [Function, a], R.map);
+
+  //# lift2 :: Apply f => (a -> b -> c) -> f a -> f b -> f c
+  //.
+  //. Promotes a binary function to a function which operates on two
+  //. [Apply][]s.
+  //.
+  //. ```javascript
+  //. > S.lift2(R.add, S.Just(2), S.Just(3))
+  //. S.Just(5)
+  //.
+  //. > S.lift2(R.add, S.Just(2), S.Nothing())
+  //. S.Nothing()
+  //.
+  //. > S.lift2(S.and, S.Just(true), S.Just(true))
+  //. S.Just(true)
+  //.
+  //. > S.lift2(S.and, S.Just(true), S.Just(false))
+  //. S.Just(false)
+  //. ```
+  S.lift2 = def('lift2', [Function, a, b], function(f, x, y) {
+    return R.ap(R.map(f, x), y);
+  });
+
+  //# lift3 :: Apply f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
+  //.
+  //. Promotes a ternary function to a function which operates on three
+  //. [Apply][]s.
+  //.
+  //. ```javascript
+  //. > S.lift3(R.reduce, S.Just(R.add), S.Just(0), S.Just([1, 2, 3]))
+  //. S.Just(6)
+  //.
+  //. > S.lift3(R.reduce, S.Just(R.add), S.Just(0), S.Nothing())
+  //. S.Nothing()
+  //. ```
+  S.lift3 = def('lift3', [Function, a, b, c], function(f, x, y, z) {
+    return R.ap(R.ap(R.map(f, x), y), z);
+  });
+
   //. ### Composition
 
   //# compose :: (b -> c) -> (a -> b) -> a -> c
@@ -2296,8 +2347,10 @@
 
 }));
 
+//. [Apply]:        https://github.com/fantasyland/fantasy-land#apply
 //. [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
 //. [R.equals]:     http://ramdajs.com/docs/#equals


Test patch:
diff --git a/test/index.js b/test/index.js
index 3cbd6f2..5c63e38 100644
--- a/test/index.js
+++ b/test/index.js
@@ -293,6 +293,87 @@ describe('function', function() {
 
   });
 
+  describe('lift', function() {
+
+    it('is a binary function', function() {
+      eq(typeof S.lift, 'function');
+      eq(S.lift.length, 2);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.lift('wrong'); },
+                    errorEq(TypeError,
+                            '‘lift’ requires a value of type Function ' +
+                            'as its first argument; received "wrong"'));
+    });
+
+    it('lifts a function into the context of Functors', function() {
+      eq(S.lift(R.multiply(2), S.Just(3)), S.Just(6));
+      eq(S.lift(R.multiply(2), S.Nothing()), S.Nothing());
+
+      eq(S.lift(R.multiply(2), S.Left(3)), S.Left(3));
+      eq(S.lift(R.multiply(2), S.Right(3)), S.Right(6));
+
+      eq(S.lift(R.multiply(2), [1, 2, 3]), [2, 4, 6]);
+      eq(S.lift(R.multiply(2), []), []);
+    });
+
+  });
+
+  describe('lift2', function() {
+
+    it('is a ternary function', function() {
+      eq(typeof S.lift2, 'function');
+      eq(S.lift2.length, 3);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.lift2('wrong'); },
+                    errorEq(TypeError,
+                            '‘lift2’ requires a value of type Function ' +
+                            'as its first argument; received "wrong"'));
+    });
+
+    it('lifts a function into the context of Applys', function() {
+      eq(S.lift2(R.add, S.Just(3), S.Just(3)), S.Just(6));
+      eq(S.lift2(R.add, S.Nothing(), S.Just(3)), S.Nothing());
+
+      eq(S.lift2(R.add, S.Right(3), S.Left(4)), S.Left(4));
+      eq(S.lift2(R.add, S.Right(3), S.Right(4)), S.Right(7));
+
+      eq(S.lift2(R.add, [1, 2], [10, 20]), [11, 21, 12, 22]);
+      eq(S.lift2(R.add, [], [1, 2]), []);
+    });
+
+  });
+
+  describe('lift3', function() {
+
+    it('is a quaternary function', function() {
+      eq(typeof S.lift3, 'function');
+      eq(S.lift3.length, 4);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.lift3('wrong'); },
+                    errorEq(TypeError,
+                            '‘lift3’ requires a value of type Function ' +
+                            'as its first argument; received "wrong"'));
+    });
+
+    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(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(R.reduce, [R.add], [0], [[1, 2, 3]]), [6]);
+      eq(S.lift3(R.reduce, [R.add], [0], []), []);
+    });
+
+  });
+
 });
 
 describe('composition', function() {