Help me understand if foreach/2 has a bug, or if I'm confused

?- length(L, 3), foreach(nth1(N,L,N), nth1(N,L,N)).
L = [_, _, _].

% it seems the second foreach argument doesn't bind variables ... but

?- length(L, 3), length(K, 3), foreach(nth1(N,L,N), nth1(N,K,N)).
L = [_, _, _],
K = [1, 2, 3].

% oh .. it does bind variables.

Why doesn’t the first example yield
L = [1, 2, 3]
?

I agree. I understand why.

Again I understand and I agree.

Here things are murky. This can’t be backtracking (as I understand), because we aren’t undoing the bindings we repeatedly update in the second term. So it’s some other “undoing”.
But it’s not even “undo what we did here”, which would work, it’s more like “reset” this variable.
But wherever that happens, couldn’t we copy_term the updated variable, reset the local one, then unify it with the copied term to undo what the first term did, but not the second?

Not sure if you want to go through it all, but “does foreach/2 have a bug” is not a new question :smiley:

See here: Foreach bug?

I am quite sure there are also older discussions on the same topic.

The docs say,

The actual implementation uses findall/3 on a template created from the variables shared between Generator and Goal. Subsequently, it uses every instance of this template to instantiate Goal, call Goal and undo only the instantiation of the template and not other instantiations created by running Goal.

You can see this “variables shared” if you try these queries:

?- length(L, 3), length(K, 3), L = K, foreach(nth1(N,L,N),nth1(N,K,N)).
L = K, K = [_, _, _].

?- length(L, 3), length(K, 3), foreach(nth1(N,L,N),nth1(N,K,N)).
L = [_, _, _],
K = [1, 2, 3].

?- length(L, 3), length(K, 3), foreach(nth1(N,L,N),nth1(N,K,N)), L = K.
L = K, K = [1, 2, 3].
3 Likes