3

Consider the code below:

foo = list("First List", 1, 2, 3)
bar = function(x) {
    cat("The list name is:", x[[1]], "\nThe items are:\n")
    for (i in 2:length(x))
        cat(x[[i]], "\n")
}
bar(foo)

The result will be:

The list name is: First List 
The items are:
1 
2 
3 

Now consider passing a list with no items, but a name:

baz = list("Second List")
bar(baz)

The result would be:

The list name is: Second List 
The items are:
Error in x[[i]] : subscript out of bounds

The error is because 2:length(x) will produce a sequence of c(2, 1) for the latter case bar(baz), so it tries to access baz[2] and it does not exist.

How to simply prevent this unwanted reverse iteration in a for loop in R?

Ali
  • 9,440
  • 12
  • 62
  • 92
  • Simply put the loop in an `if (length(x)>1)` field? – Sacha Epskamp Oct 19 '12 at 11:11
  • @SachaEpskamp While it is done simply by a single for in C++, I don't think adding an "if" is a good solution in R. – Ali Oct 19 '12 at 11:24
  • 1
    @AliSharifi Sorry but I do have to comment here about the Accept. Your Question is clearly about stopping the loop operating on a decrementing vector. As good as Flodel's Answer is, it is not the answer to the generic question you pose. It won't protect you from the pathological example Carl shows in his comment. Whilst you are free to accept whichever Answer you want, I can't not comment that the solution Andrie shows using `seq_along()` (or `seq_len()`) is the R way of answering your general question. (I'll stop now before this sounds even more of an "Andrie love-in" than it already is!) – Gavin Simpson Oct 19 '12 at 11:32
  • @GavinSimpson I enjoyed both answers by Andrie and flodel and I wished in several of my questions to be able to accept several answers, I upvoted both. However I enjoyed flodel's answer a bit more, because it is more simply doing the job, not needing a re-indexing in the body of the for loop. Also I did not get the point of Carl's example. – Ali Oct 19 '12 at 11:52
  • Ali, maybe your data structure is not the best to work with (obviously, you are having issues); I'd suggest you consider a nested list instead: `foo <- list("First List" = list(1,2,3))`. – flodel Oct 19 '12 at 12:03
  • @flodel I used this structure to provide an example. Think about C++ for loop that you can easily write for(i=2; i < length(x); i++). I wanted an easy solution, as yours, to do the same in R. – Ali Oct 19 '12 at 12:07
  • I also don't get Carl's pathological example, can someone explain how my answer or Andrie's would treat it differently? – flodel Oct 19 '12 at 12:13
  • @flodel Sorry, I should not have referred to that pathology. The only difference between yours and Andrie's Answers is that you iterate on the specific set of values, Andrie's iterates on the indices of those values. My comments have been more to do with what the Question here was rather than the relative merits of the Answers. As now stated, yours is the simpler solution to the Question. Andrie's is a better solution to the more generic question that the OP raised initially. – Gavin Simpson Oct 19 '12 at 12:54
  • @GavinSimpson But for sure the Andrie answer will be under attention by the upvotes it has received – Ali Oct 19 '12 at 13:10

3 Answers3

9

This is what seq_along helps with:

bar <- function(x) {
  cat("The list name is:", x[[1]], "\nThe items are:\n")
  for (i in seq_along(x[-1])) cat(x[[i+1]], "\n")   ### Edit ###
}

The results:

bar(foo)
The list name is: First List 
The items are:
First List 
1 
2 
3 

bar(baz)
The list name is: Second List 
The items are:
Second List 

Of course, it is better to not use a for loop at all, but lapply or family:

bar <- function(x) {
  cat("The list name is:", x[[1]], "\nThe items are:\n")
  lapply(x[-1],  function(xx)cat(xx, "\n"))
  invisible(NULL)
}

bar(foo)
The list name is: First List 
The items are:
1 
2 
3 

bar(baz)
The list name is: Second List 
The items are:
Andrie
  • 176,377
  • 47
  • 447
  • 496
  • +1 This is the defensive way to write the loop. Also worth noting the `seq_len()` function for doing things like `for(i in 1:N)` where `N` is some previously derived number of iterations to perform. This is helpful in cases where the `seq_along()` is not usable because there is nothing to go `along`. – Gavin Simpson Oct 19 '12 at 11:13
  • Thanks, but I am not looking to iterate over the whole list. I want to start from 2nd item of the list, and at the same time prevent reverse-iteration. – Ali Oct 19 '12 at 11:14
  • 2
    Replace `seq_along(x)` with `seq_along(x[-1])` and `x[[i]]` with `x[[i+1]]`. – flodel Oct 19 '12 at 11:17
  • 1
    Boo! - there is no reason to use `lapply()` here other than to save the microseconds that will come from `lapply()` doing it's loop in compiled code - which is much shorter than the time spent realising you need the `invisible()` call and writing the anonymous function ;-). The `for()` is neater here (IMNSHO) – Gavin Simpson Oct 19 '12 at 11:21
  • Hey, @GavinSimpson , was that a "We hate the Yankees" type of "Boo!" or a "It's almost Halloween" type? :-) – Carl Witthoft Oct 19 '12 at 11:49
  • @GavinSimpson It's horses for courses. Yes, of course I have to return NULL, but I think the `lapply` is easier to read, and there is less book-keeping to do. – Andrie Oct 19 '12 at 12:22
  • 1
    FYI, your first loop is going to create n copies of x. – hadley Oct 19 '12 at 12:27
  • @hadley as a side effect of repeatedly calling `x[-1]`? – Gavin Simpson Oct 19 '12 at 13:21
  • 1
    Yes, that's why `x[[i+1]]` would be better in that regards, although `x[-1][[i]]` is more readable from the local context. – flodel Oct 19 '12 at 13:26
  • 1
    @GavinSimpson yes. @Andrie you still make one copy ;). But I think the code would be more clear if you did `y <- x[-1]` – hadley Oct 19 '12 at 13:35
4

No need to loop over the list indices, you can just loop over a sub-list:

> bar = function(x) {
+     cat("The list name is:", x[[1]], "\nThe items are:\n")
+     for (i in x[-1])
+         cat(i, "\n")
+ }

If there is a single item in your list, the sub-list will be empty and the for loop will be skipped.

Edit: As GavinSimpson points out, this works well because your particular case did not really need to loop over indices. If indices were absolutely needed, then you would have to loop over seq_along(x[-1]) instead of x[-1] as Andrie showed.

flodel
  • 87,577
  • 21
  • 185
  • 223
  • Good answer to the specific case shown, but doesn't solve the more generic point of stopping a `for()` from even iterating through values `c(2,1)`. – Gavin Simpson Oct 19 '12 at 11:15
  • Cleaner than my answer, but I'd still recommend validation checks, in case some wiseguy uses `x<-c('hello','world', 1:5)` – Carl Witthoft Oct 19 '12 at 11:15
  • @GavinSimpson, I am not sure about your comment. The key in avoiding unwanted loops is that `for` can (and here should) be passed something empty. `2:length(x)` will never be empty, but both `x[-1]` and `seq_along(x[-1])` might be. – flodel Oct 19 '12 at 11:37
  • Sorry, I should have been more specific. Your code works and is a good Answer (I +1'd it). But it isn't a generic solution to the general point about having `i` take only values `2` in the OP's case. Yours is another way to solve the specific example but doesn't address the issue of getting `i` to be the required index - it makes `i` take the values of the object in turn. I guess it depends on whether the OP was interested in the generic issue & illustrated it via this example or interested in this specific example. The edit makes me think the former was wanted, but I may be wrong...!? – Gavin Simpson Oct 19 '12 at 11:48
  • @GavinSimpson You are right, but what to do? I prefer to solve my problem using the flodel's answer because it's easier and faster to develop. Even more generic answer to the problem is Andrie's. In this case I need really a double accept that is not permitted in SO. So what to do? :) – Ali Oct 19 '12 at 12:04
  • You could edit your question slightly to remove the bit about C/C++ which makes this look like you want a generic solution, then Accept Flodel's Answer. If you want to leave the Question as is, then that is fine too - you can Accept what you want. I only wanted to leave a comment for future readers of this Q that they should seek a generic solution via Andrie's Answer and not overlook it in favour of the Accepted one. The main thing is not to change the Q such that the other Answers are no longer Answers to the new Q. In the end think about what is best for [so], the site, not your situation. – Gavin Simpson Oct 19 '12 at 12:12
2

I think the correct answer is: do input validation on your functions. Rather than creating spaghetti code to "work around" this behavior, test your input variable for length, or check typeof for each element, etc.

In your example case, a simple if (length(x)<2) #skip the loop section of code would suffice.

Carl Witthoft
  • 20,573
  • 9
  • 43
  • 73
  • I was going to add this to @SachaEpskamp comment. There is no need to introduce these additional checks if you write the `for()` loop constructor in a defensive fashion using `seq_along()` or `seq_len()` as Andrie shows in his Answer. That way you don;t incur all the calls to `if()` and evaluations of the `condition`. – Gavin Simpson Oct 19 '12 at 11:17
  • Thanks. But in C, we can simply do the same thing with a for loop not to combine it with if. I am looking for such a solution in R – Ali Oct 19 '12 at 11:17
  • @AliSharifi then the idiom that Andrie shows is the correct one to use here. – Gavin Simpson Oct 19 '12 at 11:19