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# # Pass notes as 'after' callbacks to other operations 41# async_operation $args [$chord add_note] 42# other_async_operation $args [$chord add_note] 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 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". 63oo::class create SimpleChord { 64 variable notes body is_completed 65 66 # Constructor: 67 # set chord [SimpleChord new {body}] 68 # Creates a new chord object with the specified body script. The 69 # body script is evaluated at most once, when a note is activated 70 # and the chord has no other non-activated notes. 71 constructor {body} { 72 set notes [list] 73 my eval [list set body $body] 74 set is_completed 0 75 } 76 77 # Method: 78 # $chord eval {script} 79 # Runs the specified script in the same context (namespace) in which 80 # the chord body will be evaluated. This can be used to set variable 81 # values for the chord body to use. 82 method eval {script} { 83 namespace eval [self] $script 84 } 85 86 # Method: 87 # set note [$chord add_note] 88 # Adds a new note to the chord, an instance of ChordNote. Raises an 89 # error if the chord is already completed, otherwise the chord is 90 # updated so that the new note must also be activated before the 91 # body is evaluated. 92 method add_note {} { 93 if {$is_completed} { error "Cannot add a note to a completed chord" } 94 95 set note [ChordNote new [self]] 96 97 lappend notes $note 98 99 return $note 100 } 101 102 # This method is for internal use only and is intentionally undocumented. 103 method notify_note_activation {} { 104 if {!$is_completed} { 105 foreach note $notes { 106 if {![$note is_activated]} { return } 107 } 108 109 set is_completed 1 110 111 namespace eval [self] $body 112 namespace delete [self] 113 } 114 } 115} 116 117# ChordNote class: 118# Represents a note within a chord, providing a way to activate it. When the 119# final note of the chord is activated (this can be any note in the chord, 120# with all other notes already previously activated in any order), the chord's 121# body is evaluated. 122oo::class create ChordNote { 123 variable chord is_activated 124 125 # Constructor: 126 # Instances of ChordNote are created internally by calling add_note on 127 # SimpleChord objects. 128 constructor {chord} { 129 my eval set chord $chord 130 set is_activated 0 131 } 132 133 # Method: 134 # [$note is_activated] 135 # Returns true if this note has already been activated. 136 method is_activated {} { 137 return $is_activated 138 } 139 140 # Method: 141 # $note 142 # Activates the note, if it has not already been activated, and 143 # completes the chord if there are no other notes awaiting 144 # activation. Subsequent calls will have no further effect. 145 # 146 # NB: In TclOO, if an object is invoked like a method without supplying 147 # any method name, then this internal method `unknown` is what 148 # actually runs (with no parameters). It is used in the ChordNote 149 # class for the purpose of allowing the note object to be called as 150 # a function (see example above). (The `unknown` method can also be 151 # used to support dynamic dispatch, but must take parameters to 152 # identify the "unknown" method to be invoked. In this form, this 153 # proc serves only to make instances behave directly like methods.) 154 method unknown {} { 155 if {!$is_activated} { 156 set is_activated 1 157 $chord notify_note_activation 158 } 159 } 160} 161