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.