This is a question provoked by an already deleted answer to this question. The issue could be summarized as follows:
Is it possible to fold over a list, with the tail of the list generated while folding?
Here is what I mean. Say I want to calculate the factorial (this is a silly example but it is just for demonstration), and decide to do it like this:
fac_a(N, F) :-
must_be(nonneg, N),
( N =< 1
-> F = 1
; numlist(2, N, [H|T]),
foldl(multiplication, T, H, F)
).
multiplication(X, Y, Z) :-
Z is Y * X.
Here, I need to generate the list that I give to foldl
. However, I could do the same in constant memory (without generating the list and without using foldl
):
fac_b(N, F) :-
must_be(nonneg, N),
( N =< 1
-> F = 1
; fac_b_1(2, N, 2, F)
).
fac_b_1(X, N, Acc, F) :-
( X < N
-> succ(X, X1),
Acc1 is X1 * Acc,
fac_b_1(X1, N, Acc1, F)
; Acc = F
).
The point here is that unlike the solution that uses foldl
, this uses constant memory: no need for generating a list with all values!
Calculating a factorial is not the best example, but it is easier to follow for the stupidity that comes next.
Let's say that I am really afraid of loops (and recursion), and insist on calculating the factorial using a fold. I still would need a list, though. So here is what I might try:
fac_c(N, F) :-
must_be(nonneg, N),
( N =< 1
-> F = 1
; foldl(fac_foldl(N), [2|Back], 2-Back, F-[])
).
fac_foldl(N, X, Acc-Back, F-Rest) :-
( X < N
-> succ(X, X1),
F is Acc * X1,
Back = [X1|Rest]
; Acc = F,
Back = []
).
To my surprise, this works as intended. I can "seed" the fold with an initial value at the head of a partial list, and keep on adding the next element as I consume the current head. The definition of fac_foldl/4
is almost identical to the definition of fac_b_1/4
above: the only difference is that the state is maintained differently. My assumption here is that this should use constant memory: is that assumption wrong?
I know this is silly, but it could however be useful for folding over a list that cannot be known when the fold starts. In the original question we had to find a connected region, given a list of x-y coordinates. It is not enough to fold over the list of x-y coordinates once (you can however do it in two passes; note that there is at least one better way to do it, referenced in the same Wikipedia article, but this also uses multiple passes; altogether, the multiple-pass algorithms assume constant-time access to neighboring pixels!).
My own solution to the original "regions" question looks something like this:
set_region_rest([A|As], Region, Rest) :-
sort([A|As], [B|Bs]),
open_set_closed_rest([B], Bs, Region0, Rest),
sort(Region0, Region).
open_set_closed_rest([], Rest, [], Rest).
open_set_closed_rest([X-Y|As], Set, [X-Y|Closed0], Rest) :-
X0 is X-1, X1 is X + 1,
Y0 is Y-1, Y1 is Y + 1,
ord_intersection([X0-Y,X-Y0,X-Y1,X1-Y], Set, New, Set0),
append(New, As, Open),
open_set_closed_rest(Open, Set0, Closed0, Rest).
Using the same "technique" as above, we can twist this into a fold:
set_region_rest_foldl([A|As], Region, Rest) :-
sort([A|As], [B|Bs]),
foldl(region_foldl, [B|Back],
closed_rest(Region0, Bs)-Back,
closed_rest([], Rest)-[]),
!,
sort(Region0, Region).
region_foldl(X-Y,
closed_rest([X-Y|Closed0], Set)-Back,
closed_rest(Closed0, Set0)-Back0) :-
X0 is X-1, X1 is X + 1,
Y0 is Y-1, Y1 is Y + 1,
ord_intersection([X0-Y,X-Y0,X-Y1,X1-Y], Set, New, Set0),
append(New, Back0, Back).
This also "works". The fold leaves behind a choice point, because I haven't articulated the end condition as in fac_foldl/4
above, so I need a cut right after it (ugly).
The Questions
- Is there a clean way of closing the list and removing the cut? In the factorial example, we know when to stop because we have additional information; however, in the second example, how do we notice that the back of the list should be the empty list?
- Is there a hidden problem I am missing?
- This looks like its somehow similar to the Implicit State with DCGs, but I have to admit I never quite got how that works; are these connected?
must_be/2
andfoldl/4
. They aren't even de facto standard predicates. I would re-add theswi-prolog
tag but users that like to pretend otherwise would simply delete again. Politics instead of facts. Sad. – Stonymust_be/2
andfoldl/4
are SWI-Prolog specific :/ – Renayrenckensfoldl/4
is definitely not SWI-specific. It even appears in Richard O'Keefe's library proposal. Any beginner can implement it in any Prolog system. The swi-prolog tag should be reserved for questions that are clearly SWI-specific, so that users find these pertaining questions more easily. Tagging everything where a single predicate that is provided by SWI is used anywhere as "SWI" makes it impossible to find such instances. – Mccloskeyfoldl/4
is right there.must_be/2
, however, isn't. Is it in a standard? – Renayrenckensmust_be/2
is definitely not the final word on type checking. SICStus provides a more generalmust_be/4
. Progress in this area is slow, exacerbated by the tendency to regard already the slightest improvements in this area as "specific" to a system instead of encouraging other systems to adopt them. I remember years ago when I usedmaplist/3
, people were quick to point out "that's not really Prolog". Now at least we have advanced to the same point being made only aboutfoldl/4
. – Mccloskeyfold/4
where you have more than one sensible argument order. – Stonyfoldl/4
andmust_be/2
are definitely not such features. The main point of this question applies to all Prolog systems, whether they providemust_be/2
or not, and is in no way SWI specific. – Mccloskeymaplist/3
andfoldl/4
, which have been for years included in Richard's library draft, included in other systems too. It is hindering progress to act for decades as if such features are somehow extremely esoteric and on the same level as non-conformant language features that some implementations provide. In my view, it is for the latter parts and other idiosyncrasies as well as implementation-defined extensions that the system-specific tags should be reserved. Other code runs with any conforming system. – Mccloskey