I'll assume that you are calling union/3 with the first and second arguments instantiated. The third argument can be either uninstantiated upon calling and will be unified at return with the union of the two lists or if it's already instantiated it can be used to check whether it matches the (ordered) union of the first two lists.
The first clause states that if the second argument is the empty list and the first list has at least one element then the union is just this first list.
Likewise, the second clause states that if the first argument is the empty list and the second list has at least one element then the union is just this second list.
The third clause recurses over the first list and checks the second list to see if the item is already there. In that case it just calls itself with the tail of the first list.
The fourth clause tests the head of the first list to check that it is not contained in the second list and calls recursively with the tail (just like the third clause). However upon return of recursion it adds the item to the head of the third list, thus adding the item to the union.
Note that in your implementation, the union of two empty sets will always fail. You can fix this by modifying the first or second clause to allow an empty list, or the add another clause for this case. E.g.
union([],[],[]).
Now let's see what happens when we call union([1,2],[2,3], Result)
:
The first two clauses will fail to match as neither of them are the empty list.
We enter the third clause and check to see that element 1 is not a member of the second list thus failing there.
We try now the fourth clause and test that element 1 i not in the second list, so we call union([2], [2,3], Result)
, we mark this execution point (*1).
Again the two first clauses will fail to match, so we enter the third clause. Here we test that indeed element 2 is contained in the second list so we call union([], [2,3], Result)
, we mark this execution point (*2)
Now the first clause fails as the first argument is the empty list.
We now enter the second clause unifying the third argument with the second list ([2,3]).
At this point we return to (*2) where now Result gets instantiated with [2,3]. This clauses end there so we bound the third argument with [1,2,3], and we return to (*1).
We are now at (*1) where Result and therefore the third argument gets instantiated with [1,2,3].
This gives us the first result [1,2,3].
However a choice point was left when we succedded in (*2), so if we ask Prolog to search for another answer it still has to try the fourth clause of union([2],[2,3], Result).
So we enter the fourth clause to test if 2 is not a member of [2,3] which fails, so Prolog will tell us that there are no other answers.