Static (lexical) vs dynamic scoping

Eric and I were discussing scoping in Scheme and Python earlier today, our third over the past few weeks – and we finally nailed it shut. The first time he brought up dynamic scoping in Common Lisp and how Prof. Friedman dislikes it; the second was on how Python appears to have dynamic scoping (which turns out to be true, pre-Python 2.2), and now, thanks to Wikipedia, I think we have it right.

Provided Eric gets the H211 Introduction to Programming (Honors) class, which is in Python, and I get the C211 Introduction to Programming (Scheme), our discussion should stand us in good steed, though funnily today I played the Python guy and he played the Scheme one.

I’m going to show the examples, both in Scheme and Python; the first one in each section would appear to show that the language in question features dynamic scoping, which is incorrect as both actually do lexical scoping.

Scheme:
Bad:

(let ((pi 3.1415))
 (define area
  (lambda (r)
   (* pi r r)))
 (display (area 10))
 (newline)
 (set! pi 3)
 (display (area 10)))

Good:

(let ((pi 3.1415))
 (define area
  (lambda (r)
   (* pi r r)))
 (display (area 10))
 (newline)
 (let ((pi 3))
  (display (area 10))))

Python:
Bad:

pi = 3.1415
def area(r):
 return pi*r*r
area(10)
pi = 3
area(10)

Good:

pi = 3.1415
pi_holder = 10
def create_area():
 pi_holder = pi  # local pi_holder, different from pi_holder outside
 def area(r):
  return pi_holder*r*r
 return area
area = create_area()
area(10)    # 314.15
pi_holder    # Still 10
pi_holder = 3
area(10)    # Still 314.15

The above works, but is a bit problematic. I introduced pi_holder=10 to show that, (1), pi_holder inside of create_area() is a local variable; (2), that this local pi_holder is the one that is in area‘s scope, and thus changing the value of pi_holder does not affect it.

Isn’t it easier to just do pi = pi ? Well, that does not work. My initial hunch was that Python reads the LHS of the expression, decided pi has been redeclared as a local variable, and thus since it’s not been initialized it got confused trying to assign it the value of itself. But it’s actually worse; this code does not work either:

x = 42
def local_var_test():
 temp = x
 print temp  # 42
 x = temp
 print x   # 42?

Surprise! Python won’t let you do that either. Take out the last two lines and the code works, though. Basically, if in the block a variable is declared anywhere, it is a local variable everywhere in that block, and trying to refer to a variable declared in the surrounding scope, even before the local declaration, will fail.

But this is where default parameters come in handy. A better way to rewrite the clunky code above is as follows:

pi = 3.1415
def create_area(pi = pi):
 def area(r):
  return pi*r*r
 return area
area = create_area()
area(10)    #314.15
pi = 3
area(10)    #314.15

So Python has static scoping after all. The thing to bear in mind is that Scheme functions are named closures, while Python functions inherit the surrounding scope, so to freeze the variables you depend on you have to wrap your function definition inside another function that copies in the values you need into its local variables.

References:

And the funny thing is, I started the day trying to find good dynamic languages that run on the Java platform (platform envy, I guess, since .NET more prominently touts its language neutrality). Sun’s finally catching up, though – Tim Bray wrote a few months back about the Coyote project to support dynamic languages in Sun’s open source IDE, NetBeans, and pointed to an interesting Sun-developed scripting language, Pnuts. Which reminded me of Groovy and Boo.

Googling for groovy boo .net – Groovy being a Ruby-like scripting language for Java that received a lot of attention a few months ago, and then taken some flak over its development model, and Boo being the Python-like language for .NET – yields this very interesting Slashdot discussion that led me to such intriguing functional OO languages as Scala and Nice. .NET fans do not get to have all the fun!

Groovy, on the other hand, seems rather disappointing. Oh well. Scala looks more like Haskell, but with dynamic type inference (like Boo).. yay!

Update2005/06/06

Realized a few days ago, but haven’t gotten round to posting about it, that I was unfairly comparing Scheme and Python, and that Python methods are closures in themselves. Note:

pi = 3.1415
def area(r):
 return pi*r*r
print area(10)  # 314.15
def test():
 pi = 3
 print area(10) # 314.15
test()

In the earlier example, overriding the value of pi with pi = 3 is the equivalent of doing (set! pi 3) in Scheme, i.e. it will change the value of the variable that both the top-level pi, which is the one that area knows. In a dynamic scope, which uses a stack to figure out which value assignment should apply, pi = 3 would affect the call to area just after it.

About these ads

6 responses to “Static (lexical) vs dynamic scoping

  1. Hi,

    I’m reading “Concepts, Techniques and Models of Computer Programming” and came across the section on static vs. dynamic scoping. I whipped open my laptop to check whether python was statically scoped, and was fooled into thinking it was dynamically scoped using something very similar to your “bad” example.

    A couple google searches later and I find this post, the perfect clarification. Thanks!

  2. update: after more experimenting, seems that python is dynamically scoped for function values:

    **** naive example
    >>> def q(x):
    … print ‘stat’, x

    >>> def p(x):
    … q_holder = q
    … return q_holder(x)

    >>> p(‘dude’)
    stat dude
    >>> def q(x):
    … print ‘dyn’, x

    >>> p(‘dude’)
    dyn dude
    >>>

    ***** should show static scoping
    >>> def q(x):
    … print ‘stat’, x

    >>> def p(x):
    … q_holder = q
    … return q_holder(x)

    >>> def blah(x):
    … print ‘dyn’, x

    >>> p(‘dude’)
    stat dude
    >>> q = blah
    >>> p(‘dude’)
    dyn dude

    I guess you can’t copy a function definition and save it from being rebound later….

  3. ok ok, i finally figured it out. python is statically scoped. here is a good example:

    >>> def a1():
    … def q(x):
    … print ‘stat’, x
    … def p(x):
    … return q(x)
    … def a2():
    … def q(x):
    … print ‘dyn’, x
    … p(‘hello’)
    … a2()

    >>> a1()
    stat hello

    the example i was working from the book was in a language (“Oz”) was a dynamic language that still had variable declaration:

    local P Q in
    proc {Q X} {Browse stat(X)} end
    proc {P X} {Q X} end
    local Q in
    proc {Q X} {Browse dyn(X)} end
    {P hello}
    end
    end

    so it is easier to create inner scopes. with python, i needed a separate inner function for each layer of scope for a comparable example.

  4. The top-level namespace is normally an exception to the rule. Even in Scheme:

    (define (f) 42)
    (define (g) (f))
    (g) ;; 42
    (define (f) 50)
    (g) ;; 50

    The neat thing about Python’s lexical scope is that you can actually fake private variables: declare a local dictionary inside your constructor __init__, put all the private variables there (key = the name, value = initial value), and define all the accessor methods in the constructor as well.

    Because of lexical scoping, they have access to this dictionary. Now attach them to the class:

    class bean(object):
    def __init__(self):
    privates = {"val" : 0}
    def get():
    return privates["val"]
    def set(x):
    privates["val"] = x
    return
    self.get = get
    self.set = set
    return

    dir(bean()) will show you that the privates are, well, privates. Note the neat trick that if you use self.method_name = internal_name, you don’t need to take self as an argument (but you don’t have access to the object’s namespace, apart from privates). If you use classname.method_name = internal_name (i.e. bean.get = get), now get must take self as a parameter, but gets to use it as needed.

    I lifted this from a fascinating discussion, that unfortunately I can’t find at the moment. Will update when I manage to locate it.

  5. Python 2.5 (r25:51918, Sep 19 2006, 08:49:13)
    [GCC 4.0.1 (Apple Computer, Inc. build 5341)] on darwin
    Type “help”, “copyright”, “credits” or “license” for more information.
    >>> x = 3
    >>> def hello():
    … print x

    >>> def world():
    … x = 5
    … hello()

    >>> hello()
    3
    >>> world()
    3