Repository: sanctuary-js/sanctuary (js)

Issue:
Update head and tail to work for all Foldable
Currently Head and Tail only work for `Array`, but it is possible to implement them for all `Foldable`.

Pull request description:
array: generalize S.head, S.last, S.tail, and S.init
Closes #570

Initial attempt to generalize head, tail, last and init functions for Foldables.


Patch:
diff --git a/index.js b/index.js
index 8e370d5..36e271f 100644
--- a/index.js
+++ b/index.js
@@ -3063,10 +3063,10 @@
     impl: at
   };
 
-  //# head :: Array a -> Maybe a
+  //# head :: Foldable f => f a -> Maybe a
   //.
-  //. Returns Just the first element of the given array if the array contains
-  //. at least one element; Nothing otherwise.
+  //. Returns Just the first element of the given structure if the structure
+  //. contains at least one element; Nothing otherwise.
   //.
   //. ```javascript
   //. > S.head ([1, 2, 3])
@@ -3074,17 +3074,32 @@
   //.
   //. > S.head ([])
   //. Nothing
+  //.
+  //. > S.head (Cons (1) (Cons (2) (Cons (3) (Nil))))
+  //. Just (1)
+  //.
+  //. > S.head (Nil)
+  //. Nothing
   //. ```
+  function head(foldable) {
+    //  Fast path for arrays.
+    if (Array.isArray (foldable)) {
+      return foldable.length > 0 ? Just (foldable[0]) : Nothing;
+    }
+    return Z.reduce (function(m, x) { return m.isJust ? m : Just (x); },
+                     Nothing,
+                     foldable);
+  }
   _.head = {
-    consts: {},
-    types: [$.Array (a), $Maybe (a)],
-    impl: array (Nothing) (B (K) (Just))
+    consts: {f: [Z.Foldable]},
+    types: [f (a), $Maybe (a)],
+    impl: head
   };
 
-  //# last :: Array a -> Maybe a
+  //# last :: Foldable f => f a -> Maybe a
   //.
-  //. Returns Just the last element of the given array if the array contains
-  //. at least one element; Nothing otherwise.
+  //. Returns Just the last element of the given structure if the structure
+  //. contains at least one element; Nothing otherwise.
   //.
   //. ```javascript
   //. > S.last ([1, 2, 3])
@@ -3092,20 +3107,31 @@
   //.
   //. > S.last ([])
   //. Nothing
+  //.
+  //. > S.last (Cons (1) (Cons (2) (Cons (3) (Nil))))
+  //. Just (3)
+  //.
+  //. > S.last (Nil)
+  //. Nothing
   //. ```
-  function last(xs) {
-    return xs.length > 0 ? Just (xs[xs.length - 1]) : Nothing;
+  function last(foldable) {
+    //  Fast path for arrays.
+    if (Array.isArray (foldable)) {
+      return foldable.length > 0 ? Just (foldable[foldable.length - 1])
+                                 : Nothing;
+    }
+    return Z.reduce (function(_, x) { return Just (x); }, Nothing, foldable);
   }
   _.last = {
-    consts: {},
-    types: [$.Array (a), $Maybe (a)],
+    consts: {f: [Z.Foldable]},
+    types: [f (a), $Maybe (a)],
     impl: last
   };
 
-  //# tail :: Array a -> Maybe (Array a)
+  //# tail :: (Applicative f, Foldable f, Monoid (f a)) => f a -> Maybe (f a)
   //.
-  //. Returns Just all but the first of the given array's elements if the
-  //. array contains at least one element; Nothing otherwise.
+  //. Returns Just all but the first of the given structure's elements if the
+  //. structure contains at least one element; Nothing otherwise.
   //.
   //. ```javascript
   //. > S.tail ([1, 2, 3])
@@ -3113,17 +3139,33 @@
   //.
   //. > S.tail ([])
   //. Nothing
+  //.
+  //. > S.tail (Cons (1) (Cons (2) (Cons (3) (Nil))))
+  //. Just (Cons (2) (Cons (3) (Nil)))
+  //
+  //. > S.tail (Nil)
+  //. Nothing
   //. ```
+  function tail(foldable) {
+    //  Fast path for arrays.
+    if (Array.isArray (foldable)) {
+      return foldable.length > 0 ? Just (foldable.slice (1)) : Nothing;
+    }
+    var empty = Z.empty (foldable.constructor);
+    return Z.reduce (function(m, x) {
+      return Just (maybe (empty) (append (x)) (m));
+    }, Nothing, foldable);
+  }
   _.tail = {
-    consts: {},
-    types: [$.Array (a), $Maybe ($.Array (a))],
-    impl: array (Nothing) (K (Just))
+    consts: {f: [Z.Applicative, Z.Foldable, Z.Monoid]},
+    types: [f (a), $Maybe (f (a))],
+    impl: tail
   };
 
-  //# init :: Array a -> Maybe (Array a)
+  //# init :: (Applicative f, Foldable f, Monoid (f a)) => f a -> Maybe (f a)
   //.
-  //. Returns Just all but the last of the given array's elements if the
-  //. array contains at least one element; Nothing otherwise.
+  //. Returns Just all but the last of the given structure's elements if the
+  //. structure contains at least one element; Nothing otherwise.
   //.
   //. ```javascript
   //. > S.init ([1, 2, 3])
@@ -3131,13 +3173,26 @@
   //.
   //. > S.init ([])
   //. Nothing
+  //.
+  //. > S.init (Cons (1) (Cons (2) (Cons (3) (Nil))))
+  //. Just (Cons (1) (Cons (2) (Nil)))
+  //.
+  //. > S.init (Nil)
+  //. Nothing
   //. ```
-  function init(xs) {
-    return xs.length > 0 ? Just (xs.slice (0, -1)) : Nothing;
+  function init(foldable) {
+    //  Fast path for arrays.
+    if (Array.isArray (foldable)) {
+      return foldable.length > 0 ? Just (foldable.slice (0, -1)) : Nothing;
+    }
+    var empty = Z.empty (foldable.constructor);
+    return Z.map (Pair.snd, Z.reduce (function(m, x) {
+      return Just (Pair (x) (maybe (empty) (pair (append)) (m)));
+    }, Nothing, foldable));
   }
   _.init = {
-    consts: {},
-    types: [$.Array (a), $Maybe ($.Array (a))],
+    consts: {f: [Z.Applicative, Z.Foldable, Z.Monoid]},
+    types: [f (a), $Maybe (f (a))],
     impl: init
   };
 
@@ -3379,10 +3434,15 @@
   //. > S.append ([3]) (S.Just ([1, 2]))
   //. Just ([1, 2, 3])
   //. ```
+  function append(x) {
+    return function(xs) {
+      return Z.append (x, xs);
+    };
+  }
   _.append = {
     consts: {f: [Z.Applicative, Z.Semigroup]},
     types: [a, f (a), f (a)],
-    impl: curry2 (Z.append)
+    impl: append
   };
 
   //# prepend :: (Applicative f, Semigroup (f a)) => a -> f a -> f a


Test patch:
diff --git a/test/head.js b/test/head.js
index 3aa3bbb..4a13912 100644
--- a/test/head.js
+++ b/test/head.js
@@ -1,7 +1,8 @@
 'use strict';
 
-const S = require ('..');
+const S = require ('./internal/sanctuary');
 
+const {Nil, Cons} = require ('./internal/List');
 const eq = require ('./internal/eq');
 
 
@@ -9,11 +10,16 @@ test ('head', () => {
 
   eq (typeof S.head) ('function');
   eq (S.head.length) (1);
-  eq (S.show (S.head)) ('head :: Array a -> Maybe a');
+  eq (S.show (S.head)) ('head :: Foldable f => f a -> Maybe a');
 
   eq (S.head ([])) (S.Nothing);
   eq (S.head (['foo'])) (S.Just ('foo'));
   eq (S.head (['foo', 'bar'])) (S.Just ('foo'));
   eq (S.head (['foo', 'bar', 'baz'])) (S.Just ('foo'));
 
+  eq (S.head (Nil)) (S.Nothing);
+  eq (S.head (Cons ('foo') (Nil))) (S.Just ('foo'));
+  eq (S.head (Cons ('foo') (Cons ('bar') (Nil)))) (S.Just ('foo'));
+  eq (S.head (Cons ('foo') (Cons ('bar') (Cons ('baz') (Nil))))) (S.Just ('foo'));
+
 });
diff --git a/test/init.js b/test/init.js
index 5817365..f9eed2c 100644
--- a/test/init.js
+++ b/test/init.js
@@ -1,7 +1,8 @@
 'use strict';
 
-const S = require ('..');
+const S = require ('./internal/sanctuary');
 
+const {Nil, Cons} = require ('./internal/List');
 const eq = require ('./internal/eq');
 
 
@@ -9,11 +10,16 @@ test ('init', () => {
 
   eq (typeof S.init) ('function');
   eq (S.init.length) (1);
-  eq (S.show (S.init)) ('init :: Array a -> Maybe (Array a)');
+  eq (S.show (S.init)) ('init :: (Applicative f, Foldable f, Monoid f) => f a -> Maybe (f a)');
 
   eq (S.init ([])) (S.Nothing);
   eq (S.init (['foo'])) (S.Just ([]));
   eq (S.init (['foo', 'bar'])) (S.Just (['foo']));
   eq (S.init (['foo', 'bar', 'baz'])) (S.Just (['foo', 'bar']));
 
+  eq (S.init (Nil)) (S.Nothing);
+  eq (S.init (Cons ('foo') (Nil))) (S.Just (Nil));
+  eq (S.init (Cons ('foo') (Cons ('bar') (Nil)))) (S.Just (Cons ('foo') (Nil)));
+  eq (S.init (Cons ('foo') (Cons ('bar') (Cons ('baz') (Nil))))) (S.Just (Cons ('foo') (Cons ('bar') (Nil))));
+
 });
diff --git a/test/last.js b/test/last.js
index 03ea7c2..1d03697 100644
--- a/test/last.js
+++ b/test/last.js
@@ -1,7 +1,8 @@
 'use strict';
 
-const S = require ('..');
+const S = require ('./internal/sanctuary');
 
+const {Nil, Cons} = require ('./internal/List');
 const eq = require ('./internal/eq');
 
 
@@ -9,11 +10,16 @@ test ('last', () => {
 
   eq (typeof S.last) ('function');
   eq (S.last.length) (1);
-  eq (S.show (S.last)) ('last :: Array a -> Maybe a');
+  eq (S.show (S.last)) ('last :: Foldable f => f a -> Maybe a');
 
   eq (S.last ([])) (S.Nothing);
   eq (S.last (['foo'])) (S.Just ('foo'));
   eq (S.last (['foo', 'bar'])) (S.Just ('bar'));
   eq (S.last (['foo', 'bar', 'baz'])) (S.Just ('baz'));
 
+  eq (S.last (Nil)) (S.Nothing);
+  eq (S.last (Cons ('foo') (Nil))) (S.Just ('foo'));
+  eq (S.last (Cons ('foo') (Cons ('bar') (Nil)))) (S.Just ('bar'));
+  eq (S.last (Cons ('foo') (Cons ('bar') (Cons ('baz') (Nil))))) (S.Just ('baz'));
+
 });
diff --git a/test/tail.js b/test/tail.js
index 29fcb74..e46b8cb 100644
--- a/test/tail.js
+++ b/test/tail.js
@@ -1,7 +1,8 @@
 'use strict';
 
-const S = require ('..');
+const S = require ('./internal/sanctuary');
 
+const {Nil, Cons} = require ('./internal/List');
 const eq = require ('./internal/eq');
 
 
@@ -9,11 +10,16 @@ test ('tail', () => {
 
   eq (typeof S.tail) ('function');
   eq (S.tail.length) (1);
-  eq (S.show (S.tail)) ('tail :: Array a -> Maybe (Array a)');
+  eq (S.show (S.tail)) ('tail :: (Applicative f, Foldable f, Monoid f) => f a -> Maybe (f a)');
 
   eq (S.tail ([])) (S.Nothing);
   eq (S.tail (['foo'])) (S.Just ([]));
   eq (S.tail (['foo', 'bar'])) (S.Just (['bar']));
   eq (S.tail (['foo', 'bar', 'baz'])) (S.Just (['bar', 'baz']));
 
+  eq (S.tail (Nil)) (S.Nothing);
+  eq (S.tail (Cons ('foo') (Nil))) (S.Just (Nil));
+  eq (S.tail (Cons ('foo') (Cons ('bar') (Nil)))) (S.Just (Cons ('bar') (Nil)));
+  eq (S.tail (Cons ('foo') (Cons ('bar') (Cons ('baz') (Nil))))) (S.Just (Cons ('bar') (Cons ('baz') (Nil))));
+
 });