http://www.complang.tuwien.ac.at/ulrich/iso-prolog/cleanup.html
Thanks to their availability it is now possible to compare the systems
and reconsider the specification in the light of actual
implementations. In fact, we went through this iteration already
several times - also in Pasadena. This is the latest case:
While comparing the behavior for setup_call_cleanup/3 for different
systems I came today to the following case which might be seen as
ambiguous.
If your systems supports call_cleanup/2 but not setup_call_cleanup/3
take
setup_call_cleanup(S, G, C) :- once(S), call_cleanup(G, C).
For this example it is sufficient.
?- setup_call_cleanup(true, (X=1;X=2), write(semel)), setup_call_cleanup(true,(Y=1;Y=2),write(bis)), !.
We have here two goals protected independently with cleanup semel and
bis. The cleanups cannot be executed prior to ! since both have a
further solution awaiting (X = 2 and Y = 2).
One moment of calling the cleanup prior to failure is (7.8.11.1 c 1):
"after the last solution..."
Now when executing ! both cleanups should be executed due to (7.8.11.1
c 3).
The precise order is defined (7.8.11.1 d) "Several cleanup goals
triggered simultaneously are executed in reversed order of
installation."
That means, the output shall be 'semelbis'. And the query succeeds,
unifying X = 1, Y = 1.
An output 'bissemel' would be not conformant.
Currently I have slight doubts: Is this an overspecification?
Couldn't both be accepted?
1mo Arguments in favor of the current specification:
+ Cleanup bis and the associated goal might take certain resources for
granted that were allocated by semel.
+ By demanding to execute first cleanup bis and then semel, a system
might recover memory earlier.
+ One could even simply the specification to "Cleanup goals are
executed in reversed order of installation".
2do Arguments that this is an overspecification
- In contrast to an exception (which might be a resource error due to
a memory overflow), there is no pressing need for space efficiency so
implementations should not be restricted unnecessarily.
- Systems might have some "selective" cut as an implementation
specific extension that cuts one choice point "in the middle" away. I
don't know if such a beast exists - just in case.
For other current activities of WG17 please refer to:
http://www.sju.edu/~jhodgson/wg17/Drafts/
We need everyone who cares about Prolog!
> ?- setup_call_cleanup(true, (X=1;X=2), write(semel)), setup_call_cleanup(true,(Y=1;Y=2),write(bis)), !.
<snip>
> The precise order is defined (7.8.11.1 d) "Several cleanup goals
> triggered simultaneously are executed in reversed order of
> installation."
>
> That means, the output shall be 'semelbis'. And the query succeeds,
> unifying X = 1, Y = 1.
? semel is installed prior to bis, so the reverse is bissemel ...
What am I missing?
Specifying this order (= bissemel) makes some sense. On the other
hand, there may be low-level reasons in some systems to reverse and
it might be a bit of an overspecification to force them to comply.
I guess most systems cancel choice-points in reverse order, but systems are
likely to use different mechanisms to insert the execution of the cleanup goal
into their scheduled work. E.g. SWI-Prolog does this using a call-back to the
Prolog engine from the routine that does the choice-point pruning. Others may
use a queue or stack of goals to be inserted at the first opportunity, etc.
Cheers --- Jan
Your viewpoint is from the side of the implementor.
But there is the other direction as well.
Is the guarantee that all cleanups will be executed in reversed order
useful for a programmer or not?
Hi, Ulrich:
Speaking as a programmer, I'd noticed the same
thing Jan did... that the spec calls for the
cleanup actions to be performed in the reverse
order of installation (and opposite to how you
first sketched the "ambiguity"), and I wondered
(as Jan seems to have) if I were missing the
point.
From a broader programming language perspective
this is the order (inside-out) that I'd expect
cleanup code to be called. When "destructing"
objects (class instances), the "cleanup" code
is generally inside-out with respect to the
inheritance hierarchy, at least for non-virtual
classes. That's an analogy, possibly a poor
one, but as I excuse myself, from a programming
point of view you want to expect something, and
this makes for a nice simple rule to remember
(always wise to keep things simple for coders!).
regards, chip
You both are right! That's what the current specification says. But
then, *I* have written it - it could be demanding too much or it could
be even inconsistent. It is by comparing different implementations (4
in the meantime...) that my doubts were raised. From the
specification's point of view any reduction of its size can only be
welcomed. For implementors the situation is not that clear. But any
reduction of special cases is a win - ceteris paribus indeed. It is
the programmer's perpective, that is still not clear.
>From a broader programming language perspective
>this is the order (inside-out) that I'd expect
>cleanup code to be called. When "destructing"
>objects (class instances), the "cleanup" code
>is generally inside-out with respect to the
>inheritance hierarchy, at least for non-virtual
>classes. That's an analogy, possibly a poor
>one, but as I excuse myself, from a programming
>point of view you want to expect something, and
>this makes for a nice simple rule to remember
>(always wise to keep things simple for coders!).
I am not sure that this analogy holds. We have different forms of
nestings of cleanups that correspond to two related notions of
termination whose accidental confusion is quite a source of sorrow.
Were do you have this with objects?
With a little bit of phantasy you could consider the termination
properties of an iterator as an analogon: So existential terminaion
corresponds somehow (not exactly) to the termination of the next
message. In fact termination of next is a stronger property. And
universal termination is given if such a for-loop terminates always.
I would rather distinguish here languages without yield - let's ignore
them - and those with yield. Those with yield might give us some
insight - I cannot fathom currently.
The other source of insight I tapped was Common LISP and Scheme. They
faced these issues a little bit longer. unwind-protect is the
corresponding construct. It turned out that the CL standard has some
ambiguity that is not explicitly flagged. So a reading alone will not
reveal the ambiguity. I want to avoid that. Of course that means
that the specification could have some more implementation
dependent/undefined parts - which would make it longer and more
unwieldy - but then leaving well known ambiguities unflagged would be
a service.
It's all about specifying something which is at its heart
non-functional.
would *NOT* be a service.
Okay, hearing from a specifier then, do you see
an ambiguity in the current standard? Of course
the standard could be other than it is, but the
most I get from your later phrasing is perhaps
in "concurrent" Prolog there may be ambiguity
about the order of installation & thus cleanup.
But concurrent Prolog introduces all sorts of
ambiguities of this kind, it seems to me. Can
threads access the same dynamic predicates? If
so, do asserts and retracts affect pending goals
(this would be bad) across threads?
As a programmer I like to maintain a very serial
outlook on how I expect Prolog code to work.
regards, chip
What do you mean by "current standard"? 13211-1, and 13211-2? And
what do you mean by ambiguity?
Everything that is implementation dependent or undefined in 13211-1
and 2 is such an ambiguity. To a lesser degree parts that are
implementation defined. That's inevitable, BTW. Think of variable
ordering which is implementation dependent. Lots of people criticised
that it is so, but there is not a single way to specify it without
hindering implementors to do the implementation they want to do. I do
not know how often this was discussed through literally years.
If you meant by current standard this one tiny draft concerning
setup_call_cleanup/3 (tiny with respect to all the other current
material - lets say it amounts to 1% or so) - let me first make clear
that this is a draft and it has therefore no actual normative
relevance. Member bodies one day (when all other things are ready)
will vote - and then things become relevant or not.
Of course, by collecting the feedback of various implementations,
discussing and testing actual implementations the current draft
reflects the intention at least of those involved. So far we have one
perfect implementation (as far as I can see) SWI, one that is very
close to perfection YAP (only microscopic differences), and two that
are usable for the normal uses but have some of the "exotic" cases
missing: B, Qu. Compare this to January, when there was none.
http://www.complang.tuwien.ac.at/ulrich/iso-prolog/cleanup
There are lots of *inherent* ambiguities in setup_call_cleanup/3.
For example you may have noted that there are many cases labelled
either-or. These cases are due to the way how an implementation is
able to determine "determinism" which currently depends on the
indexing techniques employed. All these cases are not specified
exactly: Instead an interval is given, where the cleanup will be
executed. So there is some ambiguity which is inevitable. In the
future, things might become even less determinate as detection of
determinism might (or not) be delegated to or augmented by some GC
like phrase. At least I am trying to put things such that such an
implementation would not be unnecessarily hindered. So far there was
no extra complication due to this rather long term perspective.
There are also ways to be 100% sure what happens, but then you would
have to use once/1, ! or throw/1.
>But concurrent Prolog introduces all sorts of
>ambiguities of this kind, it seems to me. Can
>threads access the same dynamic predicates? If
>so, do asserts and retracts affect pending goals
>(this would be bad) across threads?
setup_call_cleanup/3 is partially concerned with such aspects. It can
at least guarantee certain properties in the presence of asynchronuous
events. That was one of the reason to extend the traditional
call_cleanup/2 (supported by other 4 implementations) to the current
setup_call_cleanup/3 (supported by abovementioned 4 implementations).
I cannot comment much on threads except there is a thread proposal by
Paulo Moura, but input from others was so sparse and unconstructive
that he gave up. The only stable implementation seems to be SWI
(including dynamic stack management), but maybe there are others I am
not aware of. I looked at the proposal and concluded that we simply
have not enough manpower to make it. As long as there is not more
input - particularly from users - things will develop quite slowly,
but steadily. In case you are interested, consider first the
comparatively simple efforts we have now.
Just to give you one impression: Feedback concerning the state of
further built-ins and arithmetic functions by implementors:
http://www.complang.tuwien.ac.at/ulrich/iso-prolog/evaluable_functors
http://www.complang.tuwien.ac.at/ulrich/iso-prolog/built-in_predicates
The standard development for 13211-1 was very much supported by
industry, BTW. Otherwise it would have been impossible. Even if
people criticise it for being not fast "enough", I think it was quite
fast: Beginning 1987, and in vigor 1995-06-01. That is fast! In
particular if you consider all the diversity of sytems in the
beginning. And the deep problems that were resoveld and that nobody
dared to touch before.
Here are the current drafts - any comment is welcome - it's a way to
keep Prolog alife and healthy!
Yes, it is an overspecification. How can you possibly rely on
the order in the cut case, when even in the cut-independent
cases you can get any order you want:
?- X=0, <code above>.
semel
?- X=2, Y=0, <code above>.
semelbis
?- Y=2, <code above>.
bissemel
?- X=1, Y=2, <code above>.
bissemel % with bad indexing
semelbis % with good indexing
?- Y=0, <code above>.
bissemelbis
> 1mo Arguments in favor of the current specification:
>
> + Cleanup bis and the associated goal might take certain resources for
> granted that were allocated by semel.
Sounds contrived, since Cleanup is supposed to release resources.
Anyway, when you use such invisible dependencies in your code,
you deserve what you get. The proper way to deal with this is
to have dependencies visible, e.g. by the use of resource handles.
>
> + By demanding to execute first cleanup bis and then semel, a system
> might recover memory earlier.
Or vice versa - what kind of argument is that?
>
> + One could even simply the specification to "Cleanup goals are
> executed in reversed order of installation".
>
> 2do Arguments that this is an overspecification
>
> - In contrast to an exception (which might be a resource error due to
> a memory overflow), there is no pressing need for space efficiency so
> implementations should not be restricted unnecessarily.
>
> - Systems might have some "selective" cut as an implementation
> specific extension that cuts one choice point "in the middle" away. I
> don't know if such a beast exists - just in case.
Yes, it is called a soft cut (if/3 in Sicstus, *->/2 in SWI and ECLiPSe,
and probably others).
Jan Wielemaker wrote:
> I guess most systems cancel choice-points in reverse order, but systems are
> likely to use different mechanisms to insert the execution of the cleanup goal
> into their scheduled work. E.g. SWI-Prolog does this using a call-back to the
> Prolog engine from the routine that does the choice-point pruning. Others may
> use a queue or stack of goals to be inserted at the first opportunity, etc.
Thanks for making this point. A lot of grief (and cost without benefit) can
be caused by specifications that assume a certain naive implementation model.
-- Joachim
In the cases you give you are not much interested in guaranteeing
any order - as shows the ambiguous case quite clearly. So we
agree here, that for such cases the order would be qua si random.
True!
There are however cases where people explicitly leave things
open *on purpose* so they do not want to depend on the cleverness
of the implementation. For example some Erlang like network protocol
uses this. In the beginning the essential part was like this:
..., setup_call_cleanup(Nimporte, (true ; false), Cleanup), ....
So the intention of this code is to leave that choice point open.
(I tried to question that, but they wanted it like that, ok?)
First I was completely ruined: If we would demand that this
choicepoint remains open, essentially all kinds of optimizations
would be voided. But then luckily I realized that we can describe
this as follows:
..., setup_call_cleanup(Nimporte, ( X = 1; X = 2 ), Cleanup), X = 1, ...
In this manner the choicepoint has to remain open due to 7.8.11.1 c1.
We have to wait for the last solution X = 2 before the cleanup
can be executed (or one of the other events c2 or c3 happens).
Of course that is a little bit of a workaround, but they were happy
and there is no interference with whatever clever or not clever
indexing schema or dynamic GC based indexing.
So if people are that "picky" on the moment when Cleanup is executed,
above ambiguity will never happen to them.
>> 1mo Arguments in favor of the current specification:
>>
>> + Cleanup bis and the associated goal might take certain resources for
>> granted that were allocated by semel.
>
>Sounds contrived, since Cleanup is supposed to release resources.
>Anyway, when you use such invisible dependencies in your code,
>you deserve what you get. The proper way to deal with this is
>to have dependencies visible, e.g. by the use of resource handles.
You seem to think at a certain pattern of usage which is grosso
modo setup_call_cleanup(open_whatever(S), operate(S), close_somehow(S)).
That is *one* usage. And I would say that this is the most important
for the moment. open might be file opening but also opening a
connection. But there are much more interesting uses.
Apart from that networking, it seems also to be of relevance for things
like pure output (I sent you some esquisse for that).
In that case a cut might trigger a cleanup that "squeezes" out all
information needed further on.
>> + By demanding to execute first cleanup bis and then semel, a system
>> might recover memory earlier.
>
>Or vice versa - what kind of argument is that?
For the case of exceptions the order is entirely fixed - already
due to the bindings. We are interested in handling the exception with
the minimal binding, since this allows to recover a maximum of
memory which increases the chances that recovery will be successful.
I am always thinking of resource errors due to memory overflows..
So here we have a clear memory dependency. But in the
case of cut ... hm ... well that's open for the moment.
>> + One could even simply the specification to "Cleanup goals are
>> executed in reversed order of installation".
>>
>> 2do Arguments that this is an overspecification
>>
>> - In contrast to an exception (which might be a resource error due to
>> a memory overflow), there is no pressing need for space efficiency so
>> implementations should not be restricted unnecessarily.
>>
>> - Systems might have some "selective" cut as an implementation
>> specific extension that cuts one choice point "in the middle" away. I
>> don't know if such a beast exists - just in case.
>
>Yes, it is called a soft cut (if/3 in Sicstus, *->/2 in SWI and ECLiPSe,
>and probably others).
This cut cannot cut away a choicepoint that would trigger a Cleanup.
Or can it? That was what I wanted to ask for - the question was a
bit too general, I admit.
No we don't agree - you are only reading what you want to read.
My point is that if you have
p(...),
setup_call_cleanup(true, q(...), write(one)),
setup_call_cleanup(true, r(...), write(two)),
!.
you can't rely on 'one' and 'two' to appear in any particular
order, whether you specify anything for the cut or not. So don't.
> There are however cases where people explicitly leave things
> open *on purpose* so they do not want to depend on the cleverness
> of the implementation. For example some Erlang like network protocol
> uses this. In the beginning the essential part was like this:
>
> ..., setup_call_cleanup(Nimporte, (true ; false), Cleanup), ....
>
> So the intention of this code is to leave that choice point open.
> (I tried to question that, but they wanted it like that, ok?)
> First I was completely ruined: If we would demand that this
> choicepoint remains open, essentially all kinds of optimizations
> would be voided. But then luckily I realized that we can describe
> this as follows:
>
> ..., setup_call_cleanup(Nimporte, ( X = 1; X = 2 ), Cleanup), X = 1, ...
>
> In this manner the choicepoint has to remain open due to 7.8.11.1 c1.
> We have to wait for the last solution X = 2 before the cleanup
> can be executed (or one of the other events c2 or c3 happens).
> Of course that is a little bit of a workaround, but they were happy
> and there is no interference with whatever clever or not clever
> indexing schema or dynamic GC based indexing.
>
> So if people are that "picky" on the moment when Cleanup is executed,
> above ambiguity will never happen to them.
This is all quite ridiculous. When something gets that unwieldy
and hacky, don't you think it's time to stop and think
"Maybe this is the wrong approach to the problem?"
You chose to ignore the related comment about resource handles in
my previous reply. Let me try to be a bit clearer and inject an
idea into this debate, that could explain why all this is so messy:
We are trying to solve a problem that is essentially a garbage
collection type of problem (freeing a resource that is used by some
subgoals of Goal). The resource can be freed when no such subgoals
of Goal exist any longer (or will ever exist on backtracking).
However, setup_call_cleanup/3 is trying to solve this GC like problem
via a plain control flow solution. It tries to guess something
about reachability of the resource by looking at choicepoints.
This just doesn't work very well. And worse, since the programmer
knows that this is how it works, they start hacking with dummy
choicepoints in order to convey some reachability information.
It's like doing stack memory management via failure driven loops
all over again.
-- Joachim
Before this comes across in the wrong way: I greatly appreciate
Ulrichs effort to get something done. My experience is only that
one has to be wary of feature requests of the form
"I need [exotic feature X] because I want to use
[clunky method Y] to implement [sensible thing Z]"
In 99% of cases there is a better way to get there. If there
wasn't I'd long have given up computer science ;-)
-- Joachim
I clearly disagree here: It depends on the definition of q/1 and r/1!
So let me fill in the ellipsis:
p(_).
q(1).
q(2).
r(10).
r(20).
And the query is:
p(_),
setup_call_cleanup(true, q(Q), write(one)),
setup_call_cleanup(true, r(R), write(two)),
!.
Then - in this case - and clearly in contrast to your claim -
we can rely on the fact that none of the cleanups will be
executed prior to the !. And it is only that ! that will cause
them to be executed! Thus - in this situation here - the
specification of an order would be very much of relevance.
>> There are however cases where people explicitly leave things
>> open *on purpose* so they do not want to depend on the cleverness
>> of the implementation. For example some Erlang like network protocol
>> uses this. In the beginning the essential part was like this:
>>
>> ..., setup_call_cleanup(Nimporte, (true ; false), Cleanup), ....
>>
>> So the intention of this code is to leave that choice point open.
>> (I tried to question that, but they wanted it like that, ok?)
>> First I was completely ruined: If we would demand that this
>> choicepoint remains open, essentially all kinds of optimizations
>> would be voided. But then luckily I realized that we can describe
>> this as follows:
>>
>> ..., setup_call_cleanup(Nimporte, ( X = 1; X = 2 ), Cleanup), X = 1, ...
>>
>> In this manner the choicepoint has to remain open due to 7.8.11.1 c1.
>> We have to wait for the last solution X = 2 before the cleanup
>> can be executed (or one of the other events c2 or c3 happens).
>> Of course that is a little bit of a workaround, but they were happy
>> and there is no interference with whatever clever or not clever
>> indexing schema or dynamic GC based indexing.
>>
>> So if people are that "picky" on the moment when Cleanup is executed,
>> above ambiguity will never happen to them.
>
>This is all quite ridiculous. When something gets that unwieldy
>and hacky, don't you think it's time to stop and think
>"Maybe this is the wrong approach to the problem?"
Hey, I want to encourage discussion "even" with people who might
get offended by your conjecture that this is all quite ridiculous!
This can be very intimidating to people. Think of it: They
are working on a system - and then their manager reads your
statement or their imperative cow-orkers read that what they
do is all ridiculous. Not funny.
What I learned since January is that there is one further usage
of setup_call_cleanup/3 that I have not thought about before,
but that is actually in use. Oh yes, I even tried to deny
it for some weeks.
Before, I thought the purpose would be guarding some goal, and
relying on the cleverness of a system for indexing to cleanup
the mess as fast as possible. What I had in mind (for the
more complex cases - in most situations things are much
simpler) was library(pio).
Well, wrong I was - or rather narrow minded. Since there
is more.
Some use it in a way such that the execution of the cleanup is
entirely predetermined. Their programs would executed exactly
the same way whether or not the initial Goal is replaced
by ( Goal ; false ) or another trick to get a choicepoint in.
>You chose to ignore the related comment about resource handles in
>my previous reply. Let me try to be a bit clearer and inject an
>idea into this debate, that could explain why all this is so messy:
>
>We are trying to solve a problem that is essentially a garbage
>collection type of problem (freeing a resource that is used by some
>subgoals of Goal). The resource can be freed when no such subgoals
>of Goal exist any longer (or will ever exist on backtracking).
>However, setup_call_cleanup/3 is trying to solve this GC like problem
>via a plain control flow solution. It tries to guess something
>about reachability of the resource by looking at choicepoints.
>This just doesn't work very well. And worse, since the programmer
>knows that this is how it works, they start hacking with dummy
>choicepoints in order to convey some reachability information.
>
>It's like doing stack memory management via failure driven loops
>all over again.
A garbage collection type problem - yes - that what is called
usually "nonfunctional". I.e. a nonfunctional specification
depends on some external imponderabilities like execution time,
quality of service or in this case here the capability to
detect determinism.
This capability is now solved via a "plain control flow solution".
Yes! Thanks to the ingenuity of Mats Carlsson who invented
it in Autumn 1987!
Compare this to the situation before! How was it possible
to execute a goal with certain resources open? Prior to this
cleanup mechanism we had no other chance than to forcefully
cut alternative solutions of a goal. Something along the line
(without the exception handeling and unanticipated failure):
with_file_open(G, S, F) :-
open(F, read, S),
G,
!, % Ouch!
close(S).
And in fact there were such libraries (one by Richard O'Keefe)
that forcefully and entirely unnecessarily (from the viewpoint
of the user - not the system) *had* to cut into the flesh
of Prolog.
Now, we are over this. Finally!
And it turns out, that this new construct is now used for things
a little bit unanticipated - as we were completely bound
looking in one direction. Quelle bonne surprise !
There was discussion on comp.lang.lisp about standarization of Lisp.
Common agreement was that too much standarization and too many fancy
features effectively killed Lisp.
General advice: if something is not broken, don't fix it.
A.L.
Good grief. Do I really not make myself clear? I try one last time.
Of course you can work out what happens IF you analyse the program.
That's not my point. My point is: If you want to protect the two
consecutive goals q and r with setup_call_cleanups, say
setup_call_cleanup(alloc1(R), q(..,R,..), free1(R)),
setup_call_cleanup(alloc2(S), r(..,S,..), free2(S)),
then you better write free1/1 and free2/1 in such a way that they
work independently of the order in which they are executed,
because this order will depend on:
- the instantiations when entering the whole code sequence
- the delayed goals/constraints that are in place
- the internals of p and q
- the properties of your compiler
All of which means it would be highly irresponsible to assume a
particular order (even without any cuts). Whether now the use of
a cut adds one additional source of uncertainty about the order
doesn't really matter, so you may as well leave it unspecified.
Ok, I'm half sorry for that formulation, but note that it did not
refer to the application writers, but solely to your argument.
You want to introduce a standard builtin with the stated purpose
of supporting a rather clunky hack - that's what I found funny.
> What I learned since January is that there is one further usage
> of setup_call_cleanup/3 that I have not thought about before,
> but that is actually in use. Oh yes, I even tried to deny
> it for some weeks.
>
> Before, I thought the purpose would be guarding some goal, and
> relying on the cleverness of a system for indexing to cleanup
> the mess as fast as possible. What I had in mind (for the
> more complex cases - in most situations things are much
> simpler) was library(pio).
Maybe it's time that you say precisely what this mysterious
requirement of lib(pio) is, because I looked at the library
and couldn't figure it out.
All very nice, but could you comment on what I actually wrote?
> This capability is now solved via a "plain control flow solution".
No, it's not solved because, as I tried to explain, reachability
cannot be inferred from the presence of choicepoints. Example:
?- setup_call_cleanup(open(foo,write,S),freeze(X,write(S,X)),close(S)), X=2.
ERROR: write/2: stream `$stream(1070183496)' does not exist
> Yes! Thanks to the ingenuity of Mats Carlsson who invented
> it in Autumn 1987!
I'm sure Mats has a more measured appreciation of his idea.
> Compare this to the situation before! How was it possible
> to execute a goal with certain resources open?
I'm not saying that there is no problem to be solved, I'm saying
that setup_call_cleanup/3 is a poor way of addressing it.
The Prolog-like system I'm working on uses external resources
_a_lot_, but thanks to the way it implements and garbage-collects
handles for these resources, we have been happily living without
such a builtin so far. [i/o streams have for historic reasons
not been implemented using these handles, but we'll do it in the
upcoming release].
> And it turns out, that this new construct is now used for things
> a little bit unanticipated - as we were completely bound
> looking in one direction. Quelle bonne surprise !
Rather a nightmare: the primitive leaks implementation-level
information about presence of choicepoints into the program logic:
setup_call_cleanup(true, Goal, Det=yes).
Yes, I too want to have that information for implementing a
debugger or toplevel, but in a standard language primitive?
Even worse, a cut can now magically trigger a variable binding:
?- setup_call_cleanup(true, (true;true), Det=surprise),
writeln(Det), !, writeln(Det).
_G835
surprise
Hardly desirable, and not something your average program analyzer
would expect to happen!
So my suggestions: if you want to have setup_call_cleanup/3 anyway,
please at least get rid of these two nasty effects. This can be done
by requiring that the Cleanup goal has only the instantiation at the
end of Setup, and does not share variables with the context. The
basic open/close examples all work with such a semantics.
[I have put such a variant into ECLiPSe 6.0_102, lib(iso)]
If you want a primitive to tell you about choicepoints, use one that
doesn't spontaneously bind a variable, I can suggest something.
But anyway, I don't think it belongs in the Prolog standard.
-- Joachim
(ISO) Prolog is broken: there is just about no real-world program that
stays within the standard. I agree that standards can be too big. They
can also be too small and that is the problem with the current ISO
standard. Luckily there is some work to enhance portability.
Cheers --- Jan
That is a pretty good point. Not really for setup_call_cleanup, because
it is about ISO Prolog that doesn't have coroutining, but as many
real systems do have coroutining, it is a fair point.
> I'm not saying that there is no problem to be solved, I'm saying
> that setup_call_cleanup/3 is a poor way of addressing it.
> The Prolog-like system I'm working on uses external resources
> _a_lot_, but thanks to the way it implements and garbage-collects
> handles for these resources, we have been happily living without
> such a builtin so far. [i/o streams have for historic reasons
> not been implemented using these handles, but we'll do it in the
> upcoming release].
Here I'm getting curious. Do you do this in normal heap-GC? That would
exclude handles asserted into the database and requires you to inspect
the holes in the stack. It also requires scanning for handles that
become unreachable due to backtracking. The alternative (which is
supported by SWI-Prolog but (still) not used much) is to use things that
behave as atoms and rely on atom-gc. The problem with this type of GC
however is that it requires synchronising threads and it is difficult to
schedule.
Let us look at I/O handles. Typically, you do not have that many of
them, so you should collect them in time. How do you ensure that? Call
the GC if you are out of I/O handles? Second issue is that you sometimes
simply want to close an I/O channel. Ideally, that means forcing that
all pending computation (choice-points and constraints) are `finished'.
How do you do that?
> Rather a nightmare: the primitive leaks implementation-level
> information about presence of choicepoints into the program logic:
> setup_call_cleanup(true, Goal, Det=yes).
> Yes, I too want to have that information for implementing a
> debugger or toplevel, but in a standard language primitive?
Being deterministic or not is of such importance to (advanced) Prolog
programmers that it can hardly be called `implementing-level' :-)
Nevertheless, I agree that setup_call_cleanup/3 isn't the way to find
out for most practical usage. That should be declarations that are
checked at compile- or (optionally) runtime.
Cheers --- Jan
Please consider how an out-of-context quote like yours above would
look like in a meeting. We really need a little bit more toned
down language.
>You want to introduce a standard builtin with the stated purpose
>of supporting a rather clunky hack - that's what I found funny.
With this alleging ad-hominem language you destroy broad
and open discussion in comp.lang.prolog. How many of actual
users will now dare to say a single word - when the main
developer of a Prolog system called their work a clunky
hack? I can only hope you do not treat users of your system
in the same manner. In any case it will remain a dialogue
between you and me - at best. And my aim here was to get a
discussion going that also includes actual users - whose
contribution would be primarily them telling us about their
experience.
We now have a lot of users of setup_call_cleanup/3. It did
solve some of the open issues which are important in the context
of threads and lots of stability issues for programs running
for weeks.
Read my posting of 2009-01-14 to see my stated purpose.
*That* is my stated purpose.
Would you be more happy if I would not state all purposes people
told me in the meantime? We can consider those
and weigh them against the effort to implement, what that would
mean to the development of the language, their semantic
impact - whatever. We could dismiss some of the parts. But
in any case we would need more justification than judgmental
phrases. And we can do this without going on a level that
neither befits scholars nor professional programmers alike.
There are some points in your posting nevertheless. I will answer
them sometime - but if you continue with your choice of language
it would be futile.
In any case, should some other system developer be interested
to implement the current approach - don't hesitate to contact
me. The test cases cover all the problems I know of.
That's part of it.
> That would exclude handles asserted into the database
These are treated by reference counting.
> and requires you to inspect the holes in the stack.
Not sure what you refer to here??
> It also requires scanning for handles that
> become unreachable due to backtracking.
No scanning, it's done via trailing.
...
> Let us look at I/O handles. Typically, you do not have that many of
> them, so you should collect them in time. How do you ensure that? Call
> the GC if you are out of I/O handles?
That would be the solution, except you would probably trigger
the GC when you are _almost_ out of handles.
> Second issue is that you sometimes
> simply want to close an I/O channel. Ideally, that means forcing that
> all pending computation (choice-points and constraints) are `finished'.
> How do you do that?
I can't comment without seeing a clearer specification of that.
>
>> Rather a nightmare: the primitive leaks implementation-level
>> information about presence of choicepoints into the program logic:
>> setup_call_cleanup(true, Goal, Det=yes).
>> Yes, I too want to have that information for implementing a
>> debugger or toplevel, but in a standard language primitive?
>
> Being deterministic or not is of such importance to (advanced) Prolog
> programmers that it can hardly be called `implementing-level' :-)
> Nevertheless, I agree that setup_call_cleanup/3 isn't the way to find
> out for most practical usage. That should be declarations that are
> checked at compile- or (optionally) runtime.
Cheers,
Joachim
I mean, normal GC only pays attention to the life data on the stacks. Now you
must also look at the dead data and take action if you GC'ed a handle.
>> It also requires scanning for handles that
>> become unreachable due to backtracking.
>
> No scanning, it's done via trailing.
Bu what if you are creating a term with a handle above the top-of-stack
marker of the last choice-point. Normally you wouldn't trail that, but
you simply chop-off the top-part of the stack.
>> Let us look at I/O handles. Typically, you do not have that many of
>> them, so you should collect them in time. How do you ensure that? Call
>> the GC if you are out of I/O handles?
>
> That would be the solution, except you would probably trigger
> the GC when you are _almost_ out of handles.
:-)
> > Second issue is that you sometimes
>> simply want to close an I/O channel. Ideally, that means forcing that
>> all pending computation (choice-points and constraints) are `finished'.
>> How do you do that?
>
> I can't comment without seeing a clearer specification of that.
What I'm referring to is that using setup_call_clean/3, you can make
sure all work is being completed and the I/O channel is being closed
by forcing determinism. You want to deal with coroutining. That
will will make it pretty hard to force termination.
A scenario I had in mind is that we had Prolog generating a file that was then
passed to an external application. I used setup_call_cleanup/3, but the code
was non-deterministic. As a result, the file remained open and on some
unnamed OS we got a sharing violation. With setup_call_cleanup/3 and
only choice-points to worry about, this is still quite tracktable, but
with arbitrary coroutining?
Cheers --- Jan
> ?- setup_call_cleanup(true, (X=1;X=2), write(semel)),
> setup_call_cleanup(true,(Y=1;Y=2),write(bis)), !.
[..]
> The precise order is defined (7.8.11.1 d) "Several cleanup goals
> triggered simultaneously are executed in reversed order of
> installation."
>
> That means, the output shall be 'semelbis'. And the query succeeds,
> unifying X = 1, Y = 1.
>
> An output 'bissemel' would be not conformant.
???
I have skimmed through the resulting conversation, and I got the impression
that some people said that actually bissemel would be the required output, but
you never confirmed that - so before attempting into any "to the point" reaction,
please, according to the order defined in 7.8.11.1.d - the reverse order - should
it be semelbis or bissemel ?
Cheers
Bart Demoen
ps. reacting anyway :-) does anyone think we should in Prolog repeat the
mistake Java made with the finalizer ?
That was wrong, and I confirmed it twice in response to Jan and
Chip Eastham.
>ps. reacting anyway :-) does anyone think we should in Prolog repeat the
>mistake Java made with the finalizer ?
It is not the same situation. Java does always depend on GC.
But setup_call_cleanup/3 has very precise intervals
defined that an implementatin must respect.
That is, indexing, GC or whatever other approach is chosen
is of relevance between those intervals not outside.
The spec defines the intervals - the programmer can decide
how he uses it: Such that he has complete control (actually
we are discussing this - there was this tiny little ambiguity)
- or that he gives some part of the control to the system.
So far, we have both - the analogy you mention to Java
thus does not hold. Also, could you please for the sake
of the discussion give use some evidence by third party
that this finalizer issue in Java is actually considered
a mistake? Does Sun share this view? Or any other involved
or opposing party?
> bart demoen <b...@cs.kuleuven.be> writes:
>>ps. reacting anyway :-) does anyone think we should in Prolog repeat the
>>mistake Java made with the finalizer ?
>
> It is not the same situation. Java does always depend on GC. But
> setup_call_cleanup/3 has very precise intervals defined that an
> implementatin must respect.
I was not referring to setup_call_cleanup/3 but to the subsequent
exchange of postings between Joachim and Jan (and maybe you).
> So far, we have both - the analogy you mention to Java thus does not
> hold.
As I just said and confirming now: not related to any specs
of setup_call_cleanup/3.
> Also, could you please for the sake of the discussion give use
> some evidence by third party that this finalizer issue in Java is
> actually considered a mistake? Does Sun share this view? Or any other
> involved or opposing party?
ISMM 1998 Vancouver - lots of it. Also some proponents of course, but
lots of critisism. Enough for me to conclude "let's not go there in Prolog".
But maybe you want to form your own opinion about finalizers - I suggest you
think a bit (really a bit is enough) about to what extent it helps the
programmer in constructing more reliable software and what constraints it
puts on the collector. Whether Sun agrees is not an issue. Read the
Lindholm/Yellin book: you will find all the facts there - no Sun opinion,
but enough for anyone with a mind to make up his/her own mind.
Cheers
Bart Demoen
The main idea is that an external object has a representative on the
global stack (called the anchor). All references from Prolog data
go to this anchor. When the anchor becomes garbage, the external
object can be freed. When the anchor is created, it is always above
the last choicepoint, but it is trailed anyway (with a special undo-trail
frame). On failure, this trail frame causes the object to be freed.
On GC, when the GC discovers an undo-trail frame that points to an
otherwise unreferenced anchor, it frees the external object and collects
anchor and trail frame. I have written a little bit about it in the
TPLP submission that you should have access to, or see the comment in
this file
http://eclipse-clp.cvs.sourceforge.net/viewvc/eclipse-clp/Eclipse/Kernel/src/handle.c?revision=1.1&view=markup
>
>>> Let us look at I/O handles. Typically, you do not have that many of
>>> them, so you should collect them in time. How do you ensure that? Call
>>> the GC if you are out of I/O handles?
>> That would be the solution, except you would probably trigger
>> the GC when you are _almost_ out of handles.
>
> :-)
>
>>> Second issue is that you sometimes
>>> simply want to close an I/O channel. Ideally, that means forcing that
>>> all pending computation (choice-points and constraints) are `finished'.
>>> How do you do that?
>> I can't comment without seeing a clearer specification of that.
>
> What I'm referring to is that using setup_call_clean/3, you can make
> sure all work is being completed and the I/O channel is being closed
> by forcing determinism.
There are special cases where the liveness of a handle indeed
coincides with the lifetime of a choicepoint. For example in the
database interface, we have a nondeterministic retrieve_tuple
primitive which uses a database cursor. The cursor is only used
locally in that predicate, and is not needed after the predicate's
choicepoint is cut. Since cursors are precious, we want to free
them asap, rather than waiting for GC. So, yes, here we use
call_cleanup's idea of doing something at cut time, but it's done
optionally in addition to the default untrail and GC mechanism.
> You want to deal with coroutining.
That was just an example to illustrate the problem of carrying
some copy of the handle out of the scope of call_cleanup. It
could simply be that handle copies end up inside complex data
structures that are passed out of call_cleanup's Goal. My point
was just to say that in general there is no connection between
choicepoints and the lifetime of a handle.
> You want to deal with coroutining. That
> will will make it pretty hard to force termination.
Termination of what?
>
> A scenario I had in mind is that we had Prolog generating a file that was then
> passed to an external application. I used setup_call_cleanup/3, but the code
> was non-deterministic. As a result, the file remained open and on some
> unnamed OS we got a sharing violation. With setup_call_cleanup/3 and
> only choice-points to worry about, this is still quite tracktable, but
> with arbitrary coroutining?
>
> Cheers --- Jan
Cheers,
Joachim
I got the idea. Thanks.
> > You want to deal with coroutining.
>
> That was just an example to illustrate the problem of carrying
> some copy of the handle out of the scope of call_cleanup. It
> could simply be that handle copies end up inside complex data
> structures that are passed out of call_cleanup's Goal. My point
> was just to say that in general there is no connection between
> choicepoints and the lifetime of a handle.
That was pretty clear and a very valid critisism on
setup_call_cleanup/3.
> > You want to deal with coroutining. That
>> will will make it pretty hard to force termination.
>
> Termination of what?
I/O isn't closed as long as there are delayed goals on which the
termination depends. I guess I'm refering to call_residue and friends,
which you need to use to deal with possibly remaining goals. Of course
that is a general problem with coroutining.
Cheers --- Jan
P.s. Given threading, things will have to be more complicated. Handing
I/O handles to another thread is not uncommon (e,g., a server where
one thread does the accept() on a socket and then passes the new
handle to another thread for dealing with the new connection).
That was one of the reasons to use something similar to atom-handling
for handles, but atom-gc has other not-do-desirable features.
Thank you for your suggestion. The book you mention comprises about 473
pages (2nd edition). And a cursory look on its cover shows a
Java coffee cup on the right, and a Sun logo on the left side. It
is very well possible that as you say "no Sun opinion" is
inside. But on the cover I read "... from the Source (TM)". That does
not suggest to be the kind of third party opinion I am searching
for. And a cursory search for finalization does not show me any
critical remark in the book that could help me understand your
position.
I guess the ISMM proceedings might be even more voluminous.
And weighing there the proponents against the opponents
goes beyond anything I could do here in reasonable time.
So as long as I do not get any concrete citations of
statements of experts in this field - I can only note this
as your opinion on Java.
The hearsay I got was that many Java or .net programmers are
still in this phrase of transition giving up control on
memory. But that's just hearsay.
> So as long as I do not get any concrete citations of statements of
> experts in this field - I can only note this as your opinion on Java.
You underestimate my expertise in this field :-)
I still hope you will find the time to form your own opinion.
Bart Demoen
Maybe that helps: I do not even consider myself competent
enough to estimate your expertise in the field of Java
finalization - or .net for that matter. I try to collect
arguments that can help us to get a better view of the
subject - actually of setup_call_cleanup/3.
I do remember vaguely that some years ago - when you first
saw at that time call_cleanup/2 - you compared it with finialization
and put exactly the same argument forward, namely that in Java
it is something mistaken. That's why I reacted here.
Reference to expertise alone isn't it. We need the actual
arguments! Imagine a meeting:
A: So we shall vote on subclause 3.2.5.
B: Why?
A: Because 5 is from C, and C is an expert.
B: What are C's arguments?
A: C is a very big expert in the field of 3.2 and
of quinquesian subjects in general!
B: What properties does C consider desirable?
A: We shall not underestimate C's expertise in the field!
It does not work.
It could have been a mistake of me to implicitly assume
that you still believe that the analogy between
setup_call_cleanup/3 and finalization in Java is of relevance.
Nevertheless, for completeness please note that there might
be analoguous sitations! I am not speaking about a concrete
implementation. But it is imaginable that an implementation
triggers the cleanup due to a garbage collection that also
performs indexing. So far there is not a single system
doing this. But I keep an eye on the draft that such will
not be forbidden. Maybe sometime it will be done - maybe
that takes another 20 years. Maybe earlier.
>I still hope you will find the time to form your own opinion.
I am very wary of forming an opinion where I do not expect
to gain sufficient expertise to have something I would call
opinion. I rather prefer to get more experts in, who
can be questioned. Unfortunately many real experts
are pretty hesitant to help out. Some of them say: yes,
I use that feature - but I am no expert.
Joachim Schimpf <jsch...@users.sourceforge.net> writes:
>> This capability is now solved via a "plain control flow solution".
>
>No, it's not solved because, as I tried to explain, reachability
>cannot be inferred from the presence of choicepoints. Example:
>
>?- setup_call_cleanup(open(foo,write,S),freeze(X,write(S,X)),close(S)), X=2.
>ERROR: write/2: stream `$stream(1070183496)' does not exist
How much semantics information do you expect the control construct
shall handle? You think of freeze/2. What else? All kinds of
constraints? Or all constraints somehow attached to the stream?
How shall we specify which constraint is of relevance and which is not?
Clearly you have in mind here a much more versatile construct
that requires some kind of specification languages for the
relevant constraints.
setup_call_cleanup/3 is significantly simpler than that.
>> Compare this to the situation before! How was it possible
>> to execute a goal with certain resources open?
>
>I'm not saying that there is no problem to be solved, I'm saying
>that setup_call_cleanup/3 is a poor way of addressing it.
>The Prolog-like system I'm working on uses external resources
>_a_lot_, but thanks to the way it implements and garbage-collects
>handles for these resources, we have been happily living without
>such a builtin so far. [i/o streams have for historic reasons
>not been implemented using these handles, but we'll do it in the
>upcoming release].
In your approach - if I correctly understand it - you entirely rely on
garbage collection. There is thus no way for the caller to guarantee
somehow that a cleanup has been executed.
With setup_call_cleanup/3 the temporal interval where to call the
Cleanup is - relatively small. Most Cleanups can be executed
without ever invoking GC.
But as far as I understood you, you always have to wait for GC to
decide what will happen.
With setup_call_cleanup/3 things are simpler, but then your
generality cannot be covered by it.
>> And it turns out, that this new construct is now used for things
>> a little bit unanticipated - as we were completely bound
>> looking in one direction. Quelle bonne surprise !
>
>Rather a nightmare: the primitive leaks implementation-level
>information about presence of choicepoints into the program logic:
> setup_call_cleanup(true, Goal, Det=yes).
>Yes, I too want to have that information for implementing a
>debugger or toplevel, but in a standard language primitive?
The resources managed are often files. Files are not simply
read/written once, but sometimes they might be reopenend and
rewritten. Similarly for connections. Here you sometimes have to
guarantee that a file or a connection is closed. With the property
above you get that information easily.
If bindings of the cleanup are not shared with the outside,
programmers will find their own way of guessing. Like looking into
the list of open streams or some other unreliable way. They might
also use a global variable to effectively log the cleanups. So they
would reimplement part of the mechanism. That does not sound very
reliable to me.
>Even worse, a cut can now magically trigger a variable binding:
>?- setup_call_cleanup(true, (true;true), Det=surprise),
> writeln(Det), !, writeln(Det).
>_G835
>surprise
>Hardly desirable, and not something your average program analyzer
>would expect to happen!
That can happen. True. The sequence
... var(V), !, nonvar(V), ...
is even more to that point.
But which "average program analyzer" are you referring to? Average,
that means at least two, no?
>So my suggestions: if you want to have setup_call_cleanup/3 anyway,
>please at least get rid of these two nasty effects. This can be done
>by requiring that the Cleanup goal has only the instantiation at the
>end of Setup, and does not share variables with the context. The
>basic open/close examples all work with such a semantics.
>[I have put such a variant into ECLiPSe 6.0_102, lib(iso)]
That is great! Now we have 5 systems!
I only do not understand why
setup_call_cleanup(X=throw(ex), true, X).
succeeds with X = throw(ex) in ECLiPSe while I rather expected an
error due to the uncaught ex. How can you transmit bindings from
the Setup to the Cleanup?
>If you want a primitive to tell you about choicepoints, use one that
>doesn't spontaneously bind a variable, I can suggest something.
>But anyway, I don't think it belongs in the Prolog standard.
Here is another way of separating things:
One primitive that ignores constraints but shows the bindings. And
the other primitive allows to perform the complex - but absolutely
interesting operations you have in mind. I fear only that the second
primitive will not be accepted by many systems.
Compare just the weight of implementations! setup_call_cleanup/3 is
already complex - but in 2 weeks or so it can be implemented. (For
you it was probably much easier since you had all the mechanisms
already there.)