Joel-On-Software on the power of a good language

Joel-On-Software on the power of a good language

  • Comments 12

Joel Spolsky of Joel-On-Software fame, just posted a blog, "Can Your Programming Language Do This?" http://www.joelonsoftware.com/items/2006/08/01.html

In this article, he drives home the point that when it comes to code: Maintainability, Readability, Abstraction == Good!  He then points out how languages can help or hinder this and goes on to articulate the things a language should do to help.  We couldn't agree more.  He could have renamed the article, "Why Windows PowerShell is a Great Language!". 

Open up Joel's blog entry in a separate window and compare the code. You should notice 2 things immediately

  1. Windows PowerShell code looks amazingly like the code Joel said he wanted.  There are a couple of syntactic differences but not many.
  2. Windows PowerShell is giving you answers!  In other words, I did everything using the interactive console.

Joel uses the function ALERT to display info so to make it easier to compare, I created an Alias:

PS> Set-Alias Alert Write-Host

PS> Alert "I'd Lke some Spaghetti!"

I'd Lke some Spaghetti!
PS> Alert "I'd Lke some Chocolate Moose!"

I'd Lke some Chocolate Moose!

 

Noone likes repeated code so he created a function:

PS> function SwedishChef ($food)

>> {   Alert "I'd like some $food !"

>> }

>>

PS> SwedishChef Spaghetti

I'd like some Spaghetti !
PS> SwedishChef "Chocolate Moose"

I'd like some Chocolate Moose !

 

Joel then makes the point that being able to pass functions as parameters increases the chances that you'll be able to find common code:

PS> Function PutInPot($Ingredient) {Alert "POT: $Ingredient added"}

PS> Function BoomBoom($Ingredient) {Alert "BOOM: $Ingredient"}

PS> Function Cook ($i1, $i2, $f)

>> {  Alert "Get the $i1"

>>    &$f $i1

>>    &$f $i2

>> }

>>

PS> Cook Lobster Water PutInPot

Get the Lobster
POT: Lobster added
POT: Water added
PS> Cook Chicken Coconut BoomBoom

Get the Chicken
BOOM: Chicken
BOOM: Coconut

 

He then points out that the problem with this is that you have to have the functions defined a priori.  Wouldn't it be more flexible/powerful if you could write them inline instead of declaring them elsewhere (anonymous functions)?  If you could do that, then it reduces the cost/burden to reuse code:

PS> Cook Lobster Water {Param($x) Alert "Pot $x"}

Get the Lobster
Pot Lobster
Pot Water
PS> Cook Chicken Coconut {Param($x) Alert "Boom $x"}

Get the Chicken
Boom Chicken
Boom Coconut

 

Once you have anonyous functions as arguments, you begin to see the world in a different way.  First Joel shows you a traditional approach to doubling all the elements of an array:

PS> $a = 1,2,3

PS> for ($i = 0; $i -lt $a.length; $i++) {Alert $a[$i]}

2
4
6

But now let's use write a generic function which takes a function and an array as parameter and then applies that function to each element in the array:

PS> function map ($fn, $a)

>> {  for ($i = 0; $i -lt $a.length; $i++)

>>    {  $a[$i] = &$fn $a[$i]

>>    }

>> }

That allows you to now do this:

PS> $a = 1,2,3

PS> map {param($x) return $x*2} $a

PS> map Alert $a

2
4
6

 

Another example of this is the following 2 functions.  Both of these add elements to an initial value.  In the first case (SUM), the elements are added as integers.  In the second case (JOIN), the elements are added as strings.

PS> function sum ($a)

>> {  $s = 0

>>    for ($i = 0; $i -lt $a.length; $i++)

>>    {   $s += $a[$i]

>>    }

>>    return $s

>> }

PS> function join ($a)

>> {  $s = ""

>>    for ($i = 0; $i -lt $a.length; $i++)

>>    {  $s += $a[$i]

>>    }

>>    return $s

>> }

>>

PS> alert (sum 1,2,3)

6
PS> alert (join "a","b","c")

abc

 

Look at the details of these functions and you'll see that they are almost identical except for the initial value (0 vs "").  So now let's refactor these functions to use a common function (REDUCE):

PS> function reduce ($fn, $a, $init)

>> {  $s=$init

>>    for ($i = 0; $i -lt $a.length; $i++)

>>    {  $s = &$fn $s $a[$i]

>>    }

>>    return $s

>> }

PS> function sum ($a)

>> {  return reduce {param($a,$b) return $a + $b} $a 0

>> }

>>

PS> function join ($a)

>> {  return reduce {param($a,$b) return $a + $b} $a ""

>> }

>>

PS> alert (sum 1,2,3)

6
PS> alert (join "a","b","c")

abc

Joel - thanks for an the eloquent articulation of the value of Windows PowerShell! 

Jeffrey Snover [MSFT]
Windows PowerShell/Aspen Architect
Visit the Windows PowerShell Team blog at:    http://blogs.msdn.com/PowerShell
Visit the Windows PowerShell ScriptCenter at:  http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

 

PSMDTAG:FAQ: Anonymous Functions - are they supported? ANSWER: YES

PSMDTAG:LANGUAGE: Anonymous Functions

Leave a Comment
  • Please add 7 and 8 and type the answer here:
  • Post
  • It sounds like Joel wants Perl...

    print grep ord $_,map{y/a-zA-Z//d;$x.="+ $_";chr(eval $x)
    }'J74u43-s2tA1-84n33o45th1er5-12-P3e13-82r48l21H13-a6-76
    c40k25er2wx8-y6z13-81'=~m#([^!\n]{3})#g#tr/-0-9//d;print
  • Surely the last two functions can be further refactored?
    they're both the same code with different $init values for reduce

    thus:
    <code>
    function add ( $a, $b ) { return $a + $b }
    function sum ( $a ) { return reduce add $a 0 }
    function join ( $a ) { return reduce add $a "" }
    </code>
  • Nice to see that the PowerShell scripting language offers functional programming features like LISP or Haskell do.
  • PS >Set-Alias Alert Write-Host
    PS > function SwedishChef ($food)
    >> {
    >> Alert "id like some $food"
    >> }
    PS > SwedishChef spag
    id like some spag

    Good!

    PS > Set-Alias Alert gorb
    PS > SwedishChef spag
    Alias not resolved because alias 'Alert' referenced command 'gorb', this comman
    d is not recognized as a cmdlet, function, operable program, or script file.
    At line:3 char:6
    + Alert  <<<< "id like some $food"

    I personally hate this run-time name binding.

    How do you bind the Alert alias at definition time and not run-time?

    I dont know power-shell but I would think the syntax of what happened should be
    PS > function SwedishChef ($food)
    >> {
    >> $Alert "id like some $food"
    >> }

  • > How do you bind the Alert alias at definition time and not run-time?

    Runtime binding is one of the key features of Shells and other dynamic environments.  There are some huge advantages to it but there are some downsides as well.  In future releases, we expect to provide users the option to choose more static behaviour.  Alias binding is on the top of my list for this.

    Jeffrey Snover [MSFT]
    Windows PowerShell/Aspen Architect
    Visit the Windows PowerShell Team blog at:    http://blogs.msdn.com/PowerShell
    Visit the Windows PowerShell ScriptCenter at:  http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx
  • Love it!

    so this is an anonymous function that accepts a parameter called $x --

    {Param($x) Alert "Boom $x"}

    while *this* is a named function with a parameter called $x:

    Function PutInPot($x) {Alert "Boom $x"}

    and am i right trying to compare with ruby and saying that:

    {Param($x) Alert "Boom $x"}

    is similar to:

    {|x| Alert "Boom x"}

    ---
    and next guess: the anonymous function is going to accept piped in variables, where-as the function won't? (in its current form)

    lovin the pshell!



  • I had read the Joel on sw article and it's neat to see this real life example so soon after.  :)
  • lb:

    You normally define functions with the parameters listed after the function name, but it is just as correct to provide a param statement.  That lets you write:

    Function PutInPot {Param($x) Alert "Boom $x"}

    Also, all script blocks, functions, and scripts can be written to easily accept pipeline input:

    "Hi" | & { begin { "Beginning"} process { write-host "Boom $_" } end { "Ending" } }

    function foo { begin { "Beginning"} process { write-host "Boom $_" } end { "Ending" } }
    "Hi" | foo

    We also support a shortcut for functions act on pipeline input -- called filters.  For an example, see the help topic, about_Filter.
  • I like the simplicity and "power" of PowerShell... But, check this. Nice work from a strong type-checked language, isn't it? ////////// // C# 2.0 version of "famous" Map/Reduce sample ////////// using con = System.Console; using gen = System.Collections.Generic; namespace DynamicProgram { static class Program { delegate void Function( T arg ); delegate T FunctionWithRet( T arg ); static Function alert = con.WriteLine; static Function alert_i = con.WriteLine; static void SwedishChef( T msg ){ alert( "I'd like some " + msg + "!" ); } static void PutInPot( T ingredient ){ alert( "POT: " + ingredient + " added" ); } static void BoomBoom( T ingredient ){ alert( "BOOM: " + ingredient ); } static void Cook(T i1, T i2, Function f) { alert( "Get the " + i1 ); f(i1); f(i2); } static void map(Function f, gen.IEnumerable a){ foreach (T i in a) f( i ); } static void map(FunctionWithRet f, T[] a){ for( int i = 0; i < a.Length; ++i ) a[i] = f( a[i] ); } delegate T BinaryOperation( T i, T j ); static BinaryOperation add = delegate( int i, int j ){ return i + j; }; static BinaryOperation concat = delegate( string i, string j ){ return i + j; }; static T sum(BinaryOperation binOp, params T[] a) { T t = default(T); foreach (T i in a) t = binOp( t, i ); return t; } static T join( BinaryOperation binOp, params T[] a ) { T t = default(T); foreach (T i in a) t = binOp( t, i ); return t; } static T reduce( BinaryOperation binOp, params T[] a ) { T t = default(T); foreach (T i in a) t = binOp( t, i ); return t; } static T sum_r(BinaryOperation binOp, params T[] a){ return reduce( binOp, a );} static T join_r( BinaryOperation binOp, params T[] a ){ return reduce( binOp, a ); } //extra delegate T reduceHandler( params T[] a ); static reduceHandler reduced_sum = delegate( int[] a ){ return reduce( add, a ); }; static reduceHandler reduced_join = delegate( string[] a ){ return reduce( concat, a ); }; static void Main(string[] args) { alert("I'd like some Spaghetti!"); alert("I'd like some Chocolate Moose!"); SwedishChef("Spagetthi"); SwedishChef("Chocolate Moose"); Cook("Lobster", "Water", PutInPot); Cook("Chicken", "Coconut", BoomBoom); Cook("Lobster", "Water", delegate(string ingredient) { alert("POT: " + ingredient + " added"); }); Cook("Chicken", "Coconut", delegate(string ingredient) { alert("BOOM: " + ingredient); }); int[] a = { 1, 2, 3 }; for( int i = 0; i < a.Length; ++i ) { a[i] *= 2; } foreach ( int i in a ) { alert_i( i ); } a = new int[]{ 1, 2, 3 }; map(delegate(int i) { return i * 2; }, a); map(alert_i, a); alert_i( sum( add, 1, 2, 3 ) ); alert( join( concat, "a", "b", "c" ) ); alert_i( sum_r( add, 1, 2, 3 ) ); alert( join_r( concat, "a", "b", "c" ) ); //extra alert_i( reduced_sum( 1, 2, 3 ) ); alert( reduced_join( "a", "b", "c" ) ); } } } //////////
  • I prefer this version of "map", because it works with any collection object or scalar:

    filter map( [ScriptBlock]$fn )

    {

    $_ | %{ &$fn $_ }

    }

    Works like this:

    $files = "a

    b

    c

    d"

    $files = $files.split("`n") | map { $_.Trim() }

  • Hah, that should be reduced to:

    filter map( [ScriptBlock] $fn ) { & $fn $_ }

  • I'd prefer a version of map that returned a new array rather than altering the existing one in place. Something like...

    function map ($fn, $list)

    {  

    $ret = $null

    for ($i = 0; $i -lt $list.length; $i++)

      {  

      $ret += ,(&$fn $list[$i])

      }

      return $ret

    }

Page 1 of 1 (12 items)