Help me pick an API design (OO vs functional)

Michael Herrmann michael.herrmann at getautoma.com
Thu Mar 28 07:41:17 EDT 2013


On Thursday, March 28, 2013 1:42:35 AM UTC+1, Steven D'Aprano wrote:
> On Wed, 27 Mar 2013 02:34:09 -0700, Michael Herrmann wrote:
> 
> > On Tuesday, March 26, 2013 11:37:23 PM UTC+1, Steven D'Aprano wrote:
> >> 
> >> Global *variables* are bad, not global functions. You have one global
> >> variable, "the current window". So long as your API makes it obvious
> >> when the current window changes, implicitly operating on the current
> >> window is no more dangerous than Python's implicit operations on the
> >> current namespace (e.g. "x = 2" binds 2 to x in the current namespace).
> 
> > 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?

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 a global 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.

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

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.

> > 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?

By "global" I meant function name.

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

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, but I'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 API as 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 the optional "context" parameter makes it likely that you'll prove me wrong in not including offering such an optional parameter, but I want to see first if we can get by without it, too.

Thanks for all your input.

Michael
www.getautoma.com



More information about the Python-list mailing list