Saturday, January 09, 2010

Powershell is dynamically scoped, and that will confuse you.

Lets start with an example, as the concept of dynamic scoping is a big string for most programmers.

Python Program
x = 5
def printX():
    print x
def setAndprintX():
    x=7
    printX()
printX()
setAndPrintX()
printX()

Output From Python
5
5
5
Powershell Program
$x = 5
function  printX() { echo $x } 
function setAndprintX()
{
    $x=7
    printX
}
printX
setAndprintX
printX

Output From Powershell
5
7
5


What is this dynamic scoping?
Most programs use static, also called lexical, scoping because it's easy to understand. You figure out what is in scope by looking at the source code. In the python example, the only value of x in scope is the global value of x.

By contrast, powershell uses dynamic scoping, in this model, you lookup up variables at runtime based on a scope stack. Each time you call a function you create a new scope, and copy all values from the parent scope into it. In the powershell example, when printX is called from setAndprintX we get the value of $x that was set in setAndprintX scope.

Why would you want dynamic scoping?
I can't come up with a good explanation of why you'd pick dynamic scoping over lexical scoping. My hunch was this is historical as it's how batch files and shell scripts work. Interestingly, Perl supports both dynamic scoping and lexical scoping. You can read a good article about it here. My synopsis of why Perl has dynamically scoped variables from the article:

  • In the beginning perl only supported global variables and that was painful.
  • Since it was cheap to implement Perl authors added the ability to created dynamically scoped variables (via:local keyword).
  • However, when perl authors got time, they added lexical scoped variables (via: my keyword).
  • Now people are told *not* to use dynamically scoped variables since they're weird.

Do you get other language features to make support for this easier:
Yes, you can write to a different scope explicitly:
$x = 3 # Write to local scope
$global:x = 3 # Write to global scope.

You can also execute your function without creating a new scope via '.' aka sourcing:
. func() # runs the function in local scope, and variables created are visible.

This is a fascinating , but why are we having this conversation?
Because I wanted to write:
function getPrintDogFunction()
{
        function Nested1(){echo"dog"}
        function Nested2(){Nested1}
        Get-Command Nested2
}

$printDog = getPrintDogFunction
#  Call PrintDog
& $printDog  

# This Call fails saying can't find Nested1 - which makes sense it's not in scope.
So, I changed my code to the following:
# source getPrintDogFunction, which causes Nested1 and Nested2 to be created in my scope, and thus
# Nested1 is in scope when I call printDog

$printDog = . getPrintDogFunction 
#  Call $printDog
& $printDog  

A few days pass, and I add a new feature, printCat. Via the power of cut and paste our code becomes:
function getPrintDogFunction()
{
        function Nested1(){echo "dog"}
        function Nested2(){Nested1}
        Get-Command Nested2
}

function getPrintCatFunction()
{
        function Nested1(){echo "cat"}
        function Nested2(){Nested1}
        Get-Command Nested2
}

$printDog = . getPrintDogFunction 
$printCat = . getPrintCatFunction 

#  Print all the animals 
& $printDog  
& $printCat

# Grrr - this is printing cat cat.
# Worse yet depending on the order of these calls, the behavior changes. 

To fix I use the following pattern:
function getPrintCatFunction()
{
    function Nested2()
    {
        function Nested1(){echo "cat"}
        Nested1
    }
    Get-Command Nested2
}

$printDog = getPrintDogFunction 

And now the world makes sense.
Notes:
(1) If you've never heard of dynamic scoping, any language you pick will act the same.

2 comments:

David Eisner said...

I think you're missing the last two lines of your Python example:

setAndprintX()
printX()

ig66 said...

Thanks - fixed!