Repository: plaid/sanctuary
Pull request #94 (2015-09-20 20:20:58)

composition: add S.compose, S.pipe, and S.meld
I proposed these functions in https://github.com/ramda/ramda/issues/1318#issuecomment-132005376. It's unlikely that Ramda will provide these exact functions but I happen to contribute to another functional programming library as well. ;)

The implementation of `compose` is wonderfully straightforward.

The implementation of `pipe` is elegant: a right fold over the list of functions, with `compose` as the reducing function and `identity` as the identity element.

I'm pleased with `meld` as the name of the third function. Is it appealing to others as well?

I'm interested to know whether this patch gets the @joneshf seal of approval; `compose` and `pipe` are intended to be easier to reason about than their Ramda counterparts.

I'd also like to hear from @CrossEye, @paldepind, and other Ramda contributors as some of these ideas should filter back into Ramda. We could, for example, revert `R.compose` and `R.pipe` to the pre-v0.17.0 behaviour and add `R.meld` (which would be a variadic to match `R.pipe`).


kedashoe: There is something very pleasing about defining compose `return f(g(x));`.. but in the bigger javascript picture, it just feels like an unnecessary special case to me. If I am running some data through some functions, should I use `compose` if there are only two of them and `pipe` if there are more? And if I start with two and then realize I need more, I need to change from `compose` to `pipe`, reverse the order, and add brackets. For certain things I'm sure we are quite safe with a binary compose (`lines` is unlikely to all of a sudden need a 3rd function), but when I just have any old random data from some 3rd party API and I'm doing whatever to it, I find it's not that uncommon to be changing how many functions I'm using. Do we want `compose3`, 4, etc? I feel like I would probably just always use `pipe`, in which case the only thing `compose` is doing for me is slowing down `pipe`.

:+1:  for `meld`, excited to see how that can be used!
davidchambers: Thank you for your thoughtful feedback, @kedashoe.

I'm not certain that a curried ternary `compose` is the best option. I have, on a small number of occasions, wanted a curried `compose` and have defined it via `R.curryN(2, R.compose)` (doctest's source contains one [example][1]). Composing _n_ functions at once is far more common, though, and is the more important use case.

> I feel like I would probably just always use `pipe`

`pipe` can be used to compose _n_ functions (for _n_ = 2, even). Perhaps, as you say, we're well advised to use `pipe` exclusively since pipelines may grow beyond the strict length limit imposed by `compose`. This is true, though this is arguably already the case with the Ramda counterparts if one considers line length. Even those who prefer `compose` over `pipe` for single-line compositions will concede that `pipe` is the better choice when composing many functions over many lines. A function initially defined via

```javascript
R.compose(..., ..., ...)
```

might become

```javascript
R.pipe(...,
       ...,
       ...,
       ...,
       ...)
```

as the pipeline grows in response to changing requirements.

I quite *like* the fact that `compose` and `pipe` are not symmetrical, as it makes it easier to decide which to use: `compose` in the rare cases in which one wishes to partially apply `compose` itself; `pipe` otherwise.

> […] in which case the only thing `compose` is doing for me is slowing down `pipe`.

Do you say this because `compose` is used in the implementation of `pipe`? If so, we could certainly make the implementation more efficient (in a subsequent pull request, perhaps). I'm most interested in discussing the types at this stage.

> :+1: for `meld`, excited to see how that can be used!

I am too. I really like the idea, but forcing it into `R.compose` and `R.pipe` made these functions behave in surprising ways. When choosing `meld` over `pipe` one should realize that the arity of each function in the pipeline is significant: after all, one would choose `meld` because one *wants* at least one of the functions in the composition to receive more than one argument.


[1]: https://github.com/davidchambers/doctest/blob/0.9.0/lib/doctest.js#L192

paldepind: I think these looks neat. Except `meld`. It's far from being as elegant and simple as the other functions.

I think I would prefer:

```js
// compose2 :: (c -> d) -> (a -> b -> c) -> a -> b -> d
compose2 = compose(compose, compose);
```

It is not as general. But in my opinion code is probably not very readable if you're using the more complex versions anyway.

I find this to be quite hard to understand:
```js
S.meld([Math.pow, R.multiply, R.subtract])(3, 4, 5, 6)
```

But this is understandable to me:
```js
S.compose2(Math.pow, R.multiply)(3, 4);
```

I don't like how reverse-composing is given preference when composing more than two functions. I prefer the normal composition order. I.e. I'd personally want `pipe` to be reversed. I think such an option should be available. Then the library is not opinionated about composition order.
CrossEye: > I find this to be quite hard to understand:
>
> ```js
> S.meld([Math.pow, R.multiply, R.subtract])(3, 4, 5, 6)
> ```
> 
> But this is understandable to me:
> 
> ```js
> S.compose2(Math.pow, R.multiply)(3, 4);
> ```

I don't actually make any sense of that latter one.  It looks to me like 

```js
Math.pow(R.multiply(3, 4)); //=>
Math.pow(12); //=> NaN
```

Is that what you're looking for?  But in this proposal

```js
S.meld([Math.pow, R.multiply])(3, 4, 5); //=>
R.multiply(Math.pow(3, 4), 5); //=>
R.multiply(81, 5); //=>
405
```

seems pretty straightforward, **if** you know the arities of the functions involved.  My biggest concern with this function is that unlike a normal `pipe`/`compose`, I really have to understand the component functions, or at least their arities to make any sense of it.

    S.meld([f, g, h])(3, 4, 5, 6) //=>
    h(g(f(3), 4), 5, 6)); // or
    h(g(f(3), 4, 5), 6)); // or
    h(g(f(3, 4)), 5, 6)); // or
    h(g(f(3, 4), 5, 6))); // or
    h(g(f(3, 4, 5)), 6)); // or
    h(g(f(3, 4, 5), 6))); // or
    h(g(f(3, 4, 5, 6))); // and possibly others.

While I'm generally in favor of things that help with building complex function expressively in source code, even if they make less sense when viewed as more general-purpose functions operating on data, that list gives me some pause.

paldepind: > I don't actually make any sense of that latter one. It looks to me like 

Sorry. That was a mistake. It should have been something like this:

```js
S.compose2(Math.sqrt, R.multiply)(3, 4);
```

Patch:
diff --git a/index.js b/index.js
index a9c1298..f257188 100644
--- a/index.js
+++ b/index.js
@@ -348,6 +348,83 @@
     return x;
   });
 
+  //. ### Composition
+
+  //# compose :: (b -> c) -> (a -> b) -> a -> c
+  //.
+  //. Takes two functions assumed to be unary and a value of any type,
+  //. and returns the result of applying the first function to the result
+  //. of applying the second function to the given value.
+  //.
+  //. In general terms, `compose` performs right-to-left composition of two
+  //. unary functions.
+  //.
+  //. See also [`pipe`](#pipe).
+  //.
+  //. ```javascript
+  //. > S.compose(Math.sqrt, R.inc)(99)
+  //. 10
+  //. ```
+  var compose = S.compose =
+  def('compose', [Function, Function, a], function(f, g, x) {
+    return f(g(x));
+  });
+
+  //# pipe :: [(a -> b), (b -> c), ..., (m -> n)] -> a -> n
+  //.
+  //. Takes a list of functions assumed to be unary and a value of any type,
+  //. and returns the result of applying the sequence of transformations to
+  //. the initial value.
+  //.
+  //. In general terms, `pipe` performs left-to-right composition of a list
+  //. of functions. `pipe([f, g, h], x)` is equivalent to `h(g(f(x)))`.
+  //.
+  //. See also [`meld`](#meld).
+  //.
+  //. ```javascript
+  //. > S.pipe([R.inc, Math.sqrt, R.dec])(99)
+  //. 9
+  //. ```
+  S.pipe = def('pipe', [List, a], function(fs, x) {
+    return R.reduceRight(compose, I, fs)(x);
+  });
+
+  //# meld :: [** -> *] -> (* -> * -> ... -> *)
+  //.
+  //. Takes a list of non-nullary functions and returns a curried function
+  //. whose arity is one greater than the sum of the arities of the given
+  //. functions less the number of functions.
+  //.
+  //. The behaviour of `meld` is best conveyed diagrammatically. The following
+  //. diagram depicts the "melding" of binary functions `f` and `g`:
+  //.
+  //.               +-------+
+  //.     --- a --->|       |
+  //.               |   f   |                +-------+
+  //.     --- b --->|       |--- f(a, b) --->|       |
+  //.               +-------+                |   g   |
+  //.     --- c ---------------------------->|       |--- g(f(a, b), c) --->
+  //.                                        +-------+
+  //.
+  //. See also [`pipe`](#pipe).
+  //.
+  //. ```javascript
+  //. > S.meld([Math.pow, R.subtract])(3, 4, 5)
+  //. 76
+  //.
+  //. > S.meld([Math.pow, R.subtract])(3)(4)(5)
+  //. 76
+  //. ```
+  S.meld = def('meld', [List], function(fs) {
+    return R.curryN(1 + R.sum(R.map(R.length, fs)) - fs.length, function() {
+      var args = Array.prototype.slice.call(arguments);
+      for (var idx = 0; idx < fs.length; idx += 1) {
+        args.unshift(fs[idx].apply(this, args.splice(0, fs[idx].length)));
+      }
+      return args[0];
+    });
+  });
+
   //. ### Maybe type
 
   //# Maybe :: TypeRep Maybe
@@ -1745,7 +1822,7 @@
   //. ["foo", "bar", "baz"]
   //. ```
   S.words =
-  def('words', [String], R.compose(R.reject(R.isEmpty), R.split(/\s+/)));
+  def('words', [String], compose(R.reject(R.isEmpty), R.split(/\s+/)));
 
   //# unwords :: [String] -> String
   //.
@@ -1774,7 +1851,7 @@
   //. ```
   S.lines =
   def('lines', [String],
-      R.compose(R.match(/^(?=[\s\S]).*/gm), R.replace(/\r\n?/g, '\n')));
+      compose(R.match(/^(?=[\s\S]).*/gm), R.replace(/\r\n?/g, '\n')));
 
   //# unlines :: [String] -> String
   //.
@@ -1788,7 +1865,7 @@
   //. "foo\nbar\nbaz\n"
   //. ```
   S.unlines =
-  def('unlines', [List], R.compose(R.join(''), R.map(R.concat(_, '\n'))));
+  def('unlines', [List], compose(R.join(''), R.map(R.concat(_, '\n'))));
 
   return S;
 


Test patch:
diff --git a/test/index.js b/test/index.js
index 5f07d69..a1e7753 100644
--- a/test/index.js
+++ b/test/index.js
@@ -205,6 +205,107 @@ describe('combinator', function() {
 
 });
 
+describe('composition', function() {
+
+  describe('compose', function() {
+
+    it('is a ternary function', function() {
+      eq(typeof S.compose, 'function');
+      eq(S.compose.length, 3);
+    });
+
+    it('composes two functions assumed to be unary', function() {
+      eq(S.compose(R.map(Math.sqrt), JSON.parse, '[1, 4, 9]'), [1, 2, 3]);
+    });
+
+    it('is curried', function() {
+      eq(S.compose(R.map(Math.sqrt)).length, 2);
+      eq(S.compose(R.map(Math.sqrt))(JSON.parse).length, 1);
+      eq(S.compose(R.map(Math.sqrt))(JSON.parse)('[1, 4, 9]'), [1, 2, 3]);
+    });
+
+  });
+
+  describe('pipe', function() {
+
+    it('is a binary function', function() {
+      eq(typeof S.pipe, 'function');
+      eq(S.pipe.length, 2);
+    });
+
+    it('composes a list of functions assumed to be unary', function() {
+      eq(S.pipe([], '99'), '99');
+      eq(S.pipe([parseInt], '99'), 99);
+      eq(S.pipe([parseInt, R.inc], '99'), 100);
+      eq(S.pipe([parseInt, R.inc, Math.sqrt], '99'), 10);
+      eq(S.pipe([parseInt, R.inc, Math.sqrt, R.dec], '99'), 9);
+    });
+
+    it('is curried', function() {
+      eq(S.pipe([parseInt, R.inc, Math.sqrt, R.dec]).length, 1);
+      eq(S.pipe([parseInt, R.inc, Math.sqrt, R.dec])('99'), 9);
+    });
+
+  });
+
+  describe('meld', function() {
+
+    it('is a unary function', function() {
+      eq(typeof S.meld, 'function');
+      eq(S.meld.length, 1);
+    });
+
+    it('composes a list of unary functions', function() {
+      eq(S.meld([]).length, 1);
+      eq(S.meld([])(99), 99);
+      eq(S.meld([R.inc]).length, 1);
+      eq(S.meld([R.inc])(99), 100);
+      eq(S.meld([R.inc, Math.sqrt]).length, 1);
+      eq(S.meld([R.inc, Math.sqrt])(99), 10);
+      eq(S.meld([R.inc, Math.sqrt, R.dec]).length, 1);
+      eq(S.meld([R.inc, Math.sqrt, R.dec])(99), 9);
+    });
+
+    it('melds a list of non-nullary functions of various arities', function() {
+      var f = function(x) { return -x; };
+      var g = function(x, y) { return Math.pow(x, y); };
+      var h = function(x, y, z) { return x + y + z; };
+
+      eq(S.meld([f, f, f]).length, 1);
+      eq(S.meld([f, f, g]).length, 2);
+      eq(S.meld([f, g, f]).length, 2);
+      eq(S.meld([g, f, f]).length, 2);
+      eq(S.meld([f, g, g]).length, 3);
+      eq(S.meld([g, f, g]).length, 3);
+      eq(S.meld([g, g, f]).length, 3);
+      eq(S.meld([g, g, g]).length, 4);
+
+      eq(S.meld([f, g, h]).length, 4);
+      eq(S.meld([f, h, g]).length, 4);
+      eq(S.meld([g, f, h]).length, 4);
+      eq(S.meld([g, h, f]).length, 4);
+      eq(S.meld([h, f, g]).length, 4);
+      eq(S.meld([h, g, f]).length, 4);
+
+      eq(S.meld([f, g, h])(3, 4, 5, 6), h(g(f(3), 4), 5, 6));
+      eq(S.meld([f, h, g])(3, 4, 5, 6), g(h(f(3), 4, 5), 6));
+      eq(S.meld([g, f, h])(3, 4, 5, 6), h(f(g(3, 4)), 5, 6));
+      eq(S.meld([g, h, f])(3, 4, 5, 6), f(h(g(3, 4), 5, 6)));
+      eq(S.meld([h, f, g])(3, 4, 5, 6), g(f(h(3, 4, 5)), 6));
+      eq(S.meld([h, g, f])(3, 4, 5, 6), f(g(h(3, 4, 5), 6)));
+
+      eq(S.meld([f, g, h])(3)(4)(5)(6), h(g(f(3), 4), 5, 6));
+      eq(S.meld([f, h, g])(3)(4)(5)(6), g(h(f(3), 4, 5), 6));
+      eq(S.meld([g, f, h])(3)(4)(5)(6), h(f(g(3, 4)), 5, 6));
+      eq(S.meld([g, h, f])(3)(4)(5)(6), f(h(g(3, 4), 5, 6)));
+      eq(S.meld([h, f, g])(3)(4)(5)(6), g(f(h(3, 4, 5)), 6));
+      eq(S.meld([h, g, f])(3)(4)(5)(6), f(g(h(3, 4, 5), 6)));
+    });
+
+  });
+
+});
+
 describe('maybe', function() {
 
   describe('Maybe', function() {