Help me pick an API design (OO vs functional)

M

Michael Herrmann

...
Not seeking to advocate this particular option, but it would be
possible to make a single wrapper for all your functions to handle the
focus= parameter:

def focusable(func):
@functools.wraps(func)
def wrapper(*args,focus=None):
if focus: focus.activate()
return func(*args)
return wrapper

Then you just decorate all your functions with that:

def write(string):
# do something with the active window

Hi, sure, I wouldn't have copy-pasted the code and of course there are techniques to avoid code duplication. It's not so much what I'm worried about. What I'm worried about is that the concept of window-switching gets "smeared" over several other not-really related concepts such as clicking and typing. I feel it violates orthogonality: http://www.artima.com/intv/dry3.html is the best freely available resource I could find but I think it's best explained in The Pragmatic Programmer http://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X.

Michael
www.getautoma.com
 
M

Michael Herrmann

...
Not seeking to advocate this particular option, but it would be
possible to make a single wrapper for all your functions to handle the
focus= parameter:

def focusable(func):
@functools.wraps(func)
def wrapper(*args,focus=None):
if focus: focus.activate()
return func(*args)
return wrapper

Then you just decorate all your functions with that:

def write(string):
# do something with the active window

Hi, sure, I wouldn't have copy-pasted the code and of course there are techniques to avoid code duplication. It's not so much what I'm worried about. What I'm worried about is that the concept of window-switching gets "smeared" over several other not-really related concepts such as clicking and typing. I feel it violates orthogonality: http://www.artima.com/intv/dry3.html is the best freely available resource I could find but I think it's best explained in The Pragmatic Programmer http://www.amazon.com/Pragmatic-Programmer-Journeyman-Master/dp/020161622X.

Michael
www.getautoma.com
 
M

Mitya Sirenef

I think what the context manager approach really has going for itself
is the syntactic structure it gives to scripts, that makes it easy to
see what is going on in which window. Semantically, however, I think
the fit of this approach has some rough edges: The fact that there
needs to be some special treatment for ALT + TAB, that actions such as
`press` "sometimes" return values that are needed to continue the
script and so on. It really has its appeal, but I think it's a bit too
special and intricate to be used by a broad audience.


I think alt-tab has to be special in any case. Regular alt-tab would act
like the GOTO statement. As a programmer looking at a script you have no
idea where you just alt-tabbed to without possibly looking through
dozens of lines of previous code.

Keypresses that start a new window also seem pretty special to me.
They're inherently special. After all, the essential function of a
windowing system is when a new window is created, which means subsequent
operations have an entirely different meaning, in a text editor <del>
key will delete a character, in a file manager <del> key will delete a
file!

But, as I mentioned, if you can get away with treating simple dialogs
implicitly (and I don't see why you can't, at this point), that'd be the
preferred way for me.

-m


--
Lark's Tongue Guide to Python: http://lightbird.net/larks/

"The condition of man is already close to satiety and arrogance, and
there is danger of destruction of everything in existence."
- a Brahmin to Onesicritus, 327 BC, reported in Strabo's Geography
 
E

Ethan Furman

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')

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')

Normally, I'd go for Design #4, as it results in one less global, 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().

So the problem lies in a difference between Python's and English grammar. For beauty, I should go with #2. For pragmatism, I should go with #4. It hurts, but I'm leaning towards #4. I have to think about it a little.

Go with #2. Not everything has to be a method. len(), for example, is not a method, even though it calls one.

The 'with' support would also be cool. __enter__ sets the new global window object whilst saving the old one, and then
__exit__ restores the old one.
 
S

Steven D'Aprano

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.)
 
M

Michael Herrmann

...

I think alt-tab has to be special in any case. Regular alt-tab would act
like the GOTO statement. As a programmer looking at a script you have no
idea where you just alt-tabbed to without possibly looking through
dozens of lines of previous code.

Keypresses that start a new window also seem pretty special to me.
They're inherently special. After all, the essential function of a
windowing system is when a new window is created, which means subsequent
operations have an entirely different meaning, in a text editor <del>
key will delete a character, in a file manager <del> key will delete a
file!

But, as I mentioned, if you can get away with treating simple dialogs
implicitly (and I don't see why you can't, at this point), that'd be the
preferred way for me.

Ok. Thank you for your inputs!
 
M

Michael Herrmann

...

Go with #2. Not everything has to be a method. len(), for example, is not a method, even though it calls one.

That's a good point. I actually think #2 is the one we'll use.
 
M

Michael Herrmann

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?

I do :) You made the statement that global variables are bad, not global functions. I didn't want to agree completely with this comment, because if aglobal function refers to a global variable, I would consider it "bad" too.. You correctly point out that our global functions would be exactly of that "bad" kind. Of course, it doesn't make sense to be too dogmatic about "bad", which is why I am considering the global functions as an option, for advantages they have despite being "bad".
Not trying to be argumentative, I'm just surprised at your comment.

No offense taken :) I guess I just wasn't expressing myself clearly.
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.


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.

I understand completely where you are coming from, however if we offer two ways of doing the same thing, people will start mixing the styles and things will get messy. A user commented above that this approach - offering global as well as object oriented functions to do the same thing - is offered by matplotlib and makes examples on the net very confusing and difficult to read. For this reason, I would rather only offer one way of doing things now, and add additional ways later in case they are really needed. You are right that this may not cater for experts' needs very well, but I think I prefer a smaller API that can be extended to one that may result in being difficult to read.
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?

By "global" I meant function name.
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')

As I said, I think I prefer not giving the possibility to mix usage and potentially adding it later over offering it to begin with.
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.

That's been pointed out above. I guess it's mostly a question of taste, butI'd like to avoid mixing the concept of "window switching" with the not-very related concepts of typing and clicking. I'm a big fan of orthogonality,mainly because of the book "The Pragmatic Programmer" but also because of the article http://www.artima.com/intv/dry3.html.
There's little or no additional complexity of implementation to allow the
user to optionally specify an explicit window.

That's of course true. What I'm worried about is API complexity: I think the matplotlib example shows that offering too many ways of doing one thing leads to scripts that are difficult to read/maintain. I want to keep the APIas simple as possible at first, and then add to it when it turns out it's needed.
...
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.

That's a very interesting example! I think the decimal module's "setcontext" would be similar to our "switch_to". The fact that this module offers theoptional "context" parameter makes it likely that you'll prove me wrong innot including offering such an optional parameter, but I want to see firstif we can get by without it, too.

Thanks for all your input.

Michael
www.getautoma.com
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,717
Messages
2,569,382
Members
44,705
Latest member
BerniePele

Latest Threads

Top