Beefy Boxes and Bandwidth Generously Provided by pair Networks
laziness, impatience, and hubris

Re: Serious Exporter Problems

by etcshadow (Priest)
on Jul 09, 2005 at 16:15 UTC ( #473692=note: print w/replies, xml ) Need Help??

in reply to Serious Exporter Problems

This is a problem that originates from the difference between compile-time and run-time, and how the compile-time of one module is the run-time of modules it uses. Think about it: if module A uses module B, then module A's compile-time is not complete until after module B is both compiled *and* run. So... what if A uses B and B uses A? Then A's runtime wants to get tangled up in its *own* compile-time.

I'll skip the suspense and just tell you the specific answer to your problem, though: you have to wrap your @ISA = 'Exporter' and @EXPORT = ... bits of code in a BEGIN block. Look at the example below (to which I've added some verbose information, so that you can really see what's happening):

### ### package XXA; BEGIN { print ((" " x $main::x++) . "Beginning XXA compile\n") } use strict; #BEGIN { use Exporter; use vars qw( @ISA @EXPORT ); @ISA = qw( Exporter ); @EXPORT = qw( xxa ); } use XXB; BEGIN { print ((" " x $main::x) . "Just used XXB in XXA compile\n") } sub import { my ($self) = shift; print ((" " x $main::x) . "XXA->import called\n"); $self->export_to_level(1, @_); } sub xxa { xxb; } BEGIN { print ((" " x --$main::x) . " Finishing XXA compile\n") } 1; ### end ###
### ### package XXB; BEGIN { print ((" " x $main::x++) . "Beginning XXB compile\n") } use strict; #BEGIN { use Exporter; use vars qw( @ISA @EXPORT ); @ISA = qw( Exporter ); @EXPORT = qw( xxb ); } use XXA; BEGIN { print ((" " x $main::x) . "Just used XXA in XXB compile\n") } sub import { my ($self) = shift; print ((" " x $main::x) . "XXB->import called\n"); $self->export_to_level(1, @_); } sub xxb { xxa; } BEGIN { print ((" " x --$main::x) . " Finishing XXB compile\n") } 1; ### end ###
Now, just do this:
me@host> perl -c Beginning XXA compile Beginning XXB compile Beginning XXA compile Just used XXB in XXA compile Bareword "xxb" not allowed while "strict subs" in use at line 2 +6. BEGIN not safe after errors--compilation aborted at line 29. BEGIN failed--compilation aborted at line 15. BEGIN failed--compilation aborted at line 15. me@host:>

That's basically the situation you are in currently. Now, go and un-comment the BEGIN keyword before the Exporter blocks in those modules...

me@host> perl -c Beginning XXA compile Beginning XXB compile Beginning XXA compile Just used XXB in XXA compile Finishing XXA compile XXA->import called Just used XXA in XXB compile Finishing XXB compile XXB->import called Just used XXB in XXA compile Finishing XXA compile syntax OK me@host>

Voila! Problem fixed. This should really be in some sort of FAQ, somewhere... I've gotten into arguments with VERY experienced perl programmers about this very topic before (that you must *ALWAYS* but your @EXPORT and @ISA in begin blocks (or use for your @ISA declarations)).

------------ :Wq Not an editor command: Wq

Replies are listed 'Best First'.
Re^2: Serious Exporter Problems
by tilly (Archbishop) on Jul 10, 2005 at 05:22 UTC
    You have gotten into arguments before, and you're about to again.

    You have set up a bad design and found one way out of a problem that it causes, and then you think that way is how everyone should do it. This is wrong.

    The real moral is that circular dependencies are a Bad Thing. They cause lots of complexity, and compilers don't like dealing with them. That is true in virtually any language! Avoid having circular dependencies and you'll avoid running into many subtle and strange issues.

    Secondly there is another way out of your issue. Rather than putting manipulations of @ISA into BEGIN, you could switch from use to require. This not only solves you problem, it also gives you a shot of successfully handling the situation where one module actually calls functions in the other in setting up class variables. OK, it doesn't guarantee that (see what I said above about circular dependencies), but you have a chance.

    Thirdly there are other issues with BEGIN blocks that you're ignoring. People disagree on them, but if you read (tye)Re: Supersplit carefully you'll see that there are subtle issues with, for instance, error reporting which make it desirable to avoid playing subtle BEGIN games.

    So avoid circular dependencies and don't play subtle BEGIN games. Your code will be less complex and it will work better. Furthermore if you move into other languages, this habit will avoid your having to worry about nasty corners of how they deal with circular dependencies. (A few handle it smoothly, some break, some handle it but you get fragility as a result. Perl falls into the last category.)

      You have set up a bad design and found one way out of a problem that it causes, and then you think that way is how everyone should do it.
      To be pedantic, it was Petamem that set up a bad design; etcshadow was just suggesting a way out that didn't involve redesign (redesign probably being a suggestion that would be ignored). Sometimes bad design just has to be coped with.
        To be pedantic, it was Petamem that set up a bad design;

        I would rather call it suboptimal instead of "bad". Actually - by thoroughly analyzing etcshadows suggestion, we deducted where the flaws are (circular reference) and will eliminate them.

        Actually much of this has happened already, which - IMHO - could be a sign, that the design is not that bad.

            All Perl:   MT, NLP, NLU

      Well, as to the bit about the arguments... they've really not been about questions of circular dependancies, but rather about the specific question at hand (importing from a module that imports from you), and the necessity of setting up of your exports at compile-time. That's why I happened to have a fairly detailed example of this imediately at hand.

      As to the outright condemnation of circular-uses, I have to respectfully disagree. In managing a complex project, it can become a fairly arbitrary line to draw between your modules. It forces you to structure your code-containers around your code's calling-graph, rather than around the functional nature of your code.

      I spend most of my time writing code for healthcare, so here is a simplified example: I want to be able to group code for patients in a Patient module, and code for claims in a Claim module. There are perfectly reasonable cases which require the processing of a claim to have to access patient functions, and vice-versa. Patient's have claims, and the description of a patient is not complete without his/her claim-history. When displaying a claim, I need to access the patient's information, as it is a fundamentally important part of the claim. If I understand your "circular uses are bad, 'mkay" correctly, then I should not be allowed to organize my code this way.

      Also, as to the comment that other languages do not allow circular dependancies, that's just not true (in my experience). Certainly *some* other languages will not have a means to allow for it... but *some* other languages do all sorts of crazy things and impose limitations that I don't ever want to deal with as a user of that language. Even a language which provides as little in the way of facilities to the programmer as C has a means for dealing with circularly dependant libraries. First, there is a separation between headers and code... that goes a long way, on its own. Second, a fairly simple method for preventing pre-compiler infinite-loops is employed:

      #ifndef some_token_i_use_to_represent_this_file #define some_token_i_use_to_represent_this_file 1 /* your code goes here */ #endif

      So, C, and any language which inherits it's basic separation of header files and code files from C, along with a pre-compiler as capable as C's is set. It can handle circular dependancies.

      I guess that gets somewhat to a larger point about this question (as well as some of the issues you raised with circular uses in perl): different languages do things differently with respect to this, and in many of them (all, maybe), there is a necessary discipline to build atop the language, itself, to protect yourself from problems. In perl, this discipline includes understanding the difference between BEGIN time and run-time, and using that understanding appropriately. Maybe perl requires a little bit too much in the way of requiring discipline from its programmers, in this regard... on that I won't comment. It certainly wouldn't be the first time perl's been accused of requiring self-discipline of its programmers, rather than enforcing discipline upon them.

      However, I actually don't think that there is anything all that wrong with publishing some best practices as a way to encourage that self-discipline in people who haven't yet attained the understanding of why that self-discipline is necessary. In my mind, telling people to always put their @ISA and @EXPORT setup in a BEGIN block is kind of like telling people to use strict. Actually, a better comparison would be like telling people to NEVER put a "my" operator in a conditional. The reason why it's important to do this is tricky to divine on your own, and may not bite you imediately. Still, there's no harm in doing it, and it may (proably will) prevent errors, later.

      ------------ :Wq Not an editor command: Wq

        My solution is to put everything in objects. I get a lot fewer problems that way. X, Y, and Z may all use each other indiscriminately, but because there are no exports, I get no problems. Best of all worlds ;-)

        (I'm not using B as an example because we've run into that problem before - just like using $a and $b as example variables gets some unexpected side effects, B as an example package name can get some (rare) unexpected side effects as well.)

        Let's see.

        You ignored the alternate require solution. You ignored the error reporting issues with BEGIN type games which argue for not playing them. You dismiss the existence of languages that don't handle circular dependencies. (I believe that PL/SQL is one.) You don't seem to be aware that your solution doesn't solve all of the issues with circular dependencies in Perl. (Example, suppose in your example that a Patient has claims, and a claim has a Patient. Both of those are cached for efficiency. Guess what? You have a memory leak!)

        As for your example, there are plenty of ways of handling this. For instance you can have a module that you're supposed to use which does all of the other uses for you, in the right order. Or you can use an OO style which avoids the need to export anything and makes circular dependencies not quite as bad. Or you can use lazy loading so that dependencies are resolved as they're about to be called. And often you can find a design which avoids the circular dependency entirely. (Rule of thumb, in a good design each module will call few others, but will be called from many other places. If this is achieved, then circular dependencies become unlikely.)

        Any of these solutions can be sufficient. All of them avoid having to think hard about compile vs runtime. All of them avoid the error reporting problems that I pointed out can happen with BEGIN games.

        Given the existence of alternatives, I'd prefer to not make recommendations that make code more complex and cause problems of their own for the possibility that someone will create grief. Instead I'd prefer to suggest the discipline of avoiding creating circular dependencies, and then let people know what problems they create when you do cause them.

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: note [id://473692]
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others chilling in the Monastery: (7)
As of 2017-01-18 12:28 GMT
Find Nodes?
    Voting Booth?
    Do you watch meteor showers?

    Results (161 votes). Check out past polls.