Repository: sanctuary-js/sanctuary
Pull request #413 (2017-06-14 00:09:45)

array: generalize S.reverse
:warning: This is a breaking change, as [`reverse`][1] will no longer operate on strings.

This pull request inches us closer to a goal I stated in #411: removing the [List][2] type.


[1]: https://github.com/sanctuary-js/sanctuary/tree/v0.13.2#reverse
[2]: https://github.com/sanctuary-js/sanctuary/tree/v0.13.2#list



Patch:
diff --git a/index.js b/index.js
index e36cd65..f0f2e93 100644
--- a/index.js
+++ b/index.js
@@ -376,6 +376,12 @@
 
   /* eslint-disable indent */
 
+  //  checkTypes :: Boolean
+  var checkTypes = opts.checkTypes;
+
+  //  env :: Array Type
+  var env = opts.env;
+
   var S = {};
 
   //# create :: { checkTypes :: Boolean, env :: Array Type } -> Module
@@ -429,10 +435,10 @@
   //.
   //. See also [`env`](#env).
   S.create =
-  $.create({checkTypes: opts.checkTypes, env: defaultEnv})('create',
-                                                           {},
-                                                           [Options, $.Object],
-                                                           createSanctuary);
+  $.create({checkTypes: checkTypes, env: defaultEnv})('create',
+                                                      {},
+                                                      [Options, $.Object],
+                                                      createSanctuary);
 
   //# env :: Array Type
   //.
@@ -440,7 +446,15 @@
   //. custom environment in conjunction with [`create`](#create).
   S.env = defaultEnv;
 
-  var def = $.create(opts);
+  /* istanbul ignore if */
+  if (typeof __doctest !== 'undefined') {
+    var _List = require('./test/internal/List');
+    var Cons = _List.Cons;  // eslint-disable-line no-unused-vars
+    var Nil = _List.Nil;    // eslint-disable-line no-unused-vars
+    env = Z.concat(env, [_List.Type($.Unknown)]);
+  }
+
+  var def = $.create({checkTypes: checkTypes, env: env});
 
   //. ### Placeholder
   //.
@@ -3304,23 +3318,6 @@
   S.dropLast =
   def('dropLast', {}, [$.Integer, List(a), $Maybe(List(a))], dropLast);
 
-  //# reverse :: List a -> List a
-  //.
-  //. Returns the elements of the given list in reverse order.
-  //.
-  //. ```javascript
-  //. > S.reverse([1, 2, 3])
-  //. [3, 2, 1]
-  //.
-  //. > S.reverse('abc')
-  //. 'cba'
-  //. ```
-  function reverse(xs) {
-    return $.String._test(xs) ? xs.split('').reverse().join('')
-                              : xs.slice().reverse();
-  }
-  S.reverse = def('reverse', {}, [List(a), List(a)], reverse);
-
   //. ### Array
 
   //# append :: (Applicative f, Semigroup (f a)) => a -> f a -> f a
@@ -3576,6 +3573,34 @@
       [$.Function([a, a, $.Boolean]), $.Array(a), $.Array($.Array(a))],
       groupBy_);
 
+  //# reverse :: (Applicative f, Foldable f, Monoid (f a)) => f a -> f a
+  //.
+  //. Reverses the elements of the given structure.
+  //.
+  //. ```javascript
+  //. > S.reverse([1, 2, 3])
+  //. [3, 2, 1]
+  //.
+  //. > S.reverse(Cons(1, Cons(2, Cons(3, Nil))))
+  //. Cons(3, Cons(2, Cons(1, Nil)))
+  //.
+  //. > S.pipe([S.splitOn(''), S.reverse, S.joinWith('')], 'abc')
+  //. 'cba'
+  //. ```
+  function reverse(foldable) {
+    //  Fast path for arrays.
+    if (Array.isArray(foldable)) return foldable.slice().reverse();
+    var F = foldable.constructor;
+    return Z.reduce(function(xs, x) { return Z.concat(Z.of(F, x), xs); },
+                    Z.empty(F),
+                    foldable);
+  }
+  S.reverse =
+  def('reverse',
+      {f: [Z.Applicative, Z.Foldable, Z.Monoid]},
+      [f(a), f(a)],
+      reverse);
+
   //# sort :: (Ord a, Applicative m, Foldable m, Monoid (m a)) => m a -> m a
   //.
   //. Performs a [stable sort][] of the elements of the given structure, using


Test patch:
diff --git a/test/internal/List.js b/test/internal/List.js
new file mode 100644
index 0000000..c087dfc
--- /dev/null
+++ b/test/internal/List.js
@@ -0,0 +1,105 @@
+'use strict';
+
+var FL = require('fantasy-land');
+var $ = require('sanctuary-def');
+var Z = require('sanctuary-type-classes');
+var type = require('sanctuary-type-identifiers');
+
+var curry2 = require('./curry2');
+var eq = require('./eq');
+
+
+var List = {prototype: _List.prototype};
+
+List.prototype.constructor = List;
+
+function _List(tag, head, tail) {
+  this.isCons = tag === 'Cons';
+  this.isNil = tag === 'Nil';
+  if (this.isCons) {
+    this.head = head;
+    this.tail = tail;
+  }
+}
+
+List['@@type'] = 'sanctuary/List';
+
+//  Type :: Type -> Type
+List.Type = $.UnaryType(
+  List['@@type'],
+  '',
+  function(x) { return type(x) === List['@@type']; },
+  function(list) {
+    return Z.reduce(function(xs, x) { xs.push(x); return xs; }, [], list);
+  }
+);
+
+//  Nil :: List a
+var Nil = List.Nil = new _List('Nil');
+
+//  Cons :: (a, List a) -> List a
+var Cons = List.Cons = function Cons(head, tail) {
+  eq(arguments.length, Cons.length);
+  return new _List('Cons', head, tail);
+};
+
+List[FL.empty] = function() { return Nil; };
+
+List[FL.of] = function(x) { return Cons(x, Nil); };
+
+List[FL.zero] = List[FL.empty];
+
+List.prototype[FL.equals] = function(other) {
+  return this.isNil ?
+    other.isNil :
+    other.isCons &&
+      Z.equals(other.head, this.head) &&
+      Z.equals(other.tail, this.tail);
+};
+
+List.prototype[FL.concat] = function(other) {
+  return this.isNil ?
+    other :
+    Cons(this.head, Z.concat(this.tail, other));
+};
+
+List.prototype[FL.map] = function(f) {
+  return this.isNil ?
+    Nil :
+    Cons(f(this.head), Z.map(f, this.tail));
+};
+
+List.prototype[FL.ap] = function(other) {
+  return this.isNil || other.isNil ?
+    Nil :
+    Z.concat(Z.map(other.head, this), Z.ap(other.tail, this));
+};
+
+List.prototype[FL.chain] = function(f) {
+  return this.isNil ?
+    Nil :
+    Z.concat(f(this.head), Z.chain(f, this.tail));
+};
+
+List.prototype[FL.alt] = List.prototype[FL.concat];
+
+List.prototype[FL.reduce] = function(f, x) {
+  return this.isNil ?
+    x :
+    Z.reduce(f, f(x, this.head), this.tail);
+};
+
+List.prototype[FL.traverse] = function(typeRep, f) {
+  return this.isNil ?
+    Z.of(typeRep, Nil) :
+    Z.ap(Z.map(curry2(Cons), f(this.head)), Z.traverse(typeRep, f, this.tail));
+};
+
+List.prototype.inspect =
+List.prototype.toString = function() {
+  return this.isNil ?
+    'Nil' :
+    'Cons(' + Z.toString(this.head) + ', ' + Z.toString(this.tail) + ')';
+};
+
+module.exports = List;
diff --git a/test/internal/sanctuary.js b/test/internal/sanctuary.js
index e4e1549..8923cd1 100644
--- a/test/internal/sanctuary.js
+++ b/test/internal/sanctuary.js
@@ -5,6 +5,8 @@ var type = require('sanctuary-type-identifiers');
 
 var S = require('../..');
 
+var List = require('./List');
+
 
 //  UnaryType :: String -> Type
 function UnaryType(typeIdent) {
@@ -27,6 +29,7 @@ var UselessType = $.NullaryType(
 var env = S.env.concat([
   UnaryType('sanctuary/Compose'),
   UnaryType('sanctuary/Identity'),
+  List.Type($.Unknown),
   UselessType
 ]);
 
diff --git a/test/reverse.js b/test/reverse.js
index 4d63cbb..b8bb9b3 100644
--- a/test/reverse.js
+++ b/test/reverse.js
@@ -1,21 +1,29 @@
 'use strict';
 
-var S = require('..');
+var S = require('./internal/sanctuary');
 
+var List = require('./internal/List');
 var eq = require('./internal/eq');
 
 
+var Cons = List.Cons;
+var Nil = List.Nil;
+
+
 test('reverse', function() {
 
   eq(typeof S.reverse, 'function');
   eq(S.reverse.length, 1);
-  eq(S.reverse.toString(), 'reverse :: List a -> List a');
+  eq(S.reverse.toString(), 'reverse :: (Applicative f, Foldable f, Monoid f) => f a -> f a');
 
   eq(S.reverse([]), []);
+  eq(S.reverse([1]), [1]);
+  eq(S.reverse([1, 2]), [2, 1]);
   eq(S.reverse([1, 2, 3]), [3, 2, 1]);
-  eq(S.reverse(['1', '2', '3']), ['3', '2', '1']);
 
-  eq(S.reverse(''), '');
-  eq(S.reverse('123'), '321');
+  eq(S.reverse(Nil), Nil);
+  eq(S.reverse(Cons(1, Nil)), Cons(1, Nil));
+  eq(S.reverse(Cons(1, Cons(2, Nil))), Cons(2, Cons(1, Nil)));
+  eq(S.reverse(Cons(1, Cons(2, Cons(3, Nil)))), Cons(3, Cons(2, Cons(1, Nil))));
 
 });