Updated 2011-11-08 07:08:58 by jdc

iu2 2007-02-17 In a Python's program I've recently written there is a function that builds the GUI. An annoying thing about that function is that many GUI items and their data counterparts depend on a global application mode, which leads to many ifs. All the if commands make the code look unreadable and not so maintenance-friendly.

I thought of a tcl control structure for handling this situation more nicely. The example is the following, rather silly code
  proc operateMode {} {        
    global theMode
          
    puts {Alway performed}
    if {$theMode eq "mode1"} {
      foreach x {1 2 3} {
        puts "mode1 $x"
      }
    }
          
    # mode2 or mode3
    if {$theMode in {mode2 mode3}} {
      puts "Mode $theMode!"
      puts "Do something for $theMode"
      if {$theMode eq "mode2"} {
        puts "mode is mode2"
      } else {
        puts "Mode 3 chosen"
      }
    }
          
    if {$theMode eq "mode4"} {
      puts "Now this is mode 4"
    }
    # end of modes
  }        

Trying it with the following script, let's call it run example script
  # run example script
  set allModes {mode1 mode2 mode3 mode4}
  
  foreach theMode $allModes {
    puts "Mode: $theMode"
    operateMode
    puts -----------------
  }

gives
  Mode: mode1
  Alway performed
  mode1 1
  mode1 2
  mode1 3
  -----------------
  Mode: mode2
  Alway performed
  Mode mode2!
  Do something for mode2
  mode is mode2
  -----------------
  Mode: mode3
  Alway performed
  Mode mode3!
  Do something for mode3
  Mode 3 chosen
  -----------------
  Mode: mode4
  Alway performed
  Now this is mode 4
  -----------------

With the new control structure operateMode will be written as follows
  proc operateMode {} {        
    modeAware $::theMode $::allModes {
                  
      global theMode
      puts {Alway performed}
      foreach x {1 2 3} {
        puts "mode1 $x"
      } - mode1
                  
      {
        # mode2 or mode3
        puts "Mode $theMode!"
        puts "Do something for $theMode"
        puts "mode is mode2" - mode2
        puts "Mode 3 chosen" - mode3
      } - mode2 mode3
          
      puts "Now this is mode 4" - mode4
      # end of modes
    }
  }        

which focuses on what to do rather than on when to do it.

I wrote the code for modeAware in stages.

1. A little bit shorter structure

Defining a new proc "!!" let us rewrite the original opreateMode like this
  proc operateMode {} {        
    global theMode
          
    !! puts {Alway performed}
    !! mode1 foreach x {1 2 3} {
      puts "mode1 $x"
    }
          
    # mode2 or mode3
    !! mode2 mode3 {
      puts "Mode $theMode!"
      puts "Do something for $theMode"
      !! mode2 puts "mode is mode2"
      !! mode3 puts "Mode 3 chosen"
    }
  
    !! mode4 puts "Now this is mode 4"
    # end of mode
  }        

where "!!" is defined as an "if" replacement, and it filtes the command given to it according to the modes that appear on the line.
  proc !! {args} {
    set c -1
    foreach m $args {
      incr c
      if {$m in $::allModes} {lappend chosenModes $m} break
    }
    if {[info exists chosenModes]} {
      if {$::theMode in $chosenModes} {
        set cmd [lrange $args $c end]
          if {[llength $cmd] == 1} {set cmd [join $cmd]}
          uplevel 1 $cmd
      }
    } else {
      uplevel 1 $args
    }
  }

and yields the same result upon invoking the run example script.

The proc "!!" already makes the code clearer because it cuts down lines and braces. Actually, I have used a couple of times deidcated short "if" replacements for "if" patterns that occured more than a few times in my code.

2. Shift the condition to the end of the line

With "!!" changed to find the modes at the end of each command,
  package require struct
  proc !! {args} {
    global allModes theMode
    set c [llength $args]
    foreach m [struct::list reverse $args] {
      incr c -1
      if {$m in $allModes} {lappend chosenModes $m} break
    }
  
    if {[info exists chosenModes]} {
      if {$theMode in $chosenModes} {
          set cmd [lrange $args 0 [expr {$c - 1}]]
          if {[llength $cmd] == 1} {set cmd [join $cmd]}
          uplevel 1 $cmd
      }
    } else {  ;# no modes on the line
      uplevel 1 $args
    }
  }

operateMode can be written as
  proc operateMode {} {        
    global theMode
    !! puts {Alway performed}
    !! foreach x {1 2 3} {
      puts "mode1 $x"
    } - mode1

    # mode2 or mode3          
    !! {
      puts "Mode $theMode!"
      puts "Do something for $theMode"
      !! puts "mode is mode2" - mode2
      !! puts "Mode 3 chosen" - mode3
    } - mode2 mode3
  
    !! puts "Now this is mode 4" - mode4
    # end of mode
  }        

Anything can replace the "-" symbol, except for an empty string.. Also, If a non existing mode appears in the line, an error will occur (however, protecting theMode from being assigned an invalid mode is not handled).

3. Removing "!!" altogether

The proc modeAware will take a script and add "!!" for each line, then uplevel it. Since "!!" will not be written manually anymore, let's just change it to work on a given mode-argument instead of of global mode:
  package require struct
  proc !! {mode allowedModes args} {
    set c [llength $args]
    foreach m [struct::list reverse $args] {
      incr c -1
      if {$m in $allowedModes} {lappend chosenModes $m} break
    }
          
    if {[info exists chosenModes]} {
      if {$mode in $chosenModes} {
          set cmd [lrange $args 0 [expr {$c - 1}]]
          if {[llength $cmd] == 1} {set cmd [join $cmd]}
          uplevel 1 $cmd
      }
    } else {
      uplevel 1 $args
    }
  }

And this is modeAware
  proc modeAware {mode modes body} {
    set lines [split $body \n]
    set body ""
    set cmd ""
    foreach line $lines {
      if {[string trim $line] ne ""} {
          if {[regexp -lineanchor {^\s*#} $line]} {                ;# a comment - do not add !! command
            append body $line\n
          } else {
              append cmd $line
            if {[string index $line end] ne "\\"} { ;# handle lines ending with a '\'
                append body "!! $mode [list $modes] $cmd\n"
                set cmd ""}}}}  ;# a supporting editor would help a lot stuffing those braces...
  
    uplevel 1 $body
  }

which adds "!!" for each line, while not touching comment lines, and considering lines ending with a "\".

Comparing the first version of operateMode and its final version:
  proc operateMode {} {                   |  proc operateMode {} {        
    global theMode                        |    modeAware $::theMode $::allModes { 
                                          |
    puts {Alway performed}                |      global theMode
    if {$theMode eq "mode1"} {            |      puts {Alway performed}
      foreach x {1 2 3} {                 |      foreach x {1 2 3} {
        puts "mode1 $x"                   |        puts "mode1 $x"
      }                                   |      } - mode1
    }                                     |
                                          |      {
    # mode2 or mode3                      |        # mode2 or mode3
    if {$theMode in {mode2 mode3}} {      |        puts "Mode $theMode!"
      puts "Mode $theMode!"               |        puts "Do something for $theMode"
      puts "Do something for $theMode"    |        puts "mode is mode2" - mode2
      if {$theMode eq "mode2"} {          |        puts "Mode 3 chosen" - mode3
        puts "mode is mode2"              |      } - mode2 mode3
      } else {                            |
          puts "Mode 3 chosen"            |      puts "Now this is mode 4" - mode4
      }                                   |      # end of modes
    }                                     |    }
                                          |  }
    if {$theMode eq "mode4"} {            |
      puts "Now this is mode 4"           |
    }                                     |
    # end of modes                        |
  }                                       |

With modeAware I can write all the operations I need, and then go over the code and just mark the sections that should operate on certain modes.