4

I'm trying to get all the rotations of the list v. So, in the definition of rotations, I use the flipped version of rotateLeft as the first branching function (in order to accept the list first) and then a function that returns the list [0, 1, 2, ..., v.length-1], with map as the converging function.

const {curry,mathMod,pipe,splitAt,reverse,unnest,converge,map,flip,length,times,identity} = require("ramda");

const v = [1,2,3,4];

const rotateLeft = curry((n,vet) => {
    const i = mathMod(n,vet.length);
    return pipe(
        splitAt(i),
        reverse,
        unnest
    )(vet);
});

const rotations = converge(map,[
    flip(rotateLeft),
    pipe(length,times(identity))
]);

rotations(v);

However, this doesn't return what I expected. Instead, it works fine if I rewrite that as follows:

map(flip(rotateLeft)(v),
    pipe(length,times(identity))(v));

// gives [[1,2,3,4],[2,3,4,1],[3,4,1,2],[4,1,2,3]]

As I understand, converge applies the two branching functions to v and then feeds the results as arguments to map. Is that right? So why doesn't rotations(v) return the same thing?

Code

More concise version using reduce

Inspired by your versions in vanilla JS, I've come up with the following reduceRotations function, which doesn't use the index parameter of map or recursion in an obvious way. Then, of course, I translated that into vanilla Ramda, in a totally point-free fashion. :)

const {converge,reduce,always,append,pipe,last,head,tail,identity,unapply} = require("ramda");

const reduceRotations = (v) => {
    const rotate = (v) => append(head(v),tail(v));
    return reduce(
        (acc,_) => append(rotate(last(acc)),acc),
        [v],
        tail(v)
    );
};

const pointFreeRotations = 
    converge(reduce,[
        always(converge(append,[
            pipe(last,converge(append,[head,tail])),
            identity
        ])),
        unapply(identity),
        tail
    ]);

Code

Yet another one

The following equivalent functions employ scan instead of reduce.

const {converge,scan,always,append,head,tail,identity} = require("ramda");

const scanRotations = (v) => {
    const rotate = (v) => append(head(v),tail(v));
    return scan(rotate,v,tail(v));
};

const scanPointFreeRotations =
    converge(scan,[
        always(converge(append,[head,tail])),
        identity,
        tail
    ]);

Code

Louis Sakh
  • 43
  • 4
  • 1
    Perhaps even cleaner than `const rotate = (v) => append(head(v), tail(v))` is the point-free version: `const rotate = lift(append)(head, tail)`. (The alternative `converge(append, [head, tail])` feels less elegant, and is definitely less standard.) – Scott Sauyet May 23 '19 at 17:16

2 Answers2

4

This is because converge takes as its arity the arity of the longest function supplied to it.

So, since flip(rotateLeft).length //=> 2 and pipe(length,times(identity)) //=> 1, rotations will have length 2. But you clearly want a unary function. The easiest way to fix it is to simply wrap flip(rotateLeft) inside unary:

const rotateLeft = curry((n,vet) => {
    const i = mathMod(n,vet.length);
    return pipe(
        splitAt(i),
        reverse,
        unnest
    )(vet);
});

const rotations = converge (map, [
  unary ( flip (rotateLeft) ),
  pipe ( length, times(identity) )
])

const v = [1,2,3,4];

console .log (
  rotations (v)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script><script>
const {curry, mathMod, pipe, splitAt, reverse, unnest, converge, map, unary, flip, length, times, identity} = R   </script>

Also note that this doesn't really require all of Ramda's machinery. It's pretty easy to do this in vanilla JS:

const rotations = v => v .map ( (_, i) => v .slice (i) .concat ( v .slice(0, i) ) )
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
3

As proof by this simple recursive implementation of rotations, sometimes point-free code composed of tons of tiny functions just isn't worth the added headache -

const rotations = ([ x, ...xs ], count = 0) =>
  count > xs.length
    ? []
    : [ [ x, ...xs ], ...rotations ([ ...xs, x ], count + 1) ]

console.log(rotations([1,2,3]))
// [ [ 1, 2, 3 ], [ 2, 3, 1 ], [ 3, 1, 2 ] ]

console.log(rotations([1,2,3,4]))
// [ [ 1, 2, 3, 4 ], [ 2, 3, 4, 1 ], [ 3, 4, 1, 2 ], [ 4, 1, 2, 3 ] ]

Above, destructuring assignment creates many intermediate values, but we can work around this using a slightly different algorithm -

const rotations = (xs = [], i = 0) =>
  i >= xs.length
    ? []
    : [ xs.slice(i).concat(xs.slice(0, i)) ].concat(rotations(xs, i + 1))

console.log(rotations([1,2,3]))
// [ [ 1, 2, 3 ], [ 2, 3, 1 ], [ 3, 1, 2 ] ]

console.log(rotations([1,2,3,4]))
// [ [ 1, 2, 3, 4 ], [ 2, 3, 4, 1 ], [ 3, 4, 1, 2 ], [ 4, 1, 2, 3 ] ]

Defining a helper function like you did is good hygiene. It also makes our other function more readable -

const rotate = (xs = []) =>
  xs.slice(1).concat(xs.slice(0, 1))

const rotations = (xs = [], i = 0) =>
  i >= xs.length
    ? []
    : [ xs ].concat(rotations(rotate(xs), i + 1))

console.log(rotations([1,2,3]))
// [ [ 1, 2, 3 ], [ 2, 3, 1 ], [ 3, 1, 2 ] ]

console.log(rotations([1,2,3,4]))
// [ [ 1, 2, 3, 4 ], [ 2, 3, 4, 1 ], [ 3, 4, 1, 2 ], [ 4, 1, 2, 3 ] ]
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Nice answer as always!. I was about to write a recursive version of this when I thought up my `map` answer, and then didn't bother. – Scott Sauyet May 23 '19 at 01:18
  • Well done! Nonetheless, my goal was to manipulate the previously defined ```rotateLeft```, with Ramda functions and point-free composition used as much as possible, mainly as an exercise. Otherwise, I would probably have written something very similar to what you did here... Check out my alternative version of ```rotations``` as well! – Louis Sakh May 23 '19 at 11:18