http://www.perlmonks.org?node_id=734238


in reply to Re^2: IO::Lambda: call for participation
in thread IO::Lambda: call for participation

Likewise, after puzzling my way through the documentation, I'm intrigued, but see a steep learning curve before I'd understand how I'd use this in practice. I don't think this is unique to IO::Lambda -- POE has a similar, perhaps even steeper learning curve. It wasn't until I struggled through POE::Kernel and POE::Session and similar documentation that I really understood what was going on.

Some reactions:

In general, I think you assume too much knowledge of functional programming and use too much unfamilar jargon (e.g. "predicates") without clearly explaining each one. The whole "apologetics" section should be moved to the end as it distracts from explaining how to use the module.

The synopsis itself is too complex or else insufficiently commented to explain what is going on.

Many of your examples might be clearer if you were explicit about return and fixed up some indentation. E.g. edited from the synopsis:

# return an IO::Lambda object for a given $host sub http { my $host = shift; my $socket = IO::Socket::INET-> new( PeerAddr => $host, PeerPort => 80 ); return lambda { context $socket; # argument stack for other calls # register a callback for when $socket is writable write { print $socket "GET /index.html HTTP/1.0\r\n\r\n"; my $buf = ''; # register a callback for when $socket is readable read { return $buf unless sysread( $socket, $buf, 1024, length($buf)); # restart the "read" state if more is available again; } } } }

There are too many ways of doing things described too early and without context (no pun intended). For example:

A lambda is initially called with some arguments passed from the outside. These arguments can be stored using the call method; wait and tail also issue call internally, thus replacing any previous data stored by call. Inside the lambda these arguments are available as @_.

The part in bold is extraneous and distracting, as it forces the reader to ponder the relationship between different ways of calling a lambda -- it exposes implementation details that are irrelevant to initial understanding. Moreover, I think you mean that the arguments are made available as @_ in the callback attached to the lambda object. "Inside the lambda" is a bit vague.

Overall, I think you might need to explain more of the core principles for async programming and why they are necessary. Likewise, I think you need to explain subtleties in your examples, like why the "read" predicate is inside the "write" predicate. (Presumably, we don't want to start listening until we've actually sent the request via the write predicate.)

Then I think you need to walk through what happens in an example. So the "http" lambda is executed via "wait" -- a "write" state callback is registered. (Is it executed immediately? Does it execute after the lambda callback executes?) The "write" state is the only one registered and it's immediately valid (socket is writeable) so the "write" callback is executed. During that callback, a "read" state callback is registered. When the "write" callback finishes, the socket is readable so the "read" callback is executed one or more time. And so on. (Why does execution stop?)

So my general advice is to start very small, very simple and explicit and then build up from there.

-xdg

Code written by xdg and posted on PerlMonks is public domain. It is provided as is with no warranties, express or implied, of any kind. Posted code may not have been tested. Use of posted code is at your own risk.

Replies are listed 'Best First'.
Re^4: IO::Lambda: call for participation
by dk (Chaplain) on Jan 05, 2009 at 19:35 UTC
    Thank you really so much, cannot stress enough, this is by far the most valuable response I've got with regards to the factual problems in the docs. I assume there are more problems of this sort, but I hope I should be able to figure them out myself, given guidelines I extract from your (and of course not only your) advices.

    What I've realized, is that starting small and going baby steps was something that I unintentionally avoided, as I thought that explaining obvious things would be an offense to the reader. Also, no one (until now) told me that it's not bad at all. Thus, you also partially answered the question I asked Randall, where is the generally acceptable simplification level I can begin from.

    Thank you again. Your analysis is valuable to me.

      ... explaining obvious things would be an offense to the reader ...
      In my opinion, the line for this is far far far further down than you would ever expect until you've been doing docs for quite some time.

      Especially if you're writing to a relatively wide audience... if you can narrow your audience to only an advanced few, you can take shortcuts, but even then, advanced people know to skip the things they already know. Beginners cannot make up stuff that they don't know.

      You're welcome.

      I have some follow-up thoughts, now that IO::Lambda has been rattling around in my head for a few hours. Mostly it relates to how I'd try to explain it to someone. Is this explanation correct:

      • A IO::Lambda object (a "lambda") is an FSM generator. The lambda() function takes an initialization callback and returns a lambda. When the lambda is executed, the FSM is created, the initialization callback is executed to dynamically register event handlers and associated callbacks and event processing begins.

      • Lambdas are executed using method calls to the lambda, such as call(), wait(), etc. (as described elsewhere). (In what circumstances would I want to use one method or another?) Arguments to these execution methods are passed to the initialization callback in @_ for dynamic configuration.

      • Within the initialization callback, special functions called "predicates" register a callback in response to specific events. The argument to a predicate function is the callback function itself. The context() function is used prior to calling a predicate to populate an argument stack for subsequent predicate calls. The argument stack must match the argument signature of the predicate. (E.g. the read() predicate requires a filehandle so context() must be called with a filehandle argument before read() is called.)

      • Predicate callbacks may also call predicates, registering a new event handler during the processing of an event. If context() is not explictly called, the context that existed when the currently executing callback was registered is used.

      • Some predicates may have side effects in addition to registering an event callback. For example, the tail() predicate executes a lambda (given in the context stack) and runs the callback provided when the lambda finishes running.

      That's my rough take on it. I think I'm being a little imprecise around the definition of "predicate" as you seem to indicate that lambda() is also a predicate. I remember enough of HOP to know why. In a way, predicates just define conditions under which callbacks are to be run. The lambda() function returns a lambda that can only be run explicitly with a method call. The other predicates use the context to provide sugar for automatically running the callback under certain conditions.

      Also, explaining what gets returned from running a lambda really needs a lot more explanation and some examples. Otherwise, I'd be tempted to use a global to make sure each callback put its output in the right place so I didn't have to worry about what happens inside the black box.

      -xdg

      Code written by xdg and posted on PerlMonks is public domain. It is provided as is with no warranties, express or implied, of any kind. Posted code may not have been tested. Use of posted code is at your own risk.

        A IO::Lambda object (a "lambda") is an FSM generator

        That is correct.

        Lambdas are executed using method calls to the lambda, such as call(), wait(), etc. (as described elsewhere). (In what circumstances would I want to use one method or another?)

        That too is correct. You would usually use wait(), or rather even wait(@args), that passes @args to the very first callback associated with lambda (where these are seen as @_), starts the lambda, and waits for its execution. However, call(@args) only stores the arguments, but does nothing about execution. Such pre-initialized lambda can still be awaited by wait(), which doesn't clear @args if no arguments were passed to it. Basically, $x-> wait(@args) is same as $x-> call(@args)-> wait. Also, wait_any() and wait_all() operate more than on one lambda; these functions do not pass arguments at all.

        I also like to juxtapose synchronous and asynchronous ways of executing lambdas. wait() and friends are synchronous. tail() etc functions are asynchronous. There are even pairs that do the same in sync and async modes: wait() and tail(), wait_for_all() and tails(), wait_for_any() and any_tail().

        Within the initialization callback, special functions called "predicates" register a callback in response to specific events. .. Predicate callbacks may also call predicates

        These two are also correct.

        Some predicates may have side effects in addition to registering an event callback.

        I wouldn't call it side effect, I think of it rather as a syntactic sugar. That is because tail(@args) can pass arguments to a lambda, and there's no point in passing arguments to an already running lambda. I agree that it looks like a side effect when you explicitly don't want to start a lambda that is already finished, but that is what autorestart() method is for.

        As for the predicates, as you say, they "just define conditions under which callbacks are to be run". And that is one of the ways to put it, correctly and tersely, without resorting to vocabulary of functional languages. That is though a challenge on its own, to coin simple but understandable descriptions for sophisticated concepts.

        Also, explaining what gets returned from running a lambda really needs a lot more explanation and some examples

        I see. Result of running a lambda is whatever the last executed callback has returned. This is simple for linear execution, f.ex. if

        my $x = lambda { context 1; sleep { return 42 } };
        then $x->wait returns 42. Pipelining also works in the similar vein:
        my $y = lambda { context $x; tail { return @_ } };
        $y-> wait returns 42 too. However, things get interesting when $y waits for $x and $z and $t, where the order of execution is not known. Nevertheless, it is the last result that gets returned.

        This uncertainty is partially addressed in tailo(), an "ordered" version of tails(). Where tails() returns results of lambdas in the order of their completion, tailo() keeps the order of the lambdas as they were stored in the context.