[Twisted-Python] Deferred documentation rewrite
Glyph Lefkowitz
glyph at twistedmatrix.com
Wed Aug 5 22:07:25 MDT 2009
On Mon, Aug 3, 2009 at 6:00 PM, Edward Z. Yang <ezyang at mit.edu> wrote:
> I have updated my draft here:
>
> http://ezyang.com/twisted/defer2.html
>
Thanks. Looks like it's improving. I've got more points to critique now,
but that's only because there's more meat to the tutorial now :).
1. The coding standard in this document is PEP8, not the Twisted coding
standard. Have a look here:
http://twistedmatrix.com/trac/browser/trunk/doc/core/development/policy/coding-standard.xhtml?format=raw
2. "Callbacks are the lingua franca of asynchronous programming" strikes
me as an odd turn of phrase, especially one to open the document with.
3. "This document addresses ... Deferred. It..." - "It" has an ambiguous
antecedent. Are you talking about the document or the Deferred class? Of
course it becomes obvious, but it should be phrased so you don't need to.
4. It's far from obvious what "nonblocking_call" is supposed to be, given
that its definition is "pass". On my first skim through I thought it was a
callback, then had to stop, go back and read again when I realized that
didn't make sense. Brevity is good in examples, but this is too brief.
1. "input" is a builtin function. You might want to avoid using it
for a parameter name.
5. "You might be tempted to define it like this": you're switching
back and forth from second to third person; at first referring to the
reader, then an anonymous different programmer. It might be useful to give
these roles different names; "Alice and Bob" are popular.
6. If you must use a third-person pronoun (as you do the one time you
refer to the API's anonymous user); you should stick to a gender-neutral one
wherever possible, unless of course you're referring to a specific
character.
7. "The Deferred doesn't do anything that you couldn't have done with the
two callback parameters." This isn't strictly true; chaining callbacks, and
dealing with errors that arise in different layers of an asynchronous
callback chain, aren't strictly possible without some additional mechanism.
8. Deferred is mentioned as an API link, Failure isn't.
9. Your explanations of the examples seem backwards. "At its very
simplest, Deferred has a single callback attached to it". I think you
should be explaining the problem being solved by a single callback, since
the synchronous example isn't addressed. The synchronous example obviously
doesn't have a single callback attached to it :). In other words, document
"here's what you might want to do, here's how you can do it" rather than
"here's a thing you can do! by the way, you might want to do it
because...". You've addressed the general why-you-want-to-do-it in the
section above, but it would be helpful to do it in the small for each
specific example.
10. The DeferredList docs seem wonky in several ways.
1. The opening is hard to follow.
> We are now ready to consider our original problem
what original problem?
> a Deferred that would only fire
"fire"? what does "fire" mean? The term hasn't yet been introduced.
> after some other number of Deferreds fired
Yeah, I'm still not sure what you're referring to. Why would I want
to do this, again?
2. Users really shouldn't be subclassing Deferred themselves, so it's
bad to have an example that does that. Especially one which . The fact
that this is what DeferredList is is an implementation detail,
and an ugly
one at that. Try talking about gatherResults instead, and implementing a
function which does the same thing without a subclass. Or,
perhaps, a class
of your own which just delegates to Deferred for Deferred
behavior, rather
than inheriting it.
3. Users *definitely* shouldn't be subclassing Deferred without
upcalling to its __init__. I haven't tested them, but I'm
pretty sure these
examples will just blow up with tracebacks.
4. The examples are never invoked. It's semi-obvious how to use them,
but semi-obvious things are often invoked semi-correctly. Better to have
examples with can be run, or at least ones with a 0-argument entry point
named something like 'start'.
5. "Consider the following interaction of two Deferreds:". You're
setting this up as if it's going to be very formal, but then
your language
is sloppy; you don't name the different deferreds. One of them is "one
deferred", the other is "a Deferred". You don't describe them
independently, the relationship is implicit in the description.
Given that
you're describing a fairly complex constellation of objects with
which the
user isn't necessarily familiar yet, you should be clearly labeling the
Deferreds in question in the code sample with variable names
(something as
simple as "a" and "b" would probably do fine) and then consistently using
those names to refer to them in the prose as well, so it's easy for the
reader to follow exactly which thing you're talking about. A big problem
with technical documentation, *especially* documentation of Deferreds,
is that it's very easy for a reader to start confusing which thing is
which. Once again, it would be good to set up some kind of
concrete problem
first: *why* are we waiting on multiple Deferreds?
11. "Fluent Interface"? This is more new terminology — terminology
that I am not familiar with, I might add — that isn't defined anywhere in
the document. I think it's more of an appendix than something important to
the main narrative; composing Deferreds, returning a Deferred from another
Deferred, firing a Deferred from another callback, etc, should be covered
first.
1. "Batons" looks like it's going to be more fancy ad-hoc terminology - I
would recommend keeping the language simpler and consistent with other
Twisted documentation :).
12. Still a lot of enumerated lists. Obviously a bad habit to which I am
prone ;-), but when one uses an enumerated list, there should either be in
an expectation that the numbers will be useful. Either, as in this document
review, or code reviews, where the numbers can be used to refer to points in
subsequent discussion, or there's a clear separation of steps. It's not
really clear what the "two possible scenarios" lists are enumerations *of
*. Are they different things that can happen?
13. You should try eliminating the word "consider" from the document.
You seem to have the rhetorical habit, which I've seen from other people
(myself included), of having a sentence which is missing a clear
subject/verb/object relationship, and working around it by saying "consider"
or "let's say". For example, you want to communicate that there's a
Deferred somewhere with some callbacks. You can't just say "A Deferred with
some callbacks.", so you say "Consider: a Deferred with some callbacks", and
now the sentence *seems* complete, but it doesn't really communicate a
full thought.
Okay, I think that's enough feedback for now. I'll have to do more with
your next round of edits, or my feedback is going to be longer than the
document itself :).
* Why asynchronous?
> - Define synchronous and asynchronous
> - Multiplexing IO
> - Introduce a simple reactor based on select()
> * Why callbacks?
You might want to start with this one, since callbacks are even more
generally useful than asynchronous programming. Your suggestion of a parser
example makes this clear: even if you're parsing synchronously, you'll still
probably have callbacks for different parse rule matches.
> - Asynchronous interaction to synchronous interaction
> - Delocalized execution (the parser example)
> - High level functions in Python review
>
> Quite frankly, I'm stumped on "defining synchronous and asynchronous."
I'd start with the words themselves. synchronous means "at the same time".
This refers to the timing of the function call and its effects. In a
synchronous program, if I say "read()", then at that same time that "read()"
is called, the reading happens and the data is returned. But, in an
*a*synchronous
("not at the same time") program, "read()" is called, but its effects happen
later.
This can obviously be fleshed out quite a bit, but I think that core concept
is what's important to communicate.
> Asynchronous had always made sense to me, coming from JavaScript, since
> it was "you click this button and something should happen!" But that
> is a very different use-case of asynchronous programming than Twisted is.
Your experience with JavaScript — or at least, with GUI programming, since
JavaScript itself is terrible — might actually be a good way to explain the
problem here. One example I like to use to explain why sometimes you just
can't block is this:
button1 = Button()
button2 = Button()
# I need to wait for the user to click on this button
button1.waitForClick()
# okay now they've clicked it.
message("Hooray you clicked button 1")
button2.waitForClick()
# oh dang, but what if they want to click button 2 first!?!
although you can probably devise a more lucid variant of that :).
One of these days I really want to write a combined Twisted / GTK tutorial
that shows how to ask questions in dialog boxes without blocking and
sub-main-loops and other nasty tricks that GTK programs often get up to in
order to have a question-and-answer UI. Unfortunately, although these
examples do serve as easy-to-identify for learning Twisted programmers, it's
not always immediately clear how this corresponds to networking data, and
the extra complexity of GUI libraries makes it more difficult to run the
examples.
And Glyph raised some very salient concerns about what we were trying to
> teach people. I just don't know what direction people are coming from.
>
I think the best assumption of background for such an introductory tutorial
is to assume that the user doesn't really understand what problem Deferreds
solve, and has thus never done any substantial work in an asynchronous
environment. More experienced users will skim some parts, but that's fine:
more experienced users are easily able to figure out what Deferreds are even
with just the current documentation :).
We shouldn't treat this as a Python tutorial, but it should at least touch
briefly on callable objects and nested variables.
As such, the document now is targeted to "people who know the basics
> of asynchronous programming and grok callbacks", and I've incorporated
> Itamar's excellent suggesting of comparing explicit callback parameters
> and the Deferred object, which I hope dispells the notion of Deferred
> being magical fairly well (my assertion is Deferred is merely an
> abstraction over said callback parameters.) I've also fully fleshed
> out the Deferreds reference; any omissions are my fault.
>
Again, I think this might be assuming a bit too much. At the very least,
you should find a very, very good tutorial on callbacks and higher-order
functions in Python to point people to as a dependency, so that users who *
don't* have that experience can go read about it somewhere else. (Actually,
every dependency of every document *really* ought to have hyperlinks to
other resources that teach that dependency, so that a user who doesn't know
python but needs to dive into a Twisted codebase will be put on their way
quickly.)
Even people who have some Python experience, but use callbacks rarely, will
often discover there are things they don't know when they start programming
with Twisted and nesting 5 or 6 callbacks in a function. For example, many
people don't know all the fiddly rules of scope nesting. Take a poll of
some potential targets for this intro documentation and ask them if they can
explain why this produces an error:
def f(x=1):
def t():
if x > 3:
x = 2
else:
return x
return t
... but adding 'x=x' to the parameter list of 't' makes it work (although
not like they would expect if they manipulated 'x' in f).
The plan next is to discuss composing deferreds (which will also
> touch on when you should and how to create your own deferreds, as
> well as deferredlist) and the convenience primitives.
>
I think you need to start talking about creating your own Deferreds, at
least implicitly, very early on in the document. For example, rather than
having "nonblocking_call" be a dummy function, have it maintain a list of
yet-to-complete calls, like this:
pending = []
def process(data):
return "Processed: <" + data + ">"
def nonblockingCall(data, whenSucceeded, whenFailed):
pending.append((data, whenSucceeded, whenFailed))
def completeOneCall(succeeded=True):
data, whenSucceeded, whenFailed = pending.pop(0)
if succeeded:
whenSucceeded(process(data))
else:
whenFailed(RuntimeError("It failed, for some reason."))
then (A) you can demonstrate how the callbacks actually get called in a tiny
little system that the reader can play around with and get comfortable in
before understanding Deferred, and (B) you can illustrate the same example
again with some Deferred logic involved.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: </pipermail/twisted-python/attachments/20090806/a11f9edf/attachment.html>
More information about the Twisted-Python
mailing list