Repository: sanctuary-js/sanctuary
Pull request #234 (2016-06-27 18:18:34)

Add S.mean that calculates mean of values of a foldable
I’ve implemented the `mean` function, as it’s been suggested in #194.

The tests are roughly based on `product` tests. One thing I couldn’t figure out is how to put a type constraint on the argument of type `f`, i.e. how to specify the function accepts only foldables of finite numbers. I’ve noticed neither `product` nor `sum` enforce this restriction on their argument, only on their results (even though the comments suggest they do: `product :: Foldable f => f FiniteNumber -> FiniteNumber`).

Passing a foldable with an infinite number to `mean` does not typecheck, but it fails on the call to `sum`, which makes the message a bit misleading.

davidchambers: Thank you for the pull request, @jdudek! It's exciting to receive a pull request from a new contributor. :)

> One thing I couldn’t figure out is how to put a type constraint on the argument of type `f`, i.e. how to specify the function accepts only foldables of finite numbers.

It's not currently possible, but we'll be able to add this constraint in #232 thanks to introduction of `$.UnaryTypeVariable` in sanctuary-js/sanctuary-def#79.

We can simplify the implementation quite a bit. If we define `mean` in terms of [`reduce_`][1] we can avoid type checks. Something along these lines should work:

```javascript
var result = reduce_(
  function(acc, n) { return {total: acc.total + n, count: acc.count + 1}; },
  {total: 0, count: 0},
  foldable
);
return result.count === 0 ? Nothing() : Just(result.total / result.count);
```


[1]: http://sanctuary.js.org/#reduce_

jdudek: Thanks for the reply. I thought about accumulating both sum and count in one fold, but it seemed like it would introduce unnecessary object allocations. So today I ran [a simple benchmark](https://gist.github.com/jdudek/a6897e06088cb8c9468d99281e129546) and it turns out your solution is not only more concise, but also significantly faster (both with typechecking on and off)—assuming my microbenchmark makes any sense :) I also tested with much larger arrays and the results where similar.

I’ve updated the implementation and added a test for the case with infinite arguments.
kedashoe: > but it seemed like it would introduce unnecessary object allocations

I think this is still fair to say. [Any objections](http://clojure.org/reference/transients) from anyone to something like

```js
var result = reduce_(
  function(acc, n) {
    acc.total += n;
    acc.count += 1;
    return acc;
  },
  {total: 0, count: 0},
  foldable
);
```
?
davidchambers: Thanks for the updates, @jdudek. They look good. I think @kedashoe's version with local mutation is even better. Let's go with that. :)

jdudek: I couldn’t resist running the benchmark with @kedashoe’s version and it actually is noticeably faster:

```
Results
-> benchmark.js
    mean (jd) x 2,728 ops/sec ±1.50% (86 runs sampled)
    mean (dc) x 6,699 ops/sec ±1.27% (86 runs sampled)
    mean (kedashoe) x 8,739 ops/sec ±1.42% (82 runs sampled)
```
davidchambers: Thanks! I'd like to request one final change. Could you move the definition below that of `S.div`? It doesn't seem right to me to include `S.mean` between `S.mult` and `S.div` (whereas it does make sense for `S.product` to be grouped with `S.mult`). Also, please squash the commits. :)


Patch:
diff --git a/index.js b/index.js
index ca20712..05a5fef 100644
--- a/index.js
+++ b/index.js
@@ -3083,6 +3083,41 @@
       [$.FiniteNumber, $.NonZeroFiniteNumber, $.FiniteNumber],
       function(a, b) { return a / b; });
 
+  //# mean :: Foldable f => f FiniteNumber -> Maybe FiniteNumber
+  //.
+  //. Returns the mean of the given array of (finite) numbers.
+  //.
+  //. ```javascript
+  //. > S.mean([1, 2, 3, 4, 5])
+  //. S.Just(3)
+  //.
+  //. > S.mean([])
+  //. S.Nothing()
+  //.
+  //. > S.mean(S.Just(42))
+  //. S.Just(42)
+  //.
+  //. > S.mean(S.Nothing())
+  //. S.Nothing()
+  //. ```
+  S.mean =
+  def('mean',
+      {f: [Foldable]},
+      [f, $Maybe($.FiniteNumber)],
+      function(foldable) {
+        var result = reduce_(
+          function(acc, n) {
+            acc.total += n;
+            acc.count += 1;
+            return acc;
+          },
+          {total: 0, count: 0},
+          foldable
+        );
+        return result.count === 0 ? Nothing()
+                                  : Just(result.total / result.count);
+      });
+
   //# min :: Ord a => a -> a -> a
   //.
   //. Returns the smaller of its two arguments.


Test patch:
diff --git a/test/mean.js b/test/mean.js
new file mode 100644
index 0000000..ad6dc6c
--- /dev/null
+++ b/test/mean.js
@@ -0,0 +1,76 @@
+'use strict';
+
+var throws = require('assert').throws;
+
+var eq = require('./utils').eq;
+var errorEq = require('./utils').errorEq;
+var S = require('..');
+
+
+describe('mean', function() {
+
+  it('is a unary function', function() {
+    eq(typeof S.mean, 'function');
+    eq(S.mean.length, 1);
+  });
+
+  it('type checks its arguments', function() {
+    throws(function() { S.mean('xxx'); },
+           errorEq(TypeError,
+                   'Type-class constraint violation\n' +
+                   '\n' +
+                   'mean :: Foldable f => f -> Maybe FiniteNumber\n' +
+                   '        ^^^^^^^^^^    ^\n' +
+                   '                      1\n' +
+                   '\n' +
+                   '1)  "xxx" :: String\n' +
+                   '\n' +
+                   '‘mean’ requires ‘f’ to satisfy the Foldable type-class constraint; the value at position 1 does not.\n'));
+
+    throws(function() { S.mean([1, 2, 'xxx']); },
+           errorEq(TypeError,
+                   'Type-variable constraint violation\n' +
+                   '\n' +
+                   'mean :: Foldable f => f -> Maybe FiniteNumber\n' +
+                   '                      ^\n' +
+                   '                      1\n' +
+                   '\n' +
+                   '1)  [1, 2, "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.mean([1, Infinity]); },
+           errorEq(TypeError,
+                   'Invalid value\n' +
+                   '\n' +
+                   'mean :: Foldable f => f -> Maybe 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 mean of a non-empty array of numbers', function() {
+    eq(S.mean([1, 2, 3]), S.Just(2));
+    eq(S.mean([0.1, 0.3]), S.Just(0.2));
+    eq(S.mean([-0]), S.Just(0));
+    eq(S.mean([-0, 0]), S.Just(0));
+  });
+
+  it('returns nothing when applied to an empty array', function() {
+    eq(S.mean([]), S.Nothing());
+  });
+
+  it('can be applied to Maybes', function() {
+    eq(S.mean(S.Nothing()), S.Nothing());
+    eq(S.mean(S.Just(42)), S.Just(42));
+  });
+
+  it('can be applied to Eithers', function() {
+    eq(S.mean(S.Left('xxx')), S.Nothing());
+    eq(S.mean(S.Right(42)), S.Just(42));
+  });
+
+});