1# Simple Chord for Tcl 2# 3# A "chord" is a method with more than one entrypoint and only one body, such 4# that the body runs only once all the entrypoints have been called by 5# different asynchronous tasks. In this implementation, the chord is defined 6# dynamically for each invocation. A SimpleChord object is created, supplying 7# body script to be run when the chord is completed, and then one or more notes 8# are added to the chord. Each note can be called like a proc, and returns 9# immediately if the chord isn't yet complete. When the last remaining note is 10# called, the body runs before the note returns. 11# 12# The SimpleChord class has a constructor that takes the body script, and a 13# method add_note that returns a note object. Since the body script does not 14# run in the context of the procedure that defined it, a mechanism is provided 15# for injecting variables into the chord for use by the body script. The 16# activation of a note is idempotent; multiple calls have the same effect as 17# a simple call. 18# 19# If you are invoking asynchronous operations with chord notes as completion 20# callbacks, and there is a possibility that earlier operations could complete 21# before later ones are started, it is a good practice to create a "common" 22# note on the chord that prevents it from being complete until you're certain 23# you've added all the notes you need. 24# 25# Example: 26# 27# # Turn off the UI while running a couple of async operations. 28# lock_ui 29# 30# set chord [SimpleChord::new { 31# unlock_ui 32# # Note: $notice here is not referenced in the calling scope 33# if {$notice} { info_popup $notice } 34# } 35# 36# # Configure a note to keep the chord from completing until 37# # all operations have been initiated. 38# set common_note [$chord add_note] 39# 40# # Activate notes in 'after' callbacks to other operations 41# set newnote [$chord add_note] 42# async_operation $args [list $newnote activate] 43# 44# # Communicate with the chord body 45# if {$condition} { 46# # This sets $notice in the same context that the chord body runs in. 47# $chord eval { set notice "Something interesting" } 48# } 49# 50# # Activate the common note, making the chord eligible to complete 51# $common_note activate 52# 53# At this point, the chord will complete at some unknown point in the future. 54# The common note might have been the first note activated, or the async 55# operations might have completed synchronously and the common note is the 56# last one, completing the chord before this code finishes, or anything in 57# between. The purpose of the chord is to not have to worry about the order. 58 59# SimpleChord class: 60# Represents a procedure that conceptually has multiple entrypoints that must 61# all be called before the procedure executes. Each entrypoint is called a 62# "note". The chord is only "completed" when all the notes are "activated". 63class SimpleChord { 64 field notes 65 field body 66 field is_completed 67 field eval_ns 68 69 # Constructor: 70 # set chord [SimpleChord::new {body}] 71 # Creates a new chord object with the specified body script. The 72 # body script is evaluated at most once, when a note is activated 73 # and the chord has no other non-activated notes. 74 constructor new {i_body} { 75 set notes [list] 76 set body $i_body 77 set is_completed 0 78 set eval_ns "[namespace qualifiers $this]::eval" 79 return $this 80 } 81 82 # Method: 83 # $chord eval {script} 84 # Runs the specified script in the same context (namespace) in which 85 # the chord body will be evaluated. This can be used to set variable 86 # values for the chord body to use. 87 method eval {script} { 88 namespace eval $eval_ns $script 89 } 90 91 # Method: 92 # set note [$chord add_note] 93 # Adds a new note to the chord, an instance of ChordNote. Raises an 94 # error if the chord is already completed, otherwise the chord is 95 # updated so that the new note must also be activated before the 96 # body is evaluated. 97 method add_note {} { 98 if {$is_completed} { error "Cannot add a note to a completed chord" } 99 100 set note [ChordNote::new $this] 101 102 lappend notes $note 103 104 return $note 105 } 106 107 # This method is for internal use only and is intentionally undocumented. 108 method notify_note_activation {} { 109 if {!$is_completed} { 110 foreach note $notes { 111 if {![$note is_activated]} { return } 112 } 113 114 set is_completed 1 115 116 namespace eval $eval_ns $body 117 delete_this 118 } 119 } 120} 121 122# ChordNote class: 123# Represents a note within a chord, providing a way to activate it. When the 124# final note of the chord is activated (this can be any note in the chord, 125# with all other notes already previously activated in any order), the chord's 126# body is evaluated. 127class ChordNote { 128 field chord 129 field is_activated 130 131 # Constructor: 132 # Instances of ChordNote are created internally by calling add_note on 133 # SimpleChord objects. 134 constructor new {c} { 135 set chord $c 136 set is_activated 0 137 return $this 138 } 139 140 # Method: 141 # [$note is_activated] 142 # Returns true if this note has already been activated. 143 method is_activated {} { 144 return $is_activated 145 } 146 147 # Method: 148 # $note activate 149 # Activates the note, if it has not already been activated, and 150 # completes the chord if there are no other notes awaiting 151 # activation. Subsequent calls will have no further effect. 152 method activate {} { 153 if {!$is_activated} { 154 set is_activated 1 155 $chord notify_note_activation 156 } 157 } 158} 159