Re: Lambda binding solved?

Tim Peters (tim@ksr.com)
Sat, 26 Feb 94 21:36:44 EST

I think this sig from an old E-Mail of Guido's sums it up <wink>:

"We have not succeeded in answering all your questions. The answers
we have found only serve to raise a whole set of new questions. In
some ways we feel we are as confused as ever, but we believe we are
confused on a higher level and about more important things."

> [guido]
> ...
> However this [special-casing lambdas] would break the rule that a
> lambda expression is nothing but syntactic sugar for a local function.

Just noting that, as things stand today, the only value of that rule to
Python's _users_ is that it makes it possible to explain to them why
their lambda didn't work as expected <0.3 grin>.

Supporting general nested scopes is the way to meet expectations, and
that rule, at the same time. I'd be happy with meeting just the
expectations (in effect, calling a lambda body a distinct kind of code
block, in the sense of the manual's 4.1, and extending that section's
table with type = lambda body; global NS = global NS of containing block;
local NS = local NS of containing block augmented with bindings for the
lambda's arguments).

But I fully agree the latter would be a dead-wrong approach (& general
nested scopes is the dead-right approach) if either of the following is
true:

1) People want general nested scopes for other reasons (I did when I
started using Python, but I don't miss them anymore (class + module
approaches almost always work out better)).

2) People want lambda extended to allow statements in the body (then
setting the lambda's local NS to its containing block's local NS isn't
safe, because an assignment (new binding or rebinding) in the lambda
body would alter the binding in the lambda's containing block -- or,
in other words, a lambda couldn't have local variables. In the
_current_ simple lambda, it's syntactically impossible to write
something that could alter the lambda's local NS -- so identifying the
lambda's local NS with the containing block's local NS would give read
access but not write access to the latter).

So what do people really want? I just want the current lambda to "work".

> [assuming general nested scopes is the answer ...]
> A problem might be how to extend the "global" statement to allow
> assignments to variables at an intermediate scope.

Clearly, existing "global" statements should continue to mean the same
thing. "nonlocal" is the most descriptive word for intermediate scopes,
and I assume the search for a nonlocal variable would go thru a
statically-determined sequence of namespaces, closest-containing scope
first, global NS last. E.g.,

def f():
i = 2
def g():
i = 3
def h():
print 'h', i
h()
print 'g', i
g()
print 'f', i
f()

prints

h 3
g 3
f 2

If we change h to:

def h():
nonlocal i # whatever
i = 4
print 'h', i

I suppose

h 4
g 4
f 2

is most reasonable (i.e., search stops at the first NS with a hit). Do
we need a way to explicitly get at f's "i" in this case?

Alternative: forget variants of "global", and make non-local bindings
use a new
function_name '.' variable_name

notation for the target. Strength: clarity. Weakness: if arbitrarily
complex lambdas are also allowed, inner lambdas couldn't alter bindings
in outer lambdas (since lambdas have no "function_name"). Weakness: I
suppose it's possible for any number of nested functions to have the same
name, in which case "function_name" is ambiguous.

Assumption: That you want Python to continue using static scoping (more
than OK by me!).

exec and eval may also be troublesome, yes? (I'm thinking of the forms
that allow specifying namespaces -- if general nested scopes are allowed,
presumably exec and eval should allow a flat list of NS's?)

Then there are classical problems. E.g., what should this do?:

def make_incrementer(inc):
def f(n): return n + inc
print 'inside', f(3)
return f
x = make_incrementer(12)(3)
print 'outside', x

One answer is "it prints 'inside 15' and then dumps core" <grin>. I.e.,
it's partly a question of whether make_incrementer's local NS outlives
the invocation of make_incrementer (the function returned needs it to
resolve "inc"). I believe a function's local NS is lost at function
return today, but if general nested scoping is allowed that policy can
have visible consequences (contentious ones, too!).

And some existing programs will change meaning. Haven't thought much
about it, but here's an obvious <wink> one:

def f(n):
def f(n): return f(n-1)+1
if n == 0: return 2
return f(n)
f(4)

That prints 6 today, but it's hard to see how to support general nested
scopes without having it end up in an infinite loop [for the confused,
today

def f(n): return f(n-1)+1

(a) binds "f" as a name in the outer f's local NS (so that in "return
f(n)" the inner f is referenced), and (b) "f(n-1)" refers to the outer f,
because "f" is _not_ in the inner f's local NS, but is in the global NS
(where it refers to the outer f) -- after introducing general scopes,
"f(n-1)" will refer to the _inner_ f, because "f" will be found in the
outer f's local NS (where it refers to the inner f), and then the inner f
will be in an infinite recursion].

And so on:

> ...
> unlimited nested scopes didn't work out that well in languages like
> Algol-68.

They do raise a world of questions! The range of possible answers is
broad, and bitterly fought over. If you don't _need_ to get into this
business, it might be best to continue avoiding it.

> ...
> Personally, I'd hate to see a Python programming style develop where
> people code their entire program as a set of nested functions ...

Same here. And the current 3-namespace model pretty much guarantees that
such a style will never catch on <grin>. All I want is for, e.g.,

def examine_property(self, prop):
...
damned = filter( lambda person: person.has_key(prop), self.people )
...

to work the way it "obviously" should.

elegance-be-damned<wink>-ly y'rs - tim

Tim Peters tim@ksr.com
not speaking for Kendall Square Research Corp