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