I'm generally wary of everything global, but you're right as long as no
(global) state is involved.
That comment surprises me. Your preferred API:
switch_to(notepad)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
uses implied global state (the current window). Even if you avoid the use
of an actual global for (say) an instance attribute, it's still
semantically a global. Surely you realise that?
Not trying to be argumentative, I'm just surprised at your comment.
I looked it up - I think this is a very good approach; to provide easy
access to the functionality used in 90% of cases but still give users
the flexibility to cover the edge cases.
After everybody's input, I think Design #2 or Design #4 would be the
best fit for us:
Design #2:
notepad_1 = start("Notepad")
notepad_2 = start("Notepad")
switch_to(notepad_1)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
switch_to(notepad_2)
press(CTRL + 'v')
This is nice syntax for trivial cases and beginners whose needs are not
demanding, but annoying for experts who have more complicated
requirements. If this is the only API, experts who need to simultaneously
operate in two windows will be forced to write unproductive boilerplate
code that does nothing but jump from window to window.
Well what do you know, even in the simple case above, you have
unproductive code that does nothing but jump from window to window
I'm not against this API, I'm just against it as the *only* API.
Design #4:
notepad_1 = start("Notepad")
notepad_2 = start("Notepad")
notepad_1.activate()
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
notepad_2.activate()
press(CTRL + 'v')
This is actually no different from #2 above, except that it uses method
call syntax while #2 uses function call syntax. So it has the same
limitations as above: it's simple for simple uses, but annoying for
complex use.
Neither API supports advanced users with complicated needs. A hybrid
approach, where you have function call syntax that operates on the
implicit current window, plus method call syntax that operates on any
window, strikes me as the best of both worlds. With a little forethought
in your implementation, you don't have to duplicate code. E.g. something
like this:
class WindowOps:
def __init__(self, theWindow=None):
self.theWindow = None
def press(self, c):
win = self.getWindow()
send_keypress_to(win)
def getWindow(self):
if self.theWindow is None:
return gTheTopWindow
return self.theWindow
_implicit = WindowOps(None)
press = _implicit.press
# etc.
del _implicit
This gives you the best of both worlds, for free: a simple API using an
implicit top window for simple cases, and a slightly more complex API
with an explicit window for advanced users.
Normally, I'd go for Design #4, as it results in one less global,
I don't see how this is possible. Both APIs use an implicit "top window".
What's the one less global you are referring to?
is
better for autocompletion etc. The thing with our library is that it
tries to make its scripts as similar as possible to giving instructions
to someone looking over their shoulder at a screen. And in this
situation you would just say
activate(notepad)
rather than
notepad.activate().
Depends like Yoda they talk whether or not.
Unless you go all the way to writing your own parser that accepts English-
like syntax, like Hypertalk:
select notepad
type hello world
I don't think it makes that much difference. Function call syntax is not
exactly English-like either. We don't generally speak like this:
write bracket quote hello world quote close bracket
My personal feeling is that people aren't going to be *too* confused by
method call syntax, especially not if they've seen or been introduced to
any programming at all. You say tom-a-to, I say tom-ar-to.
But I think it is useful to distinguish between the "basic API" using
function call syntax and an implied current window, and an "advanced API"
using method call syntax with an explicit window:
# Basic API is pure function calls, using an implicit window
switch_to(notepad)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
switch_to(calculator)
write('2+3=')
press(CTRL + 'a')
switch_to(notepad)
press(CTRL + 'v')
# Advanced API uses an explicit window and method calls:
notepad.write("Hello World!")
notepad.press(CTRL + 'a', CTRL + 'c')
calculator.write('2+3=')
calculator.press(CTRL + 'a')
notepad.press(CTRL + 'v')
# Of course you can mix usage:
switch_to(notepad)
write("Hello World!")
press(CTRL + 'a', CTRL + 'c')
calculator.write('2+3=')
calculator.press(CTRL + 'a')
press(CTRL + 'v')
You could avoid method call syntax altogether by giving your functions an
optional argument that points to the window to operate on:
write("Hello World!")
write("Hello World!", notepad)
but the difference is mere syntax.
There's little or no additional complexity of implementation to allow the
user to optionally specify an explicit window. I expect you have
something like this:
def write(string):
window = gTheCurrentWindow # use a global
send characters of string to window # magic goes here
This can trivially be changed to:
def write(string, window=None):
if window is None:
window = gTheCurrentWindow
send characters of string to window # magic goes here
See, for example, the decimal module. Most operations take an optional
"context" argument that specifies the number of decimal places, rounding
mode, etc. If not supplied, the global "current context" is used.
This gives the simplicity and convenience of a global, without the
disadvantages.
(Recent versions of decimal have also added "with context" syntax, for
even more power.)