Juliusz Chroboczek <j...@pps.jussieu.fr> writes: > >> Obviously, tail recursion elimination is more than a mere > >> optimisation.
> GM> So write a macro.
> Gareth,
> You're missing my point. I know there are other ways. I know half a > dozen techniques for implementing FSMs.
> My point is that I like to implement them in a certain way, and that > the Common Lisp standard allows a conforming implementation to > mis-compile my code. Fortunately, CMUCL doesn't take this liberty, > which is usually good enough for me.
It doesn't mis-compile your code, it does exactly what it says it will do: it calls the function. I think Gareth had the right impulse, though: write a macro. It's just that WITH-STATE-MACHINE wasn't the right one. Write yourself WITH-TAILCALL-OPTIMIZATION. I did for Emacs when it was easier than changing my code to produce something other than n-mutual tail-recursive code. Though I admit to not having ported it to CL (I just continue to hand rewrite or leave it to CMUCL). It's something that would have been nice to have in the language, but since it doesn't *require* vendor support, and it's easy enough to do yourself, it's not that big of a deal.
I think this discussion is getting off-topic, and doesn't really apply to the point I was trying to make.
But I enjoy nit-picking about semantics, and will gladly join you.
BM> The only reason that terminates is if there's a stack overflow. BM> That's an implementation limitation, not part of the semantics. BM> Nowhere in the CL specification does it require that the stack BM> must have a limited size.
There are two ways of understanding a specification ``assuming unbounded resources'':
(i) assuming availability of potentially unbounded resources, said program does FOO; and
(ii) there exists a finite (but arbitrarily large) amount of resources within which said program does FOO.
Obviously, you're thinking in terms of (i), while I'm thinking in terms of (ii). (Exercice for the reader: specify a class of properties FOO such that (i) and (ii) coincide.)
BM> The virtual machine described by the standard has infinite memory,
I will grant you that most well-known formal semantic frameworks do not deal with resource tracking at all, and therefore use model (i). It is not clear to me, however, which model is used by the CL standard, or whether the CL standard is explicit about the issue in the first place. Perhaps Kent can enlighten us?
KP> I think in the above loop example, if I understand you (and I am KP> not sure I do), there is termination in both cases: in one case KP> due to a resource limitation on stack, and in the other case in a KP> resource limitation brought by the mean time between failure of KP> the host OS, its hardware, etc., which I assume is finite.
Fair enough.
I think we're back to a rather fundamental problem, which is that of program equivalence.
In practical terms, would you accept a definition of program equivalence that identifies a program which runs in constant space with one which runs in linear space?
I think that one can fairly argue for both answers. (``Fairly,'' in this context, means ``without invoking Adam Smith's Invisible Appendages.'')
Juliusz Chroboczek <j...@pps.jussieu.fr> writes: > KP> I think in the above loop example, if I understand you (and I am > KP> not sure I do), there is termination in both cases: in one case > KP> due to a resource limitation on stack, and in the other case in a > KP> resource limitation brought by the mean time between failure of > KP> the host OS, its hardware, etc., which I assume is finite.
> Fair enough.
> I think we're back to a rather fundamental problem, which is that of > program equivalence.
> In practical terms, would you accept a definition of program > equivalence that identifies a program which runs in constant space > with one which runs in linear space?
I'm not sure what "linear space" is unless you mean "linear with respect to some input". Maybe (following on the subject line) you mean "linear in the number of tail calls". But if I understand the question correctly, I think I think that it doesn't matter whether it's "linear" or even "O(ackerman(n))". What I think matters to me is that it's O(finite_valued_fn(n)). I distinguish O(infinity) as a non-constant that no finite valued function can keep up with. Put another way, even with infinite tape on your turing machine, I think you're not going to get to (foo) in (progn (loop) (foo)); and therefore I think removing (loop) and letting you get to (foo) is not a valid transformation. On the other hand, in (progn (bar) (foo)), if (bar) is an exprssion that will run any long but known-to-be-finite time with no defined effects (other than memory use or other things we don't quantify), I think I think it's ok to optimize to (progn (foo)).
I don't purport to be a mathemetician, but my understanding is that the question of a discontinuity than a mere question of resources. That is, a function whose value goes to infinity at a certain point is different than "gee, that function goes off my page of graph paper" and different even than "gee, I'll never be able to buy enough graph paper to show where that function tops out" to the point of "If I set out to buy graph paper sufficient to try to show where that function tops out, I would never do anything afterward because there is no end to it." Also, I don't think it's the same to say "I recognize that pursuing that purchase of graph paper is not worth doing; I'd better stop here and do something else." (which is what I'm saying, with emphasis on "STOP", meaning the program is done) and "I recognize that pursuing that purchase of graph paper is not worth doing, so I'll just blythely continue with the next step as if it didn't matter that I skipped an instruction and I'll pretend nothing that followed was contingent on my successful completion of the prior step" which is a very "C-language" thing to do but which is NOT what I see Lisp as being about.
> I think that one can fairly argue for both answers. (``Fairly,'' in > this context, means ``without invoking Adam Smith's Invisible Appendages.'')
> Obviously, you're thinking in terms of (i), while I'm thinking in > terms of (ii). (Exercice for the reader: specify a class of > properties FOO such that (i) and (ii) coincide.)
It seems to me that arguing about the flaws of limited memory is pointless when you accept limited execution time. These two may well coincide if the system halts execution when it runs out of memory, as when it invokes the debugger on the out-of-memory condition.
If there is no upper bound on the consumption of resources of a program, it is ipso facto _broken_, and no silly quibbling over which resource is exhausted _first_ can possibly change any semantics of said program.
It is _typical_ that this has to come up in relation to a Scheme concept.
/// -- Why did that stupid George W. Bush turn to Christian fundamentalism to fight Islamic fundamentalism? Why use terms like "crusade", which only invokes fear of a repetition of that disgraceful period of Christianity with its _sustained_ terrorist attacks on Islam? He is _such_ an idiot.
>> In practical terms, would you accept a definition of program >> equivalence that identifies a program which runs in constant space >> with one which runs in linear space?
KMP> I'm not sure what "linear space" is unless you mean "linear with KMP> respect to some input".
Given some problem with a well-defined notion of input size, and given two (correct) programs that solve this problem, one of which does so in bounded space, the other in linear space, can the two programs be considered as equivalent?
I think one can argue both ways. (Or else: the correct notion of equivalence depends on the application.)
KMP> my understanding is that the question of a discontinuity than a KMP> mere question of resources.
Both Barry and I have been very careful to avoid the term ``infinite''. We were speaking of unbounded resources.
When modelling a finitary process such as deterministic computation, infinity can only appear as the limit of an unbounded process. Thus, the question of whether the process is continuous cannot even be formulated: the very assumption that leads us to introduce the notion of infinity is that the process is continuous.
If you prefer, we were not even considering the notion of actual infinity. The largest (in some intuitive sense) notion we were willing to consider was that of potentially unbounded usage of resources: a usage that remains finite at any time, but with no a priori bound.
>> (``Fairly,'' in this context, means ``without invoking Adam Smith's >> Invisible Appendages.'')
KMP> This reference escapes me.
This modern form of mysticism, ``the market's invisible hand.''
Juliusz Chroboczek <j...@pps.jussieu.fr> writes: > >> In practical terms, would you accept a definition of program > >> equivalence that identifies a program which runs in constant space > >> with one which runs in linear space?
> KMP> I'm not sure what "linear space" is unless you mean "linear with > KMP> respect to some input".
> Given some problem with a well-defined notion of input size, and given > two (correct) programs that solve this problem, one of which does so > in bounded space, the other in linear space, can the two programs be > considered as equivalent?
> I think one can argue both ways. (Or else: the correct notion of > equivalence depends on the application.)
> KMP> my understanding is that the question of a discontinuity than a > KMP> mere question of resources.
> Both Barry and I have been very careful to avoid the term ``infinite''. > We were speaking of unbounded resources.
But the issue of (progn (loop) (foo)) is not a question of unbounded resources, it's a question of infinite resources. (foo) will never be reached.
> When modelling a finitary process such as deterministic computation, > infinity can only appear as the limit of an unbounded process. Thus, > the question of whether the process is continuous cannot even be > formulated: the very assumption that leads us to introduce the notion > of infinity is that the process is continuous.
I guess I'm just not mathy enough to make sense of this. In my parlance, (loop) is an "infinite loop". There isn't any ambiguity about this. Nor is there any ambiguity that (foo) in (progn (loop) (foo)) cannot be reached. Any application of mathematics which obscures this quite obvious fact is either accidental or intentional obfuscation, IMO. But whatever the motivation, it does not aid the understanding of the problem.
> If you prefer, we were not even considering the notion of actual > infinity.
Then in my parlance, you are not addressing the form '(loop)'.
> The largest (in some intuitive sense) notion we were > willing to consider was that of potentially unbounded usage of > resources: a usage that remains finite at any time, but with no a > priori bound.
This is trivially true of all computations but does not address the "meaning" of those computations. You don't understand infinity by walking a ways and saying "it seems finite to me" and then walking a ways farther and saying "it still seems finite" and so on. You understand infinity exactly by considering the fact that in the two step process "1. do infinite work, then 2. do something else finite" that no matter how much you work on step 1, you are no closer to doing step 2, and, moreover, understanding that step 2 will never be done. That is the definition of infinity. That is what (progn (loop) (foo)) says. If you have a mathematics that says it means something else, you are using that math in the wrong place.
> >> (``Fairly,'' in this context, means ``without invoking Adam Smith's > >> Invisible Appendages.'')
> KMP> This reference escapes me.
> This modern form of mysticism, ``the market's invisible hand.''
KMP> But the issue of (progn (loop) (foo)) KMP> is not a question of unbounded resources, it's a question of KMP> infinite resources. (foo) will never be reached.
Yes, we fully agree about that. There is no such thing as an actually infinite amount of work. This was emphatically not the issue I was trying to get you to comment on.
>> If you prefer, we were not even considering the notion of actual >> infinity.
KMP> Then in my parlance, you are not addressing the form '(loop)'.
We agree about that, I am definitely not.
As you say, there is no ambiguity. With any reasonable semantics, the programs (loop) and (progn (loop) (error "Foo!")) are equivalent.
On the other hand, consider the following:
(defun foo () (foo))
and suppose that you compile FOO without tail-call elimination. Does FOO terminate?
The obvious answer is that my machine has finite memory, hence FOO will run out of stack at some point, hence FOO terminates on my machine. This answer invokes a specific maximum stack size, and so is not acceptable as part of a language definition.
The answer that the best-known formal semantic frameworks give is that assuming a potentially unbounded stack, FOO will never run out of resources, hence FOO doesn't terminate. I claim that this answer is not realistic.
Finally, you can say that, assuming any arbitrarily large but finite amount of stack, FOO will terminate. As FOO terminates in all finite models, FOO terminates. I claim that this last answer is more realistic than the second one while avoiding mentioning a specific amount of available resources.
The point I'm trying to make is that FOO terminates when compiled without tail call elimination, and that FOO does not terminate if compiled with tail call elimination. Thus, tail call elimination is not a mere optimisation, but a semantic feature.
And, incidentally, a feature that I miss in my favourite programming language.
> and suppose that you compile FOO without tail-call elimination. Does > FOO terminate?
Why do you not distinguish between "terminate" and "signal an error"?
> Thus, tail call elimination is not a mere optimisation, but a semantic > feature.
This is just plain wrong. Your semantic feature rests on an equivocation between termination and crashing. I fail to see how this can reasonably be considered anything but _completely_ bogus. What is the _value_ of a crash? Rather, a crash is the antithesis of termination, because it _prevents_ every reasonably expectation from a termination. The same is true for a non-idiotic example which takes enough time to terminate that it can run out of some resources, like electric power or memory or disk space or whatever -- hell, that might even happen immediate after it has _started_ running. It is profoundly counter-productive to treat each and every cessation of execution the same way. I wonder what kind of silly theoretical framework has found it useful to abstract away the value of a computation so one could study the conditions for cessation of execution without concern for what the computation yields. It seems to be one of those useless theories that are used only to prove something that is wrong and that every reasonable person knows to be wrong, but which can be shown to be true if you accept a whole bunch of insane premises that have been divorced from the reality and purpose of computing.
Again, it is typical that this discussion centers around a Scheme feature.
/// -- Why did that stupid George W. Bush turn to Christian fundamentalism to fight Islamic fundamentalism? Why use terms like "crusade", which only invokes fear of a repetition of that disgraceful period of Christianity with its _sustained_ terrorist attacks on Islam? He is _such_ an idiot.
Juliusz Chroboczek <j...@pps.jussieu.fr> writes: > KMP> But the issue of (progn (loop) (foo)) > KMP> is not a question of unbounded resources, it's a question of > KMP> infinite resources. (foo) will never be reached.
> Yes, we fully agree about that. There is no such thing as an actually > infinite amount of work. This was emphatically not the issue I was > trying to get you to comment on.
> >> If you prefer, we were not even considering the notion of actual > >> infinity.
> KMP> Then in my parlance, you are not addressing the form '(loop)'.
> We agree about that, I am definitely not.
> As you say, there is no ambiguity. With any reasonable semantics, the > programs (loop) and (progn (loop) (error "Foo!")) are equivalent.
Normal order semantics are unreasonable? First I've heard of that.
> On the other hand, consider the following:
> (defun foo () > (foo))
> and suppose that you compile FOO without tail-call elimination. Does > FOO terminate?
> The obvious answer is that my machine has finite memory, hence FOO > will run out of stack at some point, hence FOO terminates on my > machine. This answer invokes a specific maximum stack size, and so is > not acceptable as part of a language definition.
It also assumes that each call to (foo) consumes some amount of stack. This is an unnecessary requirement.
j...@itasoftware.com writes: > > KMP> Then in my parlance, you are not addressing the form '(loop)'.
> > We agree about that, I am definitely not.
> > As you say, there is no ambiguity. With any reasonable semantics, the > > programs (loop) and (progn (loop) (error "Foo!")) are equivalent.
> Normal order semantics are unreasonable? First I've heard of that.
I believe the context was "reasonable semantics for CL" -- i.e., "reasonable transcription of the semantics which are present and fixed for CL even if not specified formally". The absence of a formal semantics presentation is not license to do new design.
It's been a long time since I had to deal with these. Is "normal order" the one where you don't have to do things in order? If so, you should remember that "reasonableness" is like "goodness"--it depends on context and is not a universal property of the universe. In CL, evaluation works left to right, so you have to first dispense with one form to get to the next. If you are unable to dispense with a certain form, you're done.
> > On the other hand, consider the following:
> > (defun foo () > > (foo))
> > and suppose that you compile FOO without tail-call elimination. Does > > FOO terminate?
> > The obvious answer is that my machine has finite memory, hence FOO > > will run out of stack at some point, hence FOO terminates on my > > machine. This answer invokes a specific maximum stack size, and so is > > not acceptable as part of a language definition.
The language definition does not call "out of astack" a semantic erorr but a resource limitation. Read about the difference between the classes SERIOUS-CONDITION and ERROR. It is not an error to do this, it just probably will result in a serious condition (stack resource xhausted); though it might result also in lots of time passing and then the machine having to be rebooted; that's less likely to get trapped but is also a resource limitation. Neither is a semantic error per se.
> It also assumes that each call to (foo) consumes some amount of stack. > This is an unnecessary requirement.
It doesn't matter. CL semantics deal neither with stack nor time, so either of those commodities can run out and you have no control of it. This is not to say those issues are not important, but they are left, as a matter of language design, to the vendor. A vendor is not prohibited from doing tail call elimination, they are simply not required to.
What is not left to the vendor is the right to hop over inconvenient statements in order to get to one that is more convenient...
> > > KMP> Then in my parlance, you are not addressing the form '(loop)'.
> > > We agree about that, I am definitely not.
> > > As you say, there is no ambiguity. With any reasonable semantics, the > > > programs (loop) and (progn (loop) (error "Foo!")) are equivalent.
> > Normal order semantics are unreasonable? First I've heard of that.
> I believe the context was "reasonable semantics for CL" -- i.e., > "reasonable transcription of the semantics which are present and fixed > for CL even if not specified formally".
Ah, I was thrown by your use of the work `any'.
> It's been a long time since I had to deal with these. Is "normal > order" the one where you don't have to do things in order?
Technically, normal order is when you reduce the leftmost reducable redex at each step. You can look upon this as a `lazy evaluation' model. When you have no more reducable redexes, you have found the `normal form'. Both normal order and applicative order will reduce to the same normal form (when both reduce), but there are some expressions for which applicative order reduction won't reduce.
EN> Why do you not distinguish between "terminate" and "signal an error"?
[...]
EN> This is just plain wrong. Your semantic feature rests on an EN> equivocation between termination and crashing.
No. It rests on *distinguishing* between non-termination and the signalling of STORAGE-CONDITION.
Suppose you identify signalling a condition and not terminating, i.e.
(loop)
and
(error "Foo!")
are equivalent.
Then (assuming a compositional semantics) you have to identify
(progn (ignore-errors (loop)) t)
and
(progn (ignore-errors (error "Foo!")) t)
If you choose to identify the latter program with T (a rather natural hypothesis), and the former one with (LOOP), then you've just identi- fied (LOOP) with T. With a little more work (and some similarly natural hypotheses), you've just identified all Lisp programs.
Not a very interesting semantics.
Juliusz
P.S. Okay, I've cheated. Whether a stack overflow generates a STORAGE-CONDITION or causes a nuclear holocaust is ``implementation- defined'' in ANSI Common Lisp. Still, I think it's natural to distinguish between a ``crash'' and non-termination.
The accepted terminology in the Semantics community, by the way, is to include abnormal termination under the term ``termination''.
> The accepted terminology in the Semantics community, by the way, is to > include abnormal termination under the term ``termination''.
That seems like a very good way to cheat yourself out of a large number of a really hairy problems. Cleaning up such things is not interesting.
/// -- Why, yes, I love freedom, Mr. President, but freedom means accepting risks. So, no, Mr. President, I am not with you, and I am not with the terrorists. I would be happier if you left the airports alone and took care of all the cars that are a much bigger threat to my personal safety than any hijacking.
KMP> This is not to say those issues are not important, but they are KMP> left, as a matter of language design, to the vendor. A vendor is KMP> not prohibited from doing tail call elimination, they are simply KMP> not required to.
I understand that. I wasn't complaining about the vendors' behaviour, but about the guarantees given by the standard being too weak for my personal programming style.
People like you have taught me to program by the book, not for the implementation. The lack of strong guarantees about tail-call elimination in the book forces me to program for the implementation (or to give up on a number of techniques that I happen to find useful).
Of course, I am not implying that you (plural) have done anything wrong when designing ANSI Common Lisp. I am quite willing to believe that this was the right compromise at the time (possibly for the very same reasons as the ones you gave recently for the omission of a MOP).
> I think this discussion is getting off-topic, and doesn't really apply > to the point I was trying to make.
> But I enjoy nit-picking about semantics, and will gladly join you.
> BM> The only reason that terminates is if there's a stack overflow. > BM> That's an implementation limitation, not part of the semantics. > BM> Nowhere in the CL specification does it require that the stack > BM> must have a limited size.
> There are two ways of understanding a specification ``assuming > unbounded resources'':
> (i) assuming availability of potentially unbounded resources, said > program does FOO; and
> (ii) there exists a finite (but arbitrarily large) amount of resources > within which said program does FOO.
Lisp needs both (i) and (ii) togetether.
> Obviously, you're thinking in terms of (i), while I'm thinking in > terms of (ii). (Exercice for the reader: specify a class of > properties FOO such that (i) and (ii) coincide.)
> BM> The virtual machine described by the standard has infinite memory,
> I will grant you that most well-known formal semantic frameworks do > not deal with resource tracking at all, and therefore use model (i). > It is not clear to me, however, which model is used by the CL > standard, or whether the CL standard is explicit about the issue in > the first place. Perhaps Kent can enlighten us?
Necessarily because the language is reflective, it uses a hybrid model for which I'm pretty sure your semantics just doesn't apply. The standard is expressed in English and it is the burden of anyone who wants to apply any out-of-band lemmas from some other domain to CL to first show that they have a correct semantic description of the language.
We specifically chose English because it was democratic and accessible to the programming community we needed to reach, such as myself, for example. I am far from the "guiding force" behind CL. I was there, I had a lot of opinions that got incorporated, and I was the "neutral editor" once votes were taken. But people like Steele and Gabriel were there and fully aware of enough issues of formal semantics that they could have objected at any time if we were too far afield. My own personal understanding of semantics says that something is "semantically well-formed" if I think I can read it and understand it and implement it without ambiguity. I'm sorry that's probably not very scientific, but I've found it operationally useful over the years. The things I intuitively see as semantically well-formed seem to be exactly the set of things the math guys are happy describing, given that they ever take the patience to properly describe it and don't just dive in with prefab scaffolding of some system they like to use and hope it works in the current situation.
The system we use describes BOTH a virtual semantics in which you can recurse indefinitely and not worry about stack, in which you can cons indefinitely and not worry about heap, etc. AND a practical semantics in which you can limit these things and still be a correct implementation. A conforming implementation must not tell the user he has made a semantic violation if he depends on stack to be too deep--SERIOUS-CONDITION is ok, but ERROR is not. Ditto for consing too much, though handling that case is going to be hard for other reasons. Still, it doesn't make an implementation non-conforming.
(ignore-errors (loop)) does not terminate.
neither does
(defun foo () (foo)) (ignore-errors (foo))
The latter, though, creates a resource violation in some (but not all) implementations. This is permitted. And Lisp is reflective, so the resource violation is permitted to be signaled. It would be an unuusal Lisp that signaled a resource violation for (ignore-errors (loop)) but technically I think signalling a TIME-EXHAUSTED situation would be permissible, as long as it was a serious condition and not an error. It should not be trapped by IGNORE-ERRORS.
Lisp does allow even resource limitations to be trapped, so any formal semantics you apply must be one that deals not only with bottom and its infections nature compositionally, but also with some operator that can trap bottom and keep it in check, at least some of the time. My intuitive sense is that this is overpowerful and probably gets messy becuase it's got analogies to a system with both an unstoppable force and an infinite barrier. One has to give. Probably it will be the unstoppable force, since writing infinite barriers probably can't keep lisp from stopping. But conceptually, Lisp creates a reflective battleground in which the struggle is permitted to take place. And if it's uncomputable who will win, that's too bad, because in practice it's still worth trying to navigate this space in the name of robustness. I would not want a weaker language just becuase it promised more deterministic behavior.
Lisp is a hybrid of your modes (i) and (ii) above because it is intended both to recognize an idealized semantics it may not be able to implement, and also to accept its own limitations. (In this way, it's probably not that different from being religious, for those of you who are.) It is both trying to enforce the ideal semantics but also to still HAVE a semantics in the case where its semantics are violated. In this regard, it is partly reflective. It is not 3lisp nor some other dialect that purports to be wholly reflective, but it plainly involves some of the complex issues that come up in such situations, and embracing it with the same tools for semantics that you use for other languages is not meeting it on its own terms, I think. Lisp has traditionally begun by trying to figure out what it wants to do and leaving it to the theorists to catch up; the problem with working the other way around is that you always have a language which is limited by the knowledge of the day it was designed and only gets old from there; Lisp continues to challenge the theorists even after its design, which may be puzzling but I regard as appropriate.
Juliusz Chroboczek <j...@glory.dcs.ed.ac.uk> writes: > EN> Why do you not distinguish between "terminate" and "signal an error"?
> [...]
> EN> This is just plain wrong. Your semantic feature rests on an > EN> equivocation between termination and crashing.
> No. It rests on *distinguishing* between non-termination and > the signalling of STORAGE-CONDITION.
But why? STORAGE-CONDITION merely signals a lack of resource in the ability to wait. It is not predictive of whether the waiting would have done the same thing; it is utterly agnostic on the issue. No implementation is required to stop. Implementations are permitted to do tail call elimination.
> Suppose you identify signalling a condition and not terminating, i.e.
> (loop)
> and
> (error "Foo!")
> are equivalent.
They don't do the same thing. There are situations in which they are they are operationally the same and situations in which they are not.
> Then (assuming a compositional semantics) you have to identify
I don't know what a "compositional semantics" implies here--sounds like it means "a sequence of words not included in the spec". Would that be a fair interpretation? Lisp is a partly reflective language, and error invokes the reflective mechanism, while LOOP does not. So there are many ways in which the two are alike and some ways in which they are not.
> (progn (ignore-errors (loop)) t)
> and
> (progn (ignore-errors (error "Foo!")) t)
This is an example of how the two are not alike.
> If you choose to identify the latter program with T (a rather natural > hypothesis), and the former one with (LOOP), then you've just identi- > fied (LOOP) with T. With a little more work (and some similarly > natural hypotheses), you've just identified all Lisp programs.
I don't know what this "identified with" stuff here is, but it's not in the language definition, so it's really just not relevant. I see this as an improper application of the mathy game of "deltas and epsilons" where you have played deltas but are obliged to play epsilons. It's your burden to show that your reasoning conforms to the language defintiion, which is democratically expressed in English and open to all to understand; the language is not offered in some elitist mathy formal semantics and that means that attempts to apply such semantics should be made only where you can prove that it such semantics is appropriate. That as contrasted with Scheme where the language definition is offered in two allegedly redundant forms, but where if there is ever a dispute a certain part of the public will be locked out of it because they will be overwhelmed by obscure notation. In the design of CL, we specifically addressed the idea of such redundancy and decided it was a bad plan.
> Not a very interesting semantics.
You're welcome to conclude as you like (though you somewhat put the lie to the claim by continuing to discuss it... sort of like someone claiming that Jerry Springer is boring TV but tuning in every day to watch the fist fights). We're busy building programs are interesting, not semantics that are interesting. Our dull semantics has not seemed to impede that.
> Juliusz
> P.S. Okay, I've cheated. Whether a stack overflow generates a > STORAGE-CONDITION or causes a nuclear holocaust is ``implementation- > defined'' in ANSI Common Lisp. Still, I think it's natural to > distinguish between a ``crash'' and non-termination.
I don't get it. IGNORE-ERRORS doesn't trap stack overflow and it doesn't trap infinite loops. They are both the same. They are resource limitations.
ERROR is trapped because it involves a semantic violation.
> The accepted terminology in the Semantics community, by the way, is to > include abnormal termination under the term ``termination''.
Too bad, but you are misapplying the semantics of the semantics community. The "abnormal termination" here is due to an inability of the processor to have the resources to supply the correct semantics. The correct semantics allow the program you want. HOWEVER, a processor is considered conforming for not having those resources. The following is the definition of a completely conforming common lisp processor; it's not likely to be marketworthy, but it is allowed to claim conformance:
It probably also dumps out into a killer-small hello world, by the way, for those who like to optimize such things and worry that CL has missed that boat.
CL has the notion of an ideal semantics separate from the notion of an implemented semantics. I'm sorry if the semantics community doesn't like that, but they'll just have to deal. We describe arbitrary precision integers but we don't require all conforming processors to have the memory needed to house them. We assume describe programs that can have arbitrary stack depth but we leave it to implementations to sort out how deep the stacks really are and whether they are extensible. We SAY that these are not to be identified as errors but as resource limitations.
We say programs in CL have a meaning independent of the question of whether processors will reliably run them. Processors for CL are intended to try their best to meet that meaning, but are allowed to fail under certain cases and still be conforming. It is specifically left to the market to sort out whether this is worth money to buy. We figured that was a much better test of worthiness since in a pluralistic society we expect there to be cases where one solution is ok for some and not for others and vice versa... Some people will want implementations with tail call elimination, some won't. I don't, for example. You do. So we'll prefer different implementations. We can still agree on the meaning of the programs that might different in behavior between them, though. And we vendors can decide whose money they want, without losing their right to claim conformance. Seems ok to me...
If you run your (loop) example on a laptop running on batteries you will see in not so long time that even (loop) is doomed to "terminate" because some resource is exhausted.
>> No. It rests on *distinguishing* between non-termination and >> the signalling of STORAGE-CONDITION.
KMP> But why? STORAGE-CONDITION merely signals a lack of resource in KMP> the ability to wait. It is not predictive of whether the waiting KMP> would have done the same thing; it is utterly agnostic on the KMP> issue. No implementation is required to stop. Implementations KMP> are permitted to do tail call elimination.
Sure.
The question is the other way around: would it make sense for a hypothetical future standard to require that a given piece of code is *not* allowed to signal STORAGE-CONDITION or crash? In other words, is there a technically correct way to *mandate* tail-call elimination in a (hypothetical) standard of the quality of ANSI CL.
My claim is that there is; but this claim relies fundamentally on the fact that STORAGE-CONDITION *is* different from looping.
In the rest of the discussion, you would appear to have dualised my position: I am claiming that non-termination and signalling a condition (or crashing) *are* different results...
>> (progn (ignore-errors (loop)) t)
>> and
>> (progn (ignore-errors (error "Foo!")) t)
KMP> This is an example of how the two are not alike.
...which you would appear to at least partly agree with.
Juliusz Chroboczek <j...@pps.jussieu.fr> writes: > >> No. It rests on *distinguishing* between non-termination and > >> the signalling of STORAGE-CONDITION.
> KMP> But why? STORAGE-CONDITION merely signals a lack of resource in > KMP> the ability to wait. It is not predictive of whether the waiting > KMP> would have done the same thing; it is utterly agnostic on the > KMP> issue. No implementation is required to stop. Implementations > KMP> are permitted to do tail call elimination.
> Sure.
> The question is the other way around: would it make sense for a > hypothetical future standard to require that a given piece of code is > *not* allowed to signal STORAGE-CONDITION or crash? In other words, > is there a technically correct way to *mandate* tail-call elimination > in a (hypothetical) standard of the quality of ANSI CL.
I think you're really asking me whether it is a valid thing to want implementations to do, and I think "sure". But if your question is literally, as worded, "should standards be the mechanism by which this is achieved", I'm less sure.
I will say that I have in the past advocated that the standard would be more useful if it told you things like "the worst case time in doing a AREF will be O(1)" vs "the worst case time in doing ELT will be O(n) for lists of length n or O(1) for arrays". There are places where we've left it open and as a consequence an implementation can do really stupid things that are baffling to users. SET-FILE-POSITION was discussed recently; some people expect it to be O(1) and if it's O(n) [for n=filepos], that's pretty scary. But the consensus was that this should be left to an implementation.
To some extent, one might characterize my position as "there are bigger fish to fry than tail recursion" or one might say "it's just not consistent with the overall design to talk about memory use when nothing really does". You might appeal back to the "infinity" vs "finite" argument and say it was particularly important for the tail call case; I don't know. I'd rather tail call elimination be under OPTIMIZE declaration control and not a required feature of the language, so that kind of issue would need to get factored in, too.
And then there's the practical matter that the current climate is that none of this rises to the level of it being worth it to the community to change the standard. Merely opening the standard for the fixing of ANY technical item exposes the process to the risk of major politicking; it is not allowed to just make a work item to make a small correction under ANSI rules, I believe. I think you have to just open the spec for change and then other things you didn't expect might creep in. Stability is best maintained by not touching it, and there's nothing now that cries out as needing fixing.
I'd almost say that if something DID come up that required fixing, ANSI would not be the vehicle of choice to do it.
The reason I think people wanted to leave speed issues entirely out of the standard is that the market is really better at sorting things out. Let's take the example of the MOP. It's not standard either, but consensus seems to me to be that it's ok to use because most implementations do have it. Other things, like tail call elimination, haven't even been voluntarily adopted by implementations even though they are allowed to. If this came back to ANSI and we operated as we have done in the past, the first thing that would be asked when your hypothetical proposal to define this came to the table would be "what is the current practice?" and the answer would be "no one does it". And that would immediately be translated to statements like "sounds dicey" and "sounds like it will impose cost on all vendors that they don't feel a need for" and "sounds like there is no call for it".
The first stage of making the change is not to look to the standard unless the standard forbids the change. The first stage is to get vendors to experiment, and then to drive the wedge by asking that code that runs in one vendor run in another. Best, I think, would be to operate under a switch so that code that did this had to self-identify and other compilers could minimally say "I don't support that option, your code will likely lose" rather than quietly compiling the code and then losing on it at runtime...
> My claim is that there is; but this claim relies fundamentally on the > fact that STORAGE-CONDITION *is* different from looping.
> In the rest of the discussion, you would appear to have dualised my > position: I am claiming that non-termination and signalling a > condition (or crashing) *are* different results...
> >> (progn (ignore-errors (loop)) t)
> >> and
> >> (progn (ignore-errors (error "Foo!")) t)
> KMP> This is an example of how the two are not alike.
> ...which you would appear to at least partly agree with.
Juliusz Chroboczek <j...@pps.jussieu.fr> writes: > The question is the other way around: would it make sense for a > hypothetical future standard to require that a given piece of code is > *not* allowed to signal STORAGE-CONDITION or crash? In other words, > is there a technically correct way to *mandate* tail-call elimination > in a (hypothetical) standard of the quality of ANSI CL.
Defining tail call elimination is trickier than most people expect. In fact, although Scheme has for a long time expressed the intention to be "properly tail recursive", only the most recent report (R^5RS) makes an attempt at defining this concept, and for the formal definition it refers to a paper by Clinger(1998): "William D Clinger. Proper tail recursion and space efficiency."
In the abstract of this article he says: "Proper tail recursion is not the same as ad hoc tail call optimization in stack-based languages. Proper tail recursion often precludes stack allocation of variables, but yields a well-defined asymptotic space complexity that can be relied upon by portable programs."
-- Lieven Marchand <m...@wyrd.be> She says, "Honey, you're a Bastard of great proportion." He says, "Darling, I plead guilty to that sin." Cowboy Junkies -- A few simple words
Lieven Marchand <m...@wyrd.be> writes: > Defining tail call elimination is trickier than most people expect. In > fact, although Scheme has for a long time expressed the intention to > be "properly tail recursive", only the most recent report (R^5RS) > makes an attempt at defining this concept, and for the formal > definition it refers to a paper by Clinger(1998): "William D > Clinger. Proper tail recursion and space efficiency." [...]
I can hear some people saying "how could it be complicated?". For those who are looking for at least one concrete example, I believe the following kind of thing was raised as ambiguous even under the Scheme spec:
(defun (factorial x n) (if (zerop x) n (let ((result (factorial (- x 1) (* x n)))) result)))
As should be obvious, this affects the whole issue of how the language treats variables--i.e., is it just "allowed" or is it actively "required" to fold uses of variables, because this in turn forces implementations to do some code analysis that they might not be prepared to do, especially in an interpreter, the major point of which is to have lazy application of semantics by analyzing things only locally as they are encountered; this would effectively push an interpreter closer to a compiler, almost calling into question whether an interpreter was even allowed.
Of course, you could just say it wasn't a tail call, but some people are apparently offended by that because they want (let ((x y)) x) and y to be semantically the same thing. If introducing such bindings creates unwanted additional effects (beyond just naming a quantity), it inhibits the usefulness of code transformations--or, at least, makes such transformations a lot harder to write.
[Note that I often beat people up for wanting to apply certain optimizations that are not part of the language and complaining that they can't, but this is different: we're not in the context of "how do I optimize the fixed language?" but rather "how should I define the language such that optimizations I know of will be applicable?".]
Anyway, I hope this brief overview gives the sense that this issue of tail call elimination isn't just a "no brainer" that can just be dropped into the language spec trivially. It's much easier to have an implementation maintainer take on the responsibility because that maintainer is financially responsible for any impact on the usability of their own implementation. For the language designers to make the same choice, they have to be sensitive to revenue to implementations they don't own, and that's a considerably bigger responsibility. ANSI defines away such responsibility legallyl by allowing organizations to freely join and vote, and by requiring even minority parties that are economically impacted to be seriously treated. And the result of the vote the last time around was that there was even majority concern ...
Juliusz Chroboczek <j...@pps.jussieu.fr> writes: > The question is the other way around: would it make sense for a > hypothetical future standard to require that a given piece of code is > *not* allowed to signal STORAGE-CONDITION or crash? In other words, > is there a technically correct way to *mandate* tail-call elimination > in a (hypothetical) standard of the quality of ANSI CL.
Why not instead have an explicit (TAIL-CALL (F X)) special form, or otherwise two special forms (TAIL-APPLY #'F (LIST X)) and (TAIL-FUNCALL #'F X) ? It would seem much more in line with the rest of the Common LISP tradition: if it has a special, different meaning, then have a special, different, syntax for it, that signals the difference.
Yours freely,
[ François-René ÐVB Rideau | Reflection&Cybernethics | http://fare.tunes.org ] [ TUNES project for a Free Reflective Computing System | http://tunes.org ] *EULA: By reading or responding to this message you agree that all my stated opinions are correct.* "EULA" -- patent pending.
Francois-Rene Rideau <fare+NOS...@tunes.org> writes: > Why not instead have an explicit (TAIL-CALL (F X)) special form, > or otherwise two special forms (TAIL-APPLY #'F (LIST X)) > and (TAIL-FUNCALL #'F X) ? It would seem much more in line with > the rest of the Common LISP tradition: if it has a special, > different meaning, then have a special, different, syntax for it, > that signals the difference.
Because we already have DO and LOOP for that.
I suppose someone could write macros that transformed these tail-apply and tail-funcall -containing forms into DO or LOOP forms, but that sounds to me like part of the process of writing a compiler. :)
-- -> -/- - Rahul Jain - -\- <- -> -\- http://linux.rice.edu/~rahul -=- mailto:rahul-j...@usa.net -/- <- -> -/- "I never could get the hang of Thursdays." - HHGTTG by DNA -\- <- |--|--------|--------------|----|-------------|------|---------|-----|-| Version 11.423.999.220020101.23.50110101.042 (c)1996-2000, All rights reserved. Disclaimer available upon request.
> > Why not instead have an explicit (TAIL-CALL (F X)) special form, > > or otherwise two special forms (TAIL-APPLY #'F (LIST X)) > > and (TAIL-FUNCALL #'F X) ? It would seem much more in line with > > the rest of the Common LISP tradition: if it has a special, > > different meaning, then have a special, different, syntax for it, > > that signals the difference.
> Because we already have DO and LOOP for that.
(defun foo (n) (format t "There are ~D stack frames left~%" n))