Is try_call_finally/3 the same as setup_call_cleanup_each/3?

Since bilateral dialogical excursions are frowned upon by the SWI-Prolog discourse moderators, opening a new thread. Can you post your question again logicmoo? This was it, right?

Here is the behaviour I am looking for…

Does try_call_finally/3 preform like the following?

:- dynamic(state/1).
state(W):- writeln(W=true).

main:- 
  WriteFalse = ( state(W):- writeln(W=false), ! ),
  setup_call_cleanup_each(
       asserta(WriteFalse,Ref),
       ( (X=1;X=2), state(X) ),
       erase(Ref)), 
  state(X),
  fail.

?- main.

1=false
1=true
2=false
2=true

I assumed an undo/1 (in the above example) would hold the clause ref and keep it around (past even an exception)

Though another example…
(this sort of has an implicit repeat/0-like thing above the Setup)

?- setup_call_cleanup_each(
      writeln('starting one goal iteration'),
      ( (X=1;X=2),writeln(X) ),
     writeln('ending one goal iteration')),
     fail.


starting one goal iteration
1
ending one goal iteration
starting one goal iteration
2
ending one goal iteration
false.

And perhaps the exception below

?- setup_call_cleanup_each(
       writeln('starting one goal iteration'),
        ( (X=1;X=2), writeln(X), 
           (X == 2 -> throw(abort) ; true)),
       writeln('ending one goal iteration')),
    fail.

starting one goal iteration
1
ending one goal iteration
starting one goal iteration
2
ending one goal iteration
ERROR: 13:23:58.456 Unhandled exception: abort

Aha, line 34 of logicmoo.pl · GitHub
allows you to connect any shared variables of the Setup A and Cleanup C !

That is pretty amazing thank you…

My version looked like this.

trusted_redo_call_cleanup(Setup,Goal,Cleanup):- 
   HdnCleanup = mquietly(Cleanup),   
   setup_call_cleanup(Setup, 
     ((Goal,deterministic(DET)),
        (notrace(DET == true) -> ! ; 
           ((HdnCleanup,notrace(nb_setarg(1,HdnCleanup,true)));
            (Setup,notrace(nb_setarg(1,HdnCleanup,Cleanup)),notrace(fail))))),
        HdnCleanup).

Still like your version as its readable

:- meta_predicate(try_call_finally(0,0,0)).
:- export(try_call_finally/3).
try_call_finally(A, G, C) :-
   call_cleanup_(A, C),
   deterministic_(G, F),
   (F == true, !; call_cleanup_(C, A)).

deterministic_(G, F) :-
   G,
   deterministic(F),
   otherwise. /* prevent tail recursion */

call_cleanup_(G, C) :-
   call_cleanup((G; fail), C). /* prevent early determinism */

Though both both our versions is plagued…

:- dynamic(try_c_f_state/1).
try_c_f_state(W):- writeln(W=true).

try_try_call_finally:-
  WriteFalse = ( try_c_f_state(W):- writeln(W=false), ! ),
  try_call_finally(
       asserta(WriteFalse,Ref),
       ( (X=1;X=2), try_c_f_state(X) ),
       erase(Ref)), 
  try_c_f_state(X),
  fail.      

Unfortounly by this…

?-   try_try_call_finally.
1=false
1=true
ERROR: 14:02:15.231 Uninstantiated argument expected, found <clause>(0x55c88b06ce60) (2-nd argument)
ERROR: 14:02:15.231 In:
ERROR: 14:02:15.231  [109] asserta((try_c_f_state(_309080):-writeln(_309094=false),!),<clause>(0x55c88b06ce60))
ERROR: 14:02:15.231  [108] <meta call>
ERROR: 14:02:15.231  [107] setup_call_catcher_cleanup(system:true,each_call_cleanup:(each_call_cleanup:erase(<clause>(0x55c88b06ce60));fail),fail,each_call_cleanup:asserta((try_c_f_state(_309200):-writeln(_309214=false),!),<clause>(0x55c88b06ce60))) <foreign>
ERROR: 14:02:15.231  [104] each_call_cleanup:try_call_finally(each_call_cleanup:asserta((try_c_f_state(_309272):-writeln(_309286=false),!),<clause>(0x55c88b06ce60)),each_call_cleanup:((1=1;1=2),try_c_f_state(1)),each_call_cleanup:erase(<clause>(0x55c88b06ce60))) at /opt/logicmoo_workspace/packs_sys/logicmoo_utils/prolog/logicmoo/each_call.pl:80
ERROR: 14:02:15.231  [103] each_call_cleanup:try_try_call_finally at /opt/logicmoo_workspace/packs_sys/logicmoo_utils/prolog/logicmoo/each_call.pl:99
ERROR: 14:02:15.231  [102] toplevel_call(each_call_cleanup:try_try_call_finally) at /opt/logicmoo_workspace/lib/swipl/boot/toplevel.pl:1115
ERROR: 14:02:15.231
ERROR: 14:02:15.231 Note: some frames are missing due to last-call optimization.
ERROR: 14:02:15.231 Re-run your program in debug mode (:- debug.) to get more detail.
^  Exception: (107) [system] setup_call_catcher_cleanup(system:true,each_call_cleanup:(each_call_cleanup:erase(<clause>(0x55c88b06ce60));fail),_20238,each_call_cleanup:asserta((try_c_f_state(_19614):-writeln(_19614=false),!),<clause>(0x55c88b06ce60))) ```


Seems we need a way to undo the variables before the next call

This is because we should have never persisted Ref between the calls. Likely that we need a fresh copy of our Setup/Cleanup goal for each call?

Good it works out for the database

Though it would be nice to write:

:-  try_call_finally(
       gensym(hi_,X),
       member(N,[1,2,3]),
       write(X=N)),
    fail.

hi_0 = 1
hi_1 = 2
hi_2 = 3
No.

Yeah… I did try once with engines here with

https://groups.google.com/g/comp.lang.prolog/c/JToeE7Read8/m/K_NqpIhUDQAJ
(I suppose I needed to sync and the engine vars with the host vars)

Well I did create a version that works here…

is is just slow :frowning:
As you can see why (I used the clausedb as a copy mechanism to create “sharing”, asserting a new pair each time)

Maybe it is needing two trails. But it’s odd we cant get a choice point positioned above the Setup
… something that goes like this pseudocode:

      Setup = gensym(hi_,X),
      Call =  member(N,[1,2,3]),
      Cleanup = writeln(X=N),
      try_call_finally(
               repeat,
               (Setup,Call),
               Cleanup),
     ...

That does not result in…

hi_1=1
hi_1=2
hi_1=3

The way this last example does

Out of curiosity, what was the original ask and use-case?

I found the original Owicki & Lamport, 1982, paper, if you wish to see it.

Hmmm. “If you don’t know where you’re going, then any road will take you there.” – after Lewis Carroll

The main difference between all of these other methods is that they are calling some predicate in the critical section between the setup and clean-up.

Operator ‘~>’ is in-line and cumulative. Due to its non-determinism, the clean-up trap persists even after the predicate has returned to it’s caller.

what_goes_up
~> must_come_down … eventually

Example:

:- use_module('Prolog/eventually_implies').
:- use_module(library(readutil)).

show_status(N) :-
	writeln(setup(N))
	  ~> writeln(cleanup(N)).

read_codes_from(File, Codes) :-
	open(File, read, In, [type(text)])
	  ~> close(In),

    show_status(1),

	repeat,

	read_line_to_codes(In, Tmp),

	(    Tmp == end_of_file ->
	       (!, fail); Codes = Tmp).

testit :-
	show_status(2),

	forall(read_codes_from('md5sum.hash', Codes),
	       format('~s~n', [Codes])), !.

testit/0 shows:

7 ?- testit.
setup(2)
setup(1)
e185fe402d6bb856bad55fd31c1845f8  201210_A00_MCB3.eep.hex
e4b703999217be3f337aacedc81a007d  201210_A00_MCB3.hex
cleanup(1)
cleanup(2)
true.

Setups are in Entropic Time (forward-time). Clean-ups are in Syntropic Time (backward-time).

Minimal coupling and maximal cohesion properties are preserved.

p. 463:

Caveat Emptor. Experiment with it and beware of its hazards. It’s easy to comprehend, write, hide, maintain, and it keeps me out of trouble. The biggest problem is ensuring that your program eventually ends deterministically. Or, if it never ends, as many of mine do not, then choice-points are well under control.

Would this be an implementation of ~>/2 using the new undo/1 predicate?

(P ~> Q) :- call(P), undo(Q).

As it handles the code above…

	open(File, read, In, [type(text)])
	  ~> close(In),
        ...

No it is not. If P succeeds, then clean-up Q is registered atomically. There is no “window of vulnerability” where a signal can occur between call(P), and undo(Q).

OK, then?

(P ~> Q) :- 
   setup_call_cleanup(
      (call(P), 
        undo(Q -> true ; throw(error(goal_failed(Q), context(~>, _))))), 
     true, true).

No.

11 ?- X = foo ~> atom(X).
X = foo ;
false.

If P succeeds, then Q must succeed eventually, otherwise it’s an error:

12 ?- X = 2 ~> atom(X).
X = 2 ;
ERROR: ~>: goal unexpectedly failed: user:atom(2)
13 ?- 

Let’s look at one of your examples:

Assert atom ‘foo’, then look to see if it’s there:

13 ?- assert(foo, Ref) ~> erase(Ref), clause(Z, true, Ref).
Ref = <clause>(05DFFC78) ,
Z = foo .

After backtracking back into ‘~>’, erase(Ref) is triggered. If we look again, it’s not there:

14 ?- clause(Z, true, $Ref).
false.

If we cut choice points before we look, then it’s gone too:

15 ?- assert(foo, Ref) ~> erase(Ref), !, clause(Z, true, Ref).
false.

See: eventually_implies.pl (871 Bytes)

Sorry, I realized now it would have had to be different! Of course since you cannot emulate undo/1 without undo/1 currently :slight_smile:

Was there a reason you wanted this to succeed with the extra choice point?

11 ?- X = foo ~> atom(X).
X = foo ;
false.

Yes, a very good one.

My primary use-case is in providing Fault Tolerance. That is guaranteeing that fixed-resources like file handles, socket handles, mutexes, alarms, asserts, records, or any other stateful activity, gets “undone” no matter what happens, good, bad or indifferent. This is done at the time that the resource is allocated and not as an afterthought that hopefully occurs at the end of the day.

If you allow the program to proceed from ‘~>’ after the the clean-up has been triggered, (e.g. the file handle has been closed), then mayhem will certainly ensure, when you attempt to use that file handle again.

BTW: the stale file handle (In) is still bound to a file-stream handle, and if you attempt to use it after you’ve done the “one of the three things”, the you’ll still be in big trouble. That’d be a serious defect in your program:

See: MITRE CWE CATEGORY 452: Initialization and Cleanup Errors
See also: MITRE CWE-672: Operation on a Resource after Expiration or Release

Built-in predicate undo/1 is of no value to me since it’s new and cannot be used on older versions of SWIPL. As I understand it, undo/1 provides Safety only in the coarsest terms. You see why in a minute.

Hi Jan B:

Honestly, I have no interest whatsoever in defending the Owicki & Lamport paper.

As a practical matter, I do believe that they were on to something. It was insightful then and valuable now.

I write and maintain high-availability systems for a living. Once online, they only come down when I take them down, or when the crash, which is only when the power fails.

I am keenly interested in the Correctness of Software as a matter of design and construction and not as a matter of Testing. Testing after all, says nothing about things that aren’t tested.

I am face with absurd commercial imperative: “We will find no defects as soon as possible.” Finding defects is always at the end of the schedule, and the schedule is always compressed.

The only way to demonstrate Correctness of Software is to demonstrate the absurdity of the converse argument. And that isn’t accomplished by way of Testing.

Temporal Progress is Liveness and Safety. It doesn’t take much of a stretch to assert that Progress and Correctness are closely correlated.

Consider:

safely(Goal) :-
	catch(Goal, Err, (print_message(error, Err), !, fail)).

liveness(P, Q) :-  % something good always happens
	P -> Q.

safety(P, Q) :-    % nothing bad ever happens
	once(\+P ; Q).

progress( P, Q) :-
	liveness(P, Q),
	safety(P, Q).

eventual_progress(P, Q1) :-   % something good always happens ... eventually
	P ~> Q,

  %      throw(error(ouch)),

	Q = Q1, !.

As written, liveness/2, and safety/2, aren’t terribly useful, since after you’ve established thing P holds, there’s a very-good likelihood that thing Q holds as well. But as time passes, things might be different

Temporal Liveness and Safety provides for an eventual implication. If P holds, then Q must hold from now on, or it’s an error – it’s a construction error!

Actually, this is not possible in SWI-Prolog. A file handle is a blob (~atom). It can not be created other than using one of the predicates that creates a (valid) file handle. Once underlying OS handle is closed the blob knows it is closed and raises an error on any attempt to use it as a file handle. Only if the Prolog program has no references to it atom-GC will dispose the blob. The same blob (address) may now be reused to create a new stream but as it first needs to be garbage collected this is completely safe. In other words, usual OS scenario where I have a file handle (say) 7, close it, some other code opens something else that gets file handle 7, the original code missed that it closed 7 and now happily accesses the new opened resource is not possible in SWI-Prolog.

It may still be a good idea to make garbage collected file handles close the file. Not 100% sure whether that is always good.

undo/1 does guarantee is is called if backtracking or unwinding due to an exception takes us back to before the undo was called. A once registered undo is safe: if its execution fails due to an exception it remains queued. Completely safely registering an undo is hard though. Maybe this works :slight_smile:

    setup_call_catcher_cleanup(assert(Term, Ref),
                               undo(erase(Ref)),
                               exception(_),
                               erase(Ref)).

That’s precisely my point.

Shortly after cleanup(1), the file will be closed. But, the reference to the blob is still bound to variable ‘In’. But it’s no longer valid for the purpose of file i/o. SWIPL will doubtless raise an exception that we aren’t prepared for. The program will fail (exception) back to the exception-catcher of last resort.

show_status(N) :-
	writeln(setup(N))
	  ~> writeln(cleanup(N)).

read_codes_from(File, Codes) :-
	open(File, read, In, [type(text)])
	  ~> close(In),

   show_status(1),

	!,   % if I cut choice points, then then the clean-up occurs.

	repeat,

	read_line_to_codes(In, Tmp),  % disaster is bound to ensue

	(    Tmp == end_of_file ->
	       (!, fail); Codes = Tmp).

testit :-
	show_status(2),

	forall(read_codes_from('md5sum.hash', Codes),
	       format('~s~n', [Codes])), !.

21 ?- safely(testit).
setup(2)
setup(1)
cleanup(1)
cleanup(2)
ERROR: read_util:read_line_to_codes/2: stream `<stream>(05061CB0)' does not exist
false.