I am really astonished to hear protection keys being thought
of as "brittle" under transformation: that is just the opposite
of what they are about!
Sorry to astonish you. :)
No problem!-) I was just afraid of the idea being misunderstood
or being associated with fear-inducing phrasing, either of which
can easily derail a discussion in which good ideas can otherwise
speak for themselves.
By the time we have agreed to what extent the concerns remain
valid or can be alleviated, the rest of the community may well
have stampeded away in panic! Not that this group necessarily
works that way, of course, just general human nature:-)
- Berkling's protection keys add flexibility to name-based
schemes, *removing* their brittleness under transformation;
it combines suitability for automated transformation
with readability for humans (in fact, both de Bruijn's
and the classical name-based scheme are special cases
of Berkling's)
Well, you're just asserting that Berkling's keys remove their
brittleness without evidence.
Ah, ok - I have been trying to summarize the idea on its own,
using evidence-by-logic rather than by-reference or by-example,
to avoid going through the history of Berkling style reduction
systems on this list (it has been a while, but I would be happy
to explain a bit if that would be of interest/appropriate here[*]).
From your other comments I infer that you understand how
the scheme works but also that you are still lumping together
the various related schemes that have been used in this context.
The latter might keep you from seeing what is different about
Berkling's scheme, and hence how the brittleness is removed,
by straightforward technical means (no magic or subjectivity).
Since this seems to be relevant information for a language-
specific design/spec discussion list talking about bindings, I'll
try to outline the process (even as a rough proof outline, it
beats more examples, as far as evidence is concerned). For
those of you not interested in reduction: the same issues
arise in refactoring. From memory (please excuse any errors):
Church's original lambda-K calculus was meant to pin
down the scoping issues that mathematicians and logicians
encountered; it had partially defined conversion rules (in
case of naming conflicts, the rules were simply not
applicable), so one had to choose a suitable sequence of
renamings (alpha rule) and conversion (beta rule) to
ensure that such conflicts and rule failures were avoided.
That is brittle in the strictest sense.
Later presentations of lambda-calculus mixed a bit of
renaming into conversion (either in beta reduction or in
substitution). That leads to uncontrolled renamings of
bound variables, to bypass the conflicts. That is not nice,
and it is brittle wrt programmer-chosen names.
Berkling's version just acknowledges that lambda calculus
is missing some of the terms that would allow to represent
the binding structures that can arise during conversion.
The consequence of having too few terms to represent
all intermediate results of reduction is that one either has
to avoid the conversion sequences that lead into those
holes (leading to partially defined reduction rules), or
one has to put the available terms to double use
(leading to conflicts between programmer intent and
implementation needs, as far as names are concerned).
Adding the missing terms gives the manoeuvring room
to define the conversion rules fully, without gaps and
without having to resort to renaming. So brittleness
under transformation, in both senses, is avoided.
De Bruijn's scheme doesn't help there: it has even fewer
terms than Church's (only a single term representing all
alpha-equivalent ones). Similar shortcomings seemed to
apply to most other related schemes I've seen - most of
them work technically but fail pragmatically.
Berkling's extra terms do not add error-prone complexity
in terms of binding structure, because other terms with
equivalent binding structure already exist in Church's
version. The extra terms are just designed in such a way
that both binding structure and programmer-chosen
names can be preserved during program transformations.
And I'm afraid I've seen enough evidence in my experience
to make me doubt your claim.
Does the above help? We have to be careful not to confuse
"the new thing is different" with "the new thing is brittle".
In this case, "the new system has more complex rules" can
actually mean "the new system is easier to use in practice",
because the new rules have fewer exceptions.
Just take a look at what happens to the definition of alpha-
conversion in a language with Berkling's keys. Traditionally,
when you alpha-convert a binding x, you can stop the
induction whenever you hit an inner binding (shadowing)
of x. Not so with your proposal: now you have to keep going
-- and decrement an accumulator argument to the algorithm
(not exactly obvious!) -- to look for "outer" references within
the inner binding.
Indeed. That is a direct consequence of adding the "missing"
terms (terms in which parts of the binding structure are
expressed through protection keys) - the old conversion
rules arise out of the new ones as (premature) optimizations
for the case that no protection keys are present at any stage.
But that the rules have to do more work to cover the additional
terms does not make the system "brittle" in any way - and it is
quite unambiguous what needs to be done.
Also, the shortcuts can be recovered as compile-time
optimizations: if your program starts out without protection
keys, the old rules still apply; and the new rules only introduce
protection keys if the old rules would have failed. So, if you
want to stay in the protection-free subset of the language,
you can do so. You do not have to pay if you don't use the
additional flexibility.
In fact, even the "extra work" is misleading: we could represent
the same binding structure by choosing different names, and
then the corresponding alpha-conversion/renaming would
have to do the same amount of work.
Example: if we enter a new function body, the outer 'this' is
not accessible directly, so any renaming of 'this' (if it were
feasible) could stop there; but since programmers have
adapted to that by renaming 'this' to 'that' or 'self' or ..,
we wouldn't be renaming 'this', we would be renaming
'that' or 'self' or .., and _that_ renaming would have to
'look for "outer" references within the inner binding'.
It needs a shift of perspective to see the old system as an
incomplete realization of the lambda-calculus ideas, but
once that is done, it seems clear why and how the old
system is brittle (missing terms, partially defined rules)
and how the new system is more complete/consistent.
I tend to mention de Bruijn not just because his scheme
predates Berkling's but because it helps people to
understand how protection keys work. However, since
you already understand that, I recommend that you
think about the differences to de Bruijn, rather than
the similarities; in particular, that shift-of-perspective
is a rewarding exercise!
Since it has been so long, allow me to refer to a younger
self:-) Section "2.2 lambda-calculi" in [1]. That was just
my own way of formalizing Berkling's system as part of
the introductory background for my own work.
As such, it lacks formal proofs and details, but Figures
2.2-2.5 spell out what you describe, and Theorem 2.1
outlines how Church's calculus emerges if one omits
terms involving protection keys. As an introduction,
it is way too brief to touch on the wider world of
Berkling's ideas[*], but it has the advantage of being
readily available online.
What does this mean in practice? It means that someone
refactoring at an outer level may be unaware that they've
affected the index of an inner reference by changing a
binding.
Then that is not a refactoring, is it?-) I see what you mean,
but you already have that problem if you add or remove
a binding now. As you said yourself: the binding structure
is just represented differently.
It means you lose some of the locality that you
get with traditional lexical scope.
<off topic>
It would actually be possible to recover even the bit of locality
that you refer to, but that would need a more drastic language
change (the non-locality comes because the protection keys
are attached directly to variable occurences, so adjusting
protection is non-local in the same sense as traditional
substitution is; the remedies for both non-localities work
along similar lines: propagate the information step-by-step
instead of all-at-once, move representations from meta-
language to object-language).
I doubt that this generalization would ever fly with pragmatic
programmers, but for language designers and implementers
it is interesting: if we make the protection key manipulation
explicit in the language (rather than the meta-language, as
most formalizations do), we can perform compile-time
optimizations on them (similar to compile-time garbage
collection or type-checking).
I seem to recall that I once tried to derive the more efficient
supercombinator reduction from the more general beta
reduction rules (if we make the protection key manipulations
explicit instead of pushing them right down to variable
occurrences, some of them cancel out, and never need
to be pushed into the expression).
</off topic>
A machine is much better at consistently shifting big
collections of integer indices without making a mistake.
Yes, also at managing scopes during refactoring. I prefer
tool support for refactoring (I was part of the team that built
such for Haskell98[2]), but given Javascript's nature, it would
be hard to prove any refactorings correct. So one will have
to resort to a combination of tool support and testing - still
preferable to manual refactoring (which most of us are
doing so far).
And refactoring tools can profit from having a programming
language in which program transformations can be expressed
without having to force renamings.
The problem is wider than 'this', 'this' just seems to be
the main use case where the problem surfaces in
Javascript practice at the moment.
And the reason for this is that |this| is implicitly bound.
Which, as I said in my last email, is a good motivation
for considering making it possible explicitly bind |this|.
I'm not against such attempts: thinking of 'this' as implicitly
bound is already much nicer than thinking of it as dynamically
bound, and being able to make the implicit binding explicit
(so that we can choose another name than 'this') would be
a nice option to have, even if protection keys had any chance.
However, any changes in that area would also touch the
core of what Javascript is about. There is no free lunch: if
the core language needs to be changed, that is going
to have wide-ranging effects. That does not mean that
such a change is not worth the cost.
Btw, have you ever wondered whether 'var'-bindings
are recursive?
This is a really basic question about JS, and not appropriate
for this list.
Ah, perhaps I was too implicit. Let me make the design issue
clearer, and how it relates to our discussion:
function Outer() {
var prefix = "Outer>";
function Inner () {
var prefix = prefix+"Inner>"; // this does not work;
// neither the outer nor the inner definition are
// accessible on the rhs, only the *uninitialized*
// inner binding
}
}
I know definition constructs in which the left-hand side is
available on the right-hand side (recursive value definitions
in other languages, function definitions in Javascript), and
I know definition constructs in which it isn't, but any previous
value is (assignment in Javascript, non-recursive definitions
in other languages); in all these constructs, either the current
or the previous definition is available on the right-hand side;
Javascript's 'var' is the only construct I know (apart from
memory-model bugs in Java, perhaps) in which the
left-hand side name is forced to be undefined on the
right-hand side; which means that neither the current nor
the previous definition are available on the right-hand
side, because the undefined state shadows both!
One could argue that the interim state (bound, but not
initialized) should not be exposed in an initializer, and I
would agree, but with protection keys, at least, this
should work:
var prefix = ^prefix+"Inner>"; // "Outer>Inner>"
I hope that explains why I raised this issue on this list?
.. I'm not talking
about automatic rewriting. All of these representations
of binding structure (names, de Bruijn indices, the "locally
nameless" approach, the Berkling representation you
describe) are different ways of representing graphs as
trees. For the sake of formal systems, they're all equally
expressive. The problem is that some of them are more
susceptible to human error than others.
While that is true, I did not find Berkling's system any more
error-prone than Church's in practice. As a student, I
implemented a small functional language - parser, compiler,
runtime system, based on the G-machine papers, in a
Berkling-style reduction language, and so did the other
students in that course - protection keys were not a problem
(that was a semester-long project, following on from another
semester-long introduction to functional programming, so
we had ample time and opportunity to play with the system).
I only have small-scale experience with non-Berkling lambda
calculi implementations, but I always found the automatic
renamings somewhere between confusing and annoying
(both as a user and as an implementer).
During our project implementing the Haskell Refactorer
HaRe[2], I often found the lack of protection keys in Haskell
limiting for our refactoring design choices - either we had
to initiate renamings that the programmer had not asked
for, to make the intended refactoring go through, or we
had to abandon the refactoring with an error message
indicating the naming conflict, asking the programmer
to do a rename first, then try again..
If protection keys were at all "brittle" in the face of
refactoring, none of his systems would ever have worked.
Nonsense-- there are all sorts of technologies that work
and are brittle. Ask me over a beer some time, and I'll name
a few. ;)
I assume some of them have been tested on students
or similarly suitable and unsuspecting subjects?-)
Quite contrary to being "brittle", protection keys introduce
the necessary flexibility that allows these systems to *avoid
breaking* the binding structure while automatically rewriting
the code.
I think the key to the misunderstanding here is the word
"automatically." Of course you can automatically refactor
just about any representation of variable binding. The
question with a given representation is whether it's sensitive
to small changes introduced by ordinary human refactoring
(i.e., the everyday experience of programming) and therefore
more error-prone.
"automatic rewriting" is something humans can do, too:
complex tasks can be made less error-prone by having a
consistent algorithm to follow, instead of one consisting
of simple rules with lots of exceptions.
For reference, and just as a subjective impression: students
would get protection key manipulation wrong, but they
would also get name capture wrong - the difference
seemed to be that explaining the latter took longer than
explaining the former. I would be tempted to generalize
that key manipulation errors tended to be problems of
execution, while name capture errors tended to be
problems of understanding, but I have no data to
back such intuition.
But the real answer to your question is that protection keys
do not introduce new problems here: in closed programs,
the same binding structures can be expressed without
protection keys, so all the small changes and sensitivities
already arise without them.
Of course, protection keys would give Javascript obfuscators
a new toy to play with, but they have ample toys already. So,
I am only referring to normal use in absence of reflection.
Claus
[1] http://community.haskell.org/~claus/publications/phd.html
[2] http://www.cs.kent.ac.uk/projects/refactor-fp/
( http://www.youtube.com/watch?v=4I7VZV7elnY )
[*] Only for those of you who wanted more references or
evidence of use:
I learned about Berkling's ideas mostly in Werner Kluge's
research group (only met Berkling when he visited Kluge).
Kluge tended to be more interested in how language
design influenced paradigm-specific implementations
(both abstract and real machines), but he is still the best
source of references and explanations about Berkling's
work. Kluge retired long ago, but has kept some of his
old pages around:
http://www.informatik.uni-kiel.de/inf/Kluge/
http://www.informatik.uni-kiel.de/~wk/papers_func.html
For a very brief history and some references, see the
related work section in Kluge's
"Abstract Lambda Calculus Machines"
2nd Central European Functional Programming School
CEFP 2007, LNCS 5161 (2008), pp. 112 -- 157
(available online from those urls). That last section is an
extract from one of Kluge's textbooks on the subject.
The PI-RED systems were the implementations of the
Kiel Reduction Language, based on Berkling's reduction
languages, so for reports on usage and implementation
of a Berkling-style language, you might find the online
versions of
"A User's Guide for the Reduction System PI-RED"
Kluge, W., Internal Report 9409, Dept. of Computer
Science, University of Kiel, Germany, 1994
"Using PI-RED as a teaching tool for functional
programming and program execution"
Kluge, W., Rathsack, C., Scholz, S.-B., Proc. Int. Symp.
on Functional Programming Languages in Education,
Nijmegen, The Netherlands, 1996, LNCS 1022, (1996),
pp. 231-250
"PI-RED+ An interactive compiling graph reducer
for an applied lambda-calculus"
Gaertner, D., Kluge, W. E., Journal of Functional
Programming Vol. 6, No. 5, (1996), pp. 723-756
relevant (availabe online via second url above).
_______________________________________________
es-discuss mailing list
es-discuss@mozilla.org
https://mail.mozilla.org/listinfo/es-discuss