Beefy Boxes and Bandwidth Generously Provided by pair Networks
We don't bite newbies here... much
 
PerlMonks  

The Null Mull (or, when OO needs more O)

by brian_d_foy (Abbot)
on Nov 29, 2004 at 06:46 UTC ( [id://410871]=perlmeditation: print w/replies, xml ) Need Help??

I've been thinking about the Null design pattern recently. People get wrapped up in UML diagrams, Gang of Four descriptions, and all sorts of other things. Patterns are really simple, and this is one of the simpler one. It's not complicated enough to get it's own module (there is a Class::Null), and this isn't everything I've been thinking about, but it's a start.

Everyone has probably experienced the pain and frustration of something in their script trying to call a method on something that isn't a reference.

$ perl -e '1->method' Can't call method "method" without a package or object reference at -e + line 1.

This might happen from something we programmed ourselves, or because something in the bowels of a module tried to do something it shouldn't have done. This is one of my favorite examples while teaching when I need to show people which languages have objects and which don't. For extra credit, out of Perl, Java, C++,and Ruby, which one is really object based?

The problem at the very bottom of this is Perl's lack of a true object system. Perl may be an object oriented language only in that parts of it are oriented toward objects, but, like a lot of other languages that claim to be object-oriented, it has things that aren't objects. That's also what makes Perl great and malleable.

If we care about this sort of thing, and there are people who do care (perhaps too much, even), we just need to use more objects. We don't have to go crazy and make everything an object. People who like that sort of thing probably don't use Perl anyway.

So, what to do when we need to call a method but we don't have an object to send the method too? For instance, in method chaining, the previous step has to return an object to go onto the next step. methoda() has to return an object that can respond to methodb().

my $result = $obj->methoda->methodb->methodc;

If methoda() doesn't return an object, the program dies. Not only that, if methoda() doesn't return an object that can respond to methodb(), the program stops. That's broken. If we design our interface, it shouldn't break, even if things fail. We shouldn't have to duct tape an eval {} around the code to catch it from breaking, and we should be able to choose how to handle errors before perl has to.

If we can't return the object we want because things went wrong, we can return a different object. No law in the universe says you have to return an object of the same class.

Since we only need to keep the chain from breaking (which is different than making it keep working), we need something to call methods on, and it has to respond to any method we decide to throw at it since we don't know a priori which methods we might call on this other object. No problem: just return itself for any method. An AUTOLOAD function takes care of that.

package Object::EveryMethod; my $all = bless {}, __PACKAGE__; sub new { $all } sub AUTOLOAD { $_[0] }

Now I use this in my methods. If you already do the right thing with your failure codes, you have the magic value in some sort of variable or symbolic constant. Just shove the new object in there instead. The object $all only exists once, and everyone shares it. Since it doesn't do anything or store any information, we don't need more than one. This is the Singleton design pattern, for those that care. I wrote about singletons in The Perl Review 0.1

# my $False = 0; # old way my $False = Object::EveryMethod->new(); sub methoda { my $self = shift; #... if( ... everything worked ) { $self } else { $False } }

That takes care of the method chaining problem. It's not going to break because it tries to do something it shouldn't do, like call a method on a non-object. The chain goes all the way to the end, but the object switches at some point to an instance Object::EveryMethod. At the end, the last method returns the Object::EveryMethod reference. That's our failure code. Instead of 0 or undef or whatever we get are false object.

Once we get the result, we want to know if whatever we tried to do did what we wanted it to do or if it failed. If we are doing what we are supposed to be doing, it can only fail in one way: it gives us back an instance of Object::EveryMethod. If it worked, we get back some other object. We can use UNIVERSAL::isa to check that, and we can use a function to hide that nastiness.

sub hey_that_worked { not UNIVERSAL::isa( $_[0], 'Object::EveryMethod' ) }

There are other tricks to make that we can do to test the result, but you can figure those out on your own. As long as we have a way to check, we're all set.

We may also want to know when things go wrong and why. We have the initial concept, and if that's not good enough for us, we just extend it a bit. Instead of a singleton, we create a error object that can carry so information. We don't have to interrupt the flow of the program with crazy things like exceptions, and the error checking won't dominate the useful parts of the program.

Let's change the Object::EveryMethod a bit. We'll add some information to the object, and a method to get the information back. Now, the first method to create it gets to set the message and it's identity. Any method we call on it just returns the object, including all of its original information.

package Object::EveryMethod; sub new { my $class = shift; bless { message => $_[0], setter => ... caller stuff, }, $class; } sub AUTOLOAD { $_[0] } sub what_happened { @{ $self }{ qw( message setter ) } }

We use it a bit differently. We create a tiny object for each error (or, we can use a pool of flyweight objects).

sub methoda { my $self = shift; #... if( ... everything worked ) { return $self } else { my $failure = "Oops, I did it again!"; return Object::EveryMethod->new( $failure ); } }

Once we have the Object::EveryMethod, we can check it like before and then get information out of it to see what went wrong.

From there the variations are up to you, and you can design the Object::EveryMethod class to fit your needs. Indeed, you should make your own instead of using some module. Make your own design decisions to fit the situation. Design patterns aren't about using the right modules: they're about doing things that fit into the rest of the project.

We can hang some other fancy features on Object::EveryMethod. Since we use AUTOLOAD for everything, we can't call the can() method on them while returning a value that users of can() expect. We have to give back true or false, not another object. We just add our own can() method that always returns true.

package Object::EveryMethod; sub new { my $class = shift; bless { ... }, $class; } sub AUTOLOAD { $_[0] } sub can { 1 } sub what_happened { @{ $self }{ qw( message setter ) } }

If that doesn't work for us, we can make it return false, except for the methods we defined. If we are alrady using can(), we probably expect the object to actually do something useful when we call the method we check and have a way to handle it if the object can't do what we need. Since we define our own can(), Perl finds that one before it gets a chance to check UNIVERSAL.

package Object::EveryMethod; sub new { my $class = shift; bless { ... }, $class; } sub AUTOLOAD { $_[0] } sub can { 0 unless defined &{$_[1]} } sub what_happened { @{ $self }{ qw( message setter ) } }

We need only ensure that what_happened() (or any other method in Object::EveryMethod) doesn't exist in any other class interface, or we're back to breaking things again. There are lots of ways around that too.

So, for not too much work, we get a way to not break method chaining, don't have to use eval {} to catch broken code, don't need exceptions, and it can fit into what we're already doing with all of our other objects. Beware though: just because it has all that, it isn't necessarily the right way to do things for whatever we are doing.

--
brian d foy <bdfoy@cpan.org>

2004-11-29 Janitored by Arunbear - added readmore tags, as per Monastery guidelines

Replies are listed 'Best First'.
Re: The Null Mull (or, when OO needs more O)
by ysth (Canon) on Nov 29, 2004 at 07:16 UTC
    You seem to be missing something that I've seen in every other implementation of this: something to make the result false when a method is no longer being called on it, via overload. So that:

    my $obj = Foo::->new->set_bar("iron")->inc_baz(1000)
    or die "something in there went horribly wrong: ", $obj->errmsg;
    $obj or die "something in there went horribly wrong: ", $obj->errmsg;
    (Updated to fix nit picked by ihb)

    can trigger the error clause. But I'm not a big fan of this method of working anyway; when I've had to chain lots of methods (usually only when using B), I just wrap the whole chain in an eval block.

      I left out the overload stuff on purpose. I didn't want to get into it because it's just one way of doing things, and the recipe is not a concept. If someone gets the idea, they can implement the other stuff so it fits into how the rest of the system does things. I'd much rather see people use a consistent metaphor rather than cobble together different styles.

      The Gang of Four, or any other designs pattern book, is not a cookbook ;)

      --
      brian d foy <bdfoy@cpan.org>

      I'd also have it die if it gets destroyed w/o anyone having asked it whether it failed or what the failure reason was. Otherwise, $obj->this()->that()->other(); can silently break.

      - tye        

      Nitpick: you have to have die in another statement if you want to do that.

      ihb

      See perltoc if you don't know which perldoc to read!
      Read argumentation in its context!

Re: The Null Mull (or, when OO needs more O)
by bart (Canon) on Nov 29, 2004 at 14:59 UTC
    Everyone has probably experienced the pain and frustration of something in their script trying to call a method on something that isn't a reference.
    $ perl -e '1->method'
    As an aside, I stumbled across a module on CPAN just yesterday, which would seem to make this possible: Scalar::Properties.
      Scalar::Properties looks wickedly neat from a quick glance at the docs. Has anyone used it in production code?
Re: The Null Mull (or, when OO needs more O)
by elusion (Curate) on Nov 29, 2004 at 16:31 UTC
    I couldn't turn up any great links on the subject, but Objective-C (as used in Apple's Cocoa development) uses this pattern. Calling a method on a nil object returns nil. It'd be similar to this Perl, which would print undefined:
    # this would work if it were Objective-C # using Perl's syntax my $foo = undef; my $result = $foo->message; print "undefined\n" if not defined $result;
    I happen to like it.
Re: The Null Mull (or, when OO needs more O)
by jacques (Priest) on Nov 29, 2004 at 08:06 UTC
    For extra credit, out of Perl, Java, C++,and Ruby, which one is really object based?

    Ruby. That's too easy. Who would think Perl or Java?

      A lot of Java programmers think they're using a real object oriented language, and when I teach object oreinted Perl, there is usually one Java snob in the crowd who wonders why Perl doesn't do it like Java. I have to remind them that Java doesn't do it consistently either. :)
      --
      brian d foy <bdfoy@cpan.org>
Re: The Null Mull (or, when OO needs more O)
by samtregar (Abbot) on Nov 29, 2004 at 13:47 UTC
    Interesting article. I tried to sell the Krang developers on something like this but it was rejected as too much voodoo. I'm still not sure I was right, but I would have liked a chance to find out! I sure do hate tracking down method calls on undef...

    -sam

Re: The Null Mull (or, when OO needs more O)
by dragonchild (Archbishop) on Nov 29, 2004 at 21:19 UTC
    So, for not too much work, we get a way to not break method chaining, don't have to use eval {} to catch broken code, don't need exceptions, and it can fit into what we're already doing with all of our other objects.

    Would you mind going into a little more detail as to why method chaining without exceptions should be done? Most likely, you will still

    • have to check the return value
    • have to figure out where in the chain things broke
    • have to handle that error somehow in some readable fashion
    It sounds like a lot of work for the ability to have some admittedly very nice syntactic sugar.

    Personally, I haven't ever seen the need for chaining methods like that. This may be because I haven't set my classes up in such a way as to provide for it, but wouldn't that be a more Perlish way of doing things? As Larry is quoted in the Cookbook, "The trick is to use Perl's strengths rather than its weaknesses." It sounds like you're trying to get another language's strength - method chaining - and shoehorning it into Perl without the infrastructure that goes along with it.

    I'm just worried that this will be a maintenance nightmare. I would hate to come along as a consultant and be handed this stuff that uses chaining out the wazoo and tries to get cute with an AUTOLOADed everything object that overrides undef or falsehood and ...

    See what I mean?

    Being right, does not endow the right to be rude; politeness costs nothing.
    Being unknowing, is not the same as being stupid.
    Expressing a contrary opinion, whether to the individual or the group, is more often a sign of deeper thought than of cantankerous belligerence.
    Do not mistake your goals as the only goals; your opinion as the only opinion; your confidence as correctness. Saying you know better is not the same as explaining you know better.

      Before I start, I'm not saying that you should use this in every situation. It has to fit the problem you are trying to solve. If you are in doubt about whether it fits into your problem, it probably doesn't.

      That said, I think I've already covered your questions. You can still check the return values. If you got the error object, that's a failure. You can also figure out where things broke. That's an example in the original post. As for readability, you simply check the result and handle the error. Remember, I'm talking about the concept, not the recipe. If you understand the concept, you can implement it in several ways.

      People seem quick to wrap an eval around code
      my $result = eval { ...code... }; if( $@ ) { ... }
      This metaphor gets rid of some of that kludgyness. Now an error is handled as normal program flow rather than an exception. Add logging and warning capabilities and other things behind the scenes instead of the script level.
      my $result = ...code...; if( is_error( $result ) ) { ... }

      This isn't syntatic suga. It's not extra syntax at all. It's less stuff at the script level. The program is designed to handle errors, not patched to catch them.

      Don't get caught up in the method chaining. It's just one example. It's not always up to me what things I have to deal with and who else is writing code. I have to make things keep working even if other people chain methods. You don't have to shoehorn method chaining into Perl, either. It's there already. If you don't like it (and reasonably so), you don't have to design for it and you don't have to use it when the interface allows it. That's a personal preference, though. Other people do it, and I have to live with that. I'm trying to get away from people shoehorning exceptions into Perl, which is a true weakness. That's what all the eval and die nonsense is. ;)

      It's not a maintenance nightmare at all. You simply document how to check a return value. The scripter doesn't have to know anything about it. They just use the interface. They get a result, and test that result for true or false (or however the rest of the system does things). The mechanics are homologous to testing a result code through a method like a lot of modules already have in their interface. If the coders aren't checking return values, the program still breaks at the same point.

      But again, let me emphasize that you don't have to use this. Don't go looking for places to use this. Just file it away in your toolbox. Someday you might run into a situation where its useful, and those situations will come to you (not the other way around).

      --
      brian d foy <bdfoy@cpan.org>
        It's not a maintenance nightmare at all. You simply document how to check a return value. The scripter doesn't have to know anything about it. They just use the interface. They get a result, and test that result for true or false (or however the rest of the system does things). The mechanics are homologous to testing a result code through a method like a lot of modules already have in their interface. If the coders aren't checking return values, the program still breaks at the same point.

        brian, brian, brian. *sighs* If only 90% of people who type Perl code into an editor1 actually followed a tenth of what you say. But, alas, that is not the case. In fact, it is so often not the case that ... let's just say they already have enough rope to hang themselves with.

        You simply document how to check a return value. The scripter doesn't have to know anything about it. They just use the interface. They get a result, and test that result for true or false (or however the rest of the system does things).

        In 7 jobs located in 5 states over the last 4 years where I've used Perl, documentation was done in exactly two places. In one, a multi-billion dollar consortium that handles 40% of all credit card transactions, the documentation was done so we could have something to review code from. It was never actually kept up to date, nor was it any form of developer doc. In the other, a 100-million dollar insurance firm, since I wrote the system from scratch, I documented exactly enough for my protegee to keep the system running.

        What happened in the other 5 jobs? Bupkus! That's what happened. Motorola, BankOne, a tiny startup ... none of these firms documented their Perl code, processes, APIs ... nothing! You cannot assume anyone will document code, let alone Perl code. Remember - Perl isn't a programming language, so it's got to be really easy to figure out.

        If the coders aren't checking return values, the program still breaks at the same point.

        Actually, it won't. See, 90% of all Perl code currently running on some production system somewhere doesn't

        • use strict, warnings, or -w
        • check return values from system calls
        • check return values from DBI calls
        • use taint (I'm guilty of this one)
        • use hashes in any reasonable way

        As a consultant, that's the code that I have to support. And, because the people who wrote this pus-dripping, camel-vomiting dreck either have CS or MIS degrees, they obviously know everything there is to know about Perl. So, when someone comes up and says "Here, let's go ahead and make Perl feel more like VB", they're going to go "Ooooh!" and jump all over it.

        Except, they'll screw it up. But, since these flea-bitten, dog-kissing, motherless goats wouldn't recognize a development process if it painted itself pink and purple and danced naked on a bar wearing a fruit basket on its head, they DIP2 like it's 1959. Once it's in production, getting it out is like giving medicine to a toddler. Even though the child is only 30 inches talls and 25 pounds, it still takes 2 adults to hold the squirmy bastard long enough to squirt 4ml of pink goop into its mouth3.

        While this is partly tongue-in-cheek (could you tell?) ... I'm also being deadly serious. This proposal requires a lot of API documentation. I can definitely see a CPAN module using it, especially one like Mail::Sendmail for the example you gave above. Advocating development shops use it when the language doesn't have explicit support built in can be ... well ... disheartening for those of us who will have to maintain said code. I mean, can't you see how this is hurting the children?!? Think of the children, brian! The children!!!

        1. Please note that I did not say Perl coder / hacker / programmer / etc. I certainly did not want to imply that 90% of people who type Perl actually know what they're doing.
        2. DIP = Develop In Production. Only slighly better is TIP (Test in Production). And, yes, 90% of all Perl code in the world is either DIPed or TIPed. Believe it, or not.
        3. I'm not kidding. You'll want 3 adults if you don't want pink goop all over your shirt.

        Being right, does not endow the right to be rude; politeness costs nothing.
        Being unknowing, is not the same as being stupid.
        Expressing a contrary opinion, whether to the individual or the group, is more often a sign of deeper thought than of cantankerous belligerence.
        Do not mistake your goals as the only goals; your opinion as the only opinion; your confidence as correctness. Saying you know better is not the same as explaining you know better.

Re: The Null Mull (or, when OO needs more O)
by hardburn (Abbot) on Nov 29, 2004 at 17:12 UTC

    Since the undef SV is shared between all undefined values, one could modify undef using a feature in Perl 5.8. (I only thought of this because of the solution to #13 of How's your Perl? (II)).

    At the beginning of your program (possibly in a BEGIN block) (completely untested):

    &Internals::SvREADONLY(\undef, 0); undef = Object::EveryMethod->new; &Internals::SvREADONLY(\undef, 1);

    And override Object::EveryMethod for string/num/bool to behave like undef normally does.

    "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

      Hrmm ... 5.8.4/Solaris doesn't seem to do the right thing.
      BEGIN { &Internals::SvREADONLY(\undef, 0); undef = 42; &Internals::SvREADONLY(\undef, 1); } my $x = undef; print( ((undef) ? 'True' : 'False'), $/); print( ((defined undef) ? 'True' : 'False'), $/); print( (($x) ? 'True' : 'False'), $/); print( ((defined $x) ? 'True' : 'False'), $/); -------------- True True False False
      Maybe the code isn't actually affecting SV_UNDEF as you think ...

      Being right, does not endow the right to be rude; politeness costs nothing.
      Being unknowing, is not the same as being stupid.
      Expressing a contrary opinion, whether to the individual or the group, is more often a sign of deeper thought than of cantankerous belligerence.
      Do not mistake your goals as the only goals; your opinion as the only opinion; your confidence as correctness. Saying you know better is not the same as explaining you know better.

        I just ran your code on 5.8.4 i686-Linux with the same result you did. I'm not sure what is going on here--someone with more understanding of the internals is needed. My guess is that undef isn't staying shared like I expected it would.

        It does work with taking a reference to undef, but this breaks the transparency I was hoping for:

        BEGIN { &Internals::SvREADONLY(\undef, 0); undef = 42; &Internals::SvREADONLY(\undef, 1); } my $x = ${ \undef }; print( ((undef) ? 'True' : 'False'), $/); print( ((defined undef) ? 'True' : 'False'), $/); print( (($x) ? 'True' : 'False'), $/); print( ((defined $x) ? 'True' : 'False'), $/); print $x, $/; __OUTPUT__ True True True True 42

        "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

      undef isn't normally an lvalue. How about trying that as ${ \ undef } = ... instead?

        Still no:

        BEGIN { &Internals::SvREADONLY(\undef, 0); ${ \undef } = 42; &Internals::SvREADONLY(\undef, 1); } my $x = undef; print( ((undef) ? 'True' : 'False'), $/); print( ((defined undef) ? 'True' : 'False'), $/); print( (($x) ? 'True' : 'False'), $/); print( ((defined $x) ? 'True' : 'False'), $/); print $x; __OUTPUT__ True True False False

        "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

Re: The Null Mull (or, when OO needs more O)
by Anonymous Monk on Nov 29, 2004 at 15:03 UTC
    Not to be a punk here, but ...

    $obj->methoda->methodb->methodc

    If you don't know whether or not methoda returns null or not, you need to check for that. And such chains are a sign of bad design (aka TOO MANY OBJECTS!).

      Such a chain doesn't necessarily mean there are several objects. It might very well be the same object all the time:

      use Mail::Sender; eval { (new Mail::Sender {on_errors => 'die'}) ->OpenMultipart({smtp=> 'jenda.krynicky.cz', to => 'jenda@krynicky.cz', subject => 'Mail::Sender.pm - new version' }) ->Body({ msg => <<'*END*' }) Here is a new module Mail::Sender. blah blah blah ... *END* ->Attach({ description => 'Perl module Mail::Sender.pm', ctype => 'application/x-zip-encoded', encoding => 'Base64', disposition => 'attachment', file => 'W:\jenda\packages\Mail\Sender\Mail-Sender +-0.7.15.tar.gz' }) ->Close(); } or print "Error sending mail: $@\n";
      The methods return $self in case of success and throw an exception in case of failure.

      Jenda
      We'd like to help you learn to help yourself
      Look around you, all you see are sympathetic eyes
      Stroll around the grounds until you feel at home
         -- P. Simon in Mrs. Robinson

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://410871]
Approved by thor
Front-paged by htoug
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others musing on the Monastery: (2)
As of 2024-03-19 04:11 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found