If you're looking for a tail-recursive solution, one approach is to use the named let:
(define (group n xs)
(let loop ([grouped '()] [xs xs])
(if (empty? xs)
(reverse grouped)
(loop (cons (take xs n) grouped)
(drop xs n)))))
However, this fails if xs
would have leftover elements, so we'll need to add a case that checks for this:
(define (group n xs)
(let loop ([grouped '()] [xs xs])
(cond
[(empty? xs)
(reverse grouped)]
[(<= (length xs) n)
(loop (cons xs grouped) '())]
[else (loop (cons (take xs n) grouped)
(drop xs n))])))
This works, but we can do better. The problem here is that calculating (length xs)
runs in linear time, because the only way to find the length of a simple list is to walk the entire list. Since this is inside a loop that is run a number of times proportional to the size of xs
, this code runs in quadratic time when it should be simple to accomplish it in linear time. We can circumvent this problem by calculating the length of xs
once and then subtracting n
from it each iteration:
(define (group n xs)
(let loop ([grouped '()] [xs xs] [l (length xs)])
(cond
[(empty? xs)
(reverse grouped)]
[(<= l n)
(loop (cons xs grouped) '() 0)]
[else (loop (cons (take xs n) grouped)
(drop xs n)
(- l n))])))
And we're back to linear time while still preserving tail recursion, and therefore constant space. There is another improvement we can make however. The Racket function split-at
combines the functionality of take
and drop
and returns both lists as two values. To use multiple-value-returning functions, we can use let-values
:
(define (group n xs)
(let loop ([grouped '()] [xs xs] [l (length xs])
(cond
[(empty? xs)
(reverse grouped)]
[(<= l n)
(loop (cons xs grouped) '() 0)]
[else (let-values ([(taken dropped) (split-at xs n)])
(loop (cons taken grouped)
dropped
(- l n)))])))
This is slightly faster because split-at
can avoid the repeat work of ignoring the first n
elements in the drop
portion of it's functionality because those elements will already be consumed by take
. This code does however not account for bad user input. If a user supplies an n
larger than the length of xs
, it will throw an error when it should return (list xs)
. This is simple enough to check for, but our code is getting awfully extended towards the right with all this nesting. In addition to checking for this, we can split the loop off into an internally defined function:
(define (group n xs)
(define (loop grouped xs l)
(cond
[(empty? xs)
(reverse grouped)]
[(<= l n)
(loop (cons xs grouped) '() 0)]
[else (let-values ([(taken dropped) (split-at xs n)])
(loop (cons taken grouped)
dropped
(- l n)))]))
(let ([l (length xs)])
(if (>= n l)
(list xs)
(loop '() xs l))))
This function is tail recursive, does not calculate (length xs)
more than once, ensures that (group 4 '(1 2 3))
evaluates to '((1 2 3))
, and uneven grouping such that (group 2 '(1 2 3)
evaluates to '((1 2) (3))
, running in linear time and constant space.