Beefy Boxes and Bandwidth Generously Provided by pair Networks
Clear questions and runnable code
get the best and fastest answer
 
PerlMonks  

Thoughts on new 'class' OO in upcoming perl

by cavac (Parson)
on Mar 06, 2023 at 11:41 UTC ( [id://11150774]=perlmeditation: print w/replies, xml ) Need Help??

Intro

I've been looking at the docs for the new 'class' OO in upcoming Perl versions.

While i agree that Perl would benefit from a modern OO system, i think this new system shouldn't give up any of the flexibilities ye olde 'bless' provides.

Before i go into details, i must admit that i'm not a huge fan of the "attributes" stuff that most OO system use. Or at least not to the extend they are used anyway. I've seen code that uses like 4-5 attributes for a single class and just as much for a variable. I call this C++ line noise. Yes, it makes the code more concise(1), but when you have to spend a minute per line of code just to understand just the implication of these flags, they are not what i call helpful. Going forward, the new 'class' system will have to be very carefully design to avoid making a mess with attribute feature creep.

I also understand that 'class' is still in the early stages, so i can only look on what has been designed so far. My main questionmarks are:

  • Abort object creation?
  • Constructor not proper functions?
  • Deciding when to call parent constructor?

Let's look at those in detail:

Abort object creation?

One of the nice things about 'bless' is that constructors are just functions with return values. This allows the constructor a lot of flexibility. One of those is to not construct an object under some circumstances. Something like this:

package Foo::Bar::UrlHandler; sub new($proto, %config) { my $class = ref($proto) || $proto; if(!defined($config{url})) { # No URL given, don't know what to do next return; } my $self = bless \%config, $class; return $self; }

The caller can easily check if things worked out:

my $handler = Foo::Bar::UrlHandler->new(url => 'http://perlmonks.org') or do_some_error_handling();

From what i understand, a 'class' is pretty much required to be created, no matter if that makes sense with the given parameters. This will probably make error handling/detection in the caller more complicated. And no, eval() is seldomly the solution to such problems but many times the cause of them.

Constructor not proper functions?

From the design, it seems you don't write the constructor as a proper function, you can only 'ADJUST()' what it does. This has a few implications that make it much less flexible than bless():

No "named" constructor

Modules like DBI (among others) make use of the fact that bless() can run in arbitrarily names constructors. For DBI, this is connect(). In my opinion, this makes it much clearer to the user of this module that constructing an new instance is not only an in-memory operation, it also connects to the server.

No option for multiple constructors

In my projects, i have a few modules that provide multiple constructor. For example, there might be a classic new(list-of-parameters), but also a newFromConfig(filename) that takes a filename to a configuration file. This makes especially sense when using sub signatures. Another example would be, say, a file parser that has a newFromFile() and newFromUrl() method.

Yes, you can achieve this by subclassing with the new 'class' OO, but that can make the code harder to maintain, especially if only the initialization differs.

No simple "factories"

I sometimes use the concept of "factories", e.g. modules that decide on given parameters which object to return. In keeping with the example of the UrlHandler above, something like this isn't too uncommon:

package Foo::Bar::UrlHandler; sub new($proto, %config) { my $class = ref($proto) || $proto; if(!defined($config{url})) { # No URL given, don't know what to do next return; } if($config{url} =~ /^http\:/) { return Foo::Bar::UrlHandler::HTTP->new(); } elsif($config{url} =~ /^file\:/) { return Foo::Bar::UrlHandler::FILE->new(); } # Invalid URL? return; }

Deciding when to call parent constructor?

Sometimes it's useful to decide WHEN and IF(2) to call the parent constructor. I don't see how that is properly handled by the 'class' OO.

sub new($proto, %config) { my $class = ref($proto) || $proto; # Override the default template $config{htmltemplate} = 'righttoleft.tt'; # Call parent constructor to load and parse template my $self = $class->SUPER::new(%config); # Re-bless bless $self, $class; # Re-bless with our class $self->do_our_own_stuff(); return $self; }

Conclusion

The new 'class' OO is a long overdue project and i thank the developers for their hard work. But at this early stage, it seems to be only a copy of other programming languages, without the flexibility Perl can (and currently does) provide when it comes to object orientation.

I'm using Perl (and ignore the Moo* stuff(3)) because i can shape the language to fit the problem. If we go the way in which we have to shape the problem to fit the language, we all might as well switch to there-is-only-one-way-to-do-it languages like C++, Java or TypesScript.

Yes, 'class' has the potential to make object oriented code easier to read and write, and i'm certainly all for that. We just need to make sure that it turns out TIMTOWTDI enough not to feature the same headaches as Java or C++. I can only talk about this from one own, small personal viewpoint, but because of (not "despite of"!) the flexibility of the old bless() OO and the typelessnes of Perl variables, in the last two decades i was able to single-handedly write a big web framework and implement multiple commercial systems in Perl.

Footnotes:
(1) just as $_ does. Which makes following code flow (and debugging it) a huge pain. Use $_ is banned in my projects.

(2) Sometime you override, sometime you enhance, sometimes the parameters to new() tell you what to do

(3) Combining the rigidity of C++ with the speed of Windows Vista somehow never appealed to me.

PerlMonks XP is useless? Not anymore: XPD - Do more with your PerlMonks XP

Replies are listed 'Best First'.
Re: Thoughts on new 'class' OO in upcoming perl
by haj (Vicar) on Mar 06, 2023 at 15:30 UTC

    I have been playing with the new object system and in particular with its playground Object::Pad for some time now, and I'd like to encourage you to do this as well. For many of your points here there are solutions or workarounds. Whether you like them or not is a matter of personal taste, but knowing them helps. Well, and some of your points are just plain wrong.

    Abort object creation?

    In the new object system it is possible to abort object creation, and in many cases the OO system will do it by itself. Most of that has already been implemented in and popularized by Moose. The idea to have a declaration of constructor parameters instead of writing code for them. I agree that this needs effort to learn the new declarative syntax, and there are subtle differences between the different OO systems.

    In your example, a mandatory constructor parameter is missing, and in this case, and the OO system can abort the object creation. The difference is that the OO systems don't return undef (as your example does) but throw an exception.

    Of course, if you prefer to return undef in that case, you can have that, too: All you need to know is that stuff you used to write in the body of new now goes into the ADJUST phaser.

    Constructor not proper functions?

    With the new Object system a new function will be generated for you. But that's all about it. Put simply, the difference is that in the new OO systems new is sort of a bless on steroids, so instead of calling bless in the constructor function of your choice, you now call new.

    So, the only downside is that the name new is now occupied by the OO system itself. I am using "named" constructors a lot (they now call Class->new(...) instead of bless(...)), occasionally multiple constructors and also factories. In your factory, you need to change the name to e.g. urlHandler, since new is now taken. That doesn't seem unsurmountable.

    Deciding when to call parent constructor?

    Ah, here's an area where there are differences between bless-based classes, Moo(se)-style OO systems and the new core OO system. In short: You don't ever call parent constructors in core OO. This is intentional.

    Instead, the declarations of the parent classes are evaluated by the auto-generated new-method, and the ADJUST-block of the parent is called, in addition to performing these steps for the current class.

    Your example of a subclass setting a field to a fixed value has been discussed in great length in the GitHub discussions on Corinna, in particular issue 77. The length of this discussion should indicate that people are aware that this is a bit of a rough area, but nothing (as someone said on IRC) that an ADJUST block can't fix.

    Conclusion

    I agree that the core OO system takes away the flexibility of the DIY-objects one can build with bless. However, ignoring Moo* is, well, ignorance. Many lessons from Moo* went into core OO. It is a trade-off between the benefits and restrictions, but you can't get a good view if you ignore the benefits.

    The best thing of all: bless isn't going to go away. You can continue to hand-craft your classes, hand-write constructors and accessors for maximum flexibility.

      Well, and some of your points are just plain wrong.

      That's why i discuss this sort of stuff on PM, instead of ranting about "the unfairness of it all" somewhere else on the internet. I always strive to learn ;-)

      Your example of a subclass setting a field to a fixed value has been discussed in great length in the GitHub discussions on Corinna

      As long as it's a topic that isn't forgotten, i'm happy. If in the end the decision is "we will not allow that", i can live with that and will find ways to integrate "the new way of doing things" in my code.

      Instead, the declarations of the parent classes are evaluated by the auto-generated new-method, and the ADJUST-block of the parent is called

      I assume the ADJUST call order is this:

      1. grandparent
      2. parent
      3. current class

      Correct?

      However, ignoring Moo* is, well, ignorance.

      I didn't ignore Moo* as such. I looked into it (a long long time ago, though). I dimly remember it seemed to have a lot of performance and RAM usage problems, at least for my workloads. And it didn't bring enough benefits for my single-developer projects to make sense.

      Things might have changed. And having a newer OO system integrated into Perl core is a different ballgame altogether.

      But to be fair, i can be a very stubborn, ignorant person sometimes who likes to do some things "the old ways". But if i find some new way that i like more, i'am not unknown to become a preacher of those "new ways of doing things". I haven't yet decided if i will "go class" or "stay bless". But you can take it from the time i've taken to look into it and discuss it here on PM that i think it certainly has peaked my interest in a serious (and good) way. If i thought "class" was rubbish, i would have just ignored it completely ;-)

      PerlMonks XP is useless? Not anymore: XPD - Do more with your PerlMonks XP
        I didn't ignore Moo* as such. I looked into it (a long long time ago, though). I dimly remember it seemed to have a lot of performance and RAM usage problems, at least for my workloads. And it didn't bring enough benefits for my single-developer projects to make sense.

        The asterisk is doing a lot of work there. Moose has always been and continues to be a beast (appropriately enough). That is to say it is big, slow and includes a ton of stuff I will never use - YMMV. OTOH, Moo is much smaller and leaner and I've had success using it without being too disappointed with the performance. However, for me (and this is very much an individual opinion) Class::Tiny hits the sweet spot of enough good stuff to be worth using with minimal (basically immeasurably small) overhead. Combine it with Role::Tiny and you have yourself a modern OO system that's bloat free.

        Half the time I still just use bless and friends. Some of those times the model is standard enough that I should have gone with Class::Tiny from the start and other times the flexibility was a bonus. Horses for courses. But no mooses for me.


        🦛

        I am optimistic that the new Perl core OO will get the deserved amount of discussion, like so many of its precedents on CPAN. No harm in different opinions :)

        Regarding your question about ADJUST call oder: You are correct, it is parent first, child last.

      Of course, if you prefer to return undef in that case, you can have that, too: All you need to know is that stuff you used to write in the body of new now goes into the ADJUST phaser.

      How? How do you prevent your OO system from throwing in case a mandatory parameter is missing and convince it to return undef instead? How do you convince it to return undef from within an ADJUST whateverscifiweaponyoudecidetocallit?

      Your attribute based constructor abort is very limited and while throwing an exception inside an ADJUST thingie would likely abort the constructor ... after wasting time and resources in the constructors and AJDUST lasers of the parent class(es) ... there doesn't seem to be a way to return undef.

      Your OO system's new doesn't look like bless on steroids. It sounds more like bless overdosed with antidepressants.

      Deciding when to call parent constructor?

      If it's something an ADJUST block can fix SHOW US HOW!

      Jenda
      1984 was supposed to be a warning,
      not a manual!

        How? How do you prevent your OO system from throwing in case a mandatory parameter is missing and convince it to return undef instead?

        Good catch! I mixed up things here. It is definitely not possible to coerce a new method to return undef on failure. If you really want that, you need to write a custom constructor. I often write custom constructors, though mostly for other reasons.

        The new core OO system didn't invent this behavior, though. It is quite common with many OO systems on CPAN: They write a constructor method new for you, according to some declarative syntax. Therefore, new is no longer a "user defined constructor method": It is rather a part of the language on the same level as bless (disregarding the specifics of its medication for now). In none of these OO systems you get a chance to call the parent constructor. They all claim that you don't need to call the parent constructor, and they all have some mechanism to deal with inheritance during construction. For the new core OO system, ADJUST blocks are that mechanism.

        There are differences between the new core OO system and the popular CPAN OO systems, and these affect the construction of objects, in particular in an inheritance hierarchy:

        • Moose (also Moo and Object::Pad) offer a BUILDARGS method to mangle the parameter list provided to new before object creation starts. The new core OO system (like e.g. Dios) does not have this, it only allows a hash-style parameter list. So, you could keep arbitrary constructor APIs when moving from bless-style OO to Moose, but you can not achieve that with core OO. This is the main reason why I write custom constructors these days: I want to have control over my constructor API.
        • Child classes have no access to parent attributes unless there are (public) accessor methods for them. In Moose, you could always access the hash containing all fields, this is now gone. Also, Moose allowed to change a field declaration in a subclass with the has '+field' syntax, neither Object::Pad nor core OO allow this. This is sometimes an obstacle during object construction, and the discussion is still ongoing.

        How do you prevent your OO system from throwing in case a mandatory parameter is missing and convince it to return undef instead?

        How do you prevent bless() from throwing in case a mandatory parameter is missing, and convince it to return undef instead?

        my $data = undef; my $object_or_undef = bless( $data, 'My::Class' );

        (The above will throw an exception.)

        The answer is you don't even try to stop bless bless from throwing. You wrap it in sub new { ... } so you have more control over how bless is called:

        package My::Class { sub new { my ( $class, $data ) = @_; return undef unless ref $data eq 'HASH'; bless $data => $class; } } my $data = undef; my $object_or_undef = My::Class->new( $data );

        Similarly, if you don't want core OO's new to throw an exception when mandatory paramaters are missing, you wrap it in another method.

Re: Thoughts on new 'class' OO in upcoming perl
by kcott (Archbishop) on Mar 06, 2023 at 18:37 UTC

    G'day cavac,

    ++ Thanks for this meditation.

    I was looking at class a week or two ago (when I first noticed v5.37.9 had been released). I had thought that I might install v5.37.9 to check it out (with actual code tests) but decided against that for two reasons:

    • Being "experimental and very incomplete"[perlclass: TODO], it could easily change in v5.37.10 (or later).
    • A stable v5.38.0 was just a matter of weeks away and I would be installing that anyway. (I've been installing at least v5.x.0 for many years now.)
    "... i think this new system shouldn't give up any of the flexibilities ye olde 'bless' provides."

    Perhaps unintentionally, you have spoken (in various parts of your post) as if class was intended as a replacement for bless. As ++haj wrote: "bless isn't going to go away".

    Given "It [class] isn't a bless wrapper, but a completely new system built right into the perl interpreter."[perlclass: History], it would be interesting to benchmark bless-based and class-based new() functions.

    Currently, a class uses a '.../lib/Some/Class.pm' file which starts with 'package Some::Class;' (and, yes, I acknowledge that's an oversimplification). One of the things that I was interested in testing was whether simply changing 'package' to 'class' would still work in the current framework (e.g. @INC variable, parent pragma, 'SUPER' pseudoclass, and so on).

    The current documentation for class is extremely sparse. Given most of the documentation is in perlclass, the addition of a link to that should be a bare minimum. It does say that it "behaves similarly to package" but doesn't go into any detail beyond "... except that the newly-created package behaves as a class." — that strikes me as vague and terse, albeit accurate.

    — Ken

      I understand that the warnings of being experimental and very incomplete are a bit scary. I'll write a separate article about what's missing in core OO, maybe with a frequently edited head article which will get smaller and smaller over time.

      Yet, your article rises some interesting questions.

      • ...it would be interesting to benchmark bless-based and class-based new() functions.: I've tried to do this with one of my pet classes (which is not on CPAN because it is almost identical to Math::Vector::Real) in an application which creates hundreds of thousands of objects and couldn't find any significant differences. And I don't think this is too surprising. By the way: For this particular class, I consider a blessed array reference (i.e. no Moo* stuff) still to be an excellent alternative, but the current development state of core OO doesn't allow a comparison for complex classes yet.
      • One of the things that I was interested in testing was whether simply changing 'package' to 'class' would still work in the current framework...: There's quite a lot which just works after that change.
        • @INC variable: No problems here. The resolution mechanism between classes (which share their namespace with packages) to files is the same.
        • parent pragma: No. The hierarchy is defined in the class declaration like :isa(Parent::Class). Trying use parent ... fails with an error message Modification of a read-only value attempted at /home/haj/localperl/lib/5.37.10/parent.pm line 22. . The message might deserve some tuning, but the "read-only value" is actually the @ISA of your new class. Core OO populates that array, but makes it readonly for the rest of the world.
        • 'SUPER' pseudoclass: Works as usual.
      • The current documentation for class is extremely sparse. True! This is a known issue. Eventually, this chapter of perlfunc should be self-contained, with a link to perlclass. I admit that I tried to prepare a PR for that but got stuck between things which are still technically possible but ought to be forbidden (like having both a package and a class with the same name).

        G'day haj,

        ++ Thanks for looking into the points I raised and providing feedback.

        There wasn't anything that was actually "scary", at least for me. I usually try out experimental features privately when they first appear; I won't use them in production (personal, $work, or even PM code) until the experimental status is lifted. I wasn't too surprised with "very incomplete": I took class to be the first step in a staged implementation, wholly or partly, of Corinna.

        What you wrote about parent presumably also applies to base and any code that manipulates @ISA (either directly or via a module).

        That's an interesting point about package and class names. I had wondered about that but didn't list it with things I was interested in testing. Forbidding duplicate names seems appropriate; although, presumably something like this would be valid (even if it is poor design):

        class X; ... package Y; ... class X; ... extend the class here ...

        — Ken

        > (like having both a package and a class with the same name).

        Please excuse my ignorance, if it's not for speed, why else?

        What is the incentive for those parallel structures instead of building on top resp. wrapping around existing ones?

        Like a class is a special package, a new is blessing a hash, etc ...?

        Cheers Rolf
        (addicted to the 𐍀𐌴𐍂𐌻 Programming Language :)
        Wikisyntax for the Monastery

Re: Thoughts on new 'class' OO in upcoming perl
by tobyink (Canon) on Mar 06, 2023 at 14:31 UTC

    A lot of your issues can be resolved by using the :common attribute. For example:

    class Foo { field $x :param = 0; field $y :param = 0; method create :common ( %args ) { if ( $args{x} < 0 ) { return undef; } else { return $class->new( %args ); } } } my $foo = Foo->create( x => $x, y => $y ) or do_some_error_handling();

    Note that this is supported in Object::Pad but not in the first round of implementation for the class keyword (likely to be included in Perl 5.38). As Object::Pad is the testing ground for the class keyword, we can hopefully take it as an indication that :common will be supported in a forthcoming version of Perl, hopefully Perl 5.40.

      For what it's worth, I believe this will work, even without support for :common:

      class Foo { field $x :param = 0; field $y :param = 0; } sub Foo::create ( $class, %args ) { if ( $args{x} < 0 ) { return undef; } else { return $class->new( %args ); } } my $foo = Foo->create( x => $x, y => $y ) or do_some_error_handling();

      Not the nicest workaround, but also not terrible.

      Note that you can have "traditional" subroutines declared as sub create ($class,%args) in a class. The method keyword just simplifies the signature declaration because the class (for common methods) or instance(for object methods) are implied and provided as lexicals.

      :common
      Since version 0.62.

      Marks that this method is a class-common method, instead of a regular instance method. A class-common method may be invoked on class names instead of instances. Within the method body there is a lexical $class available, rather than $self. Because it is not associated with a particular object instance, a class-common method cannot see instance fields.

      Ah. You mean static. OK.

      I mean ... these things are called either static methods or class methods. ":common" ... what are they even common for? Is there actually any language in which these would be called common methods?

      Jenda
      1984 was supposed to be a warning,
      not a manual!

        Is there actually any language in which these would be called common methods?

        I agree with your point. It's just Perl being gratuitously different, yet again — like the ridiculous "invocant" vice the standard term "object" (or else "receiver"). Being gratuitously different is the main thing I hate so much about Python. Perl should be better than that. Really, stuff like this is what makes Perl look stupid. It makes it look like our best and brightest don't even know the standard terms of art. ("Invocant" is especially bad because it makes it clear that our people don't know the most basic Latin either.)

        Today's latest and greatest software contains tomorrow's zero day exploits.
Re: Thoughts on new 'class' OO in upcoming perl
by hippo (Bishop) on Mar 06, 2023 at 12:09 UTC
    But at this early stage, it seems to be only a copy of other programming languages, without the flexibility Perl can (and currently does) provide when it comes to object orientation.

    Quite so. AFAICS that is rather the point - that this will be a gateway in for those more familiar with how such other languages do it. "See, Perl now has OO, just like python/ruby/lua/go/whatever!" and when the novice hits the limits of this new system they can then be introduced to the many other ways that Perl has for working with OO.

    I don't see this new system as being aimed at experienced Perl programmers at all and there's no need for any of us to abandon what we currently use and migrate to it.


    🦛

      and when the novice hits the limits of this new system they can then be introduced to the many other ways that Perl has for working with OO

      The problem with this approach might be that people will use it, hit that limit and complain loudly about the "very limited OO support in Perl".

      If 'class' would use the keyword "constructor" for a proper constructor method AND allow it to return undef, we'd be half there i think. And 'class' could just provide a default "new()" constructor if none if given.

      Stuff like "you can have multiple constructors" would be in the advanced section of the documentation, just like "multiple inheritance", "writing factories", and so on.

      If, say, i start a new project and get a newbie programmer sidekick, i would be forced to ban 'class' at the moment and force thew newbie to use bless() anyway. Simply because 'class' is too limited in scope and i would want to avoid a mixture of both in a project. Especially because those use a very different way of doing things and switching a module from 'class' to 'bless' would mean a very major rewrite.

      Which, BTW, is the other thing that feels totally wrong to me. Not only does it seem to be a bad idea to force potential rewrites of an entire OO project on future perl users when they exceed the "novice" levels, but it looks to me like a potential PR problem as well.

      As i said, if we really add another, "more modern" way of doing OO in Perl, it should be more powerful and "perlishly flexible" than what we already got, while at the same time easier to understand. With a few small changes, 'class' could be that solution.

      I'm not a big fan of the attitude of "optimize for beginners". I started programming using BASIC (on old Commodore machines). And i must say, it was kind of frustrating hitting the limits of the language, then having to throw away most of my knowledge and learn a completely different language. Yes, BASIC was "optimized" for beginners. But had Commodore given us a C compiler instead, it wouldn't have been a much steeper learning curve and there just wouldn't have been frustrating, artificial limits imposed by the language.

      In my opinion, the ability of a language should suffice for experts, but at the basics should be easy for novices to learn. Without limiting their ability to use already learned stuff later when they start to climb their ladder to become an expert.

      PerlMonks XP is useless? Not anymore: XPD - Do more with your PerlMonks XP
      > when the novice hits the limits of this new system they can then be introduced to the many other ways that Perl has for working with OO

      Adding "Yet another incompatible OOP" system ? I'm skeptical ...

      Edit

      > "See, Perl now has OO, just like python/ruby/lua/go/whatever!"

      I dare say adding sub-signatures including named arguments to core would have a much bigger return of investment.

      Most beginners don't chose a language for their OO-system, they start fiddling around solving a minor problem and get stuck when they have fun instead of getting frustrated.

      Function signatures are a frustrating bumping stone.

      Cheers Rolf
      (addicted to the 𐍀𐌴𐍂𐌻 Programming Language :)
      Wikisyntax for the Monastery

        I dare say adding sub-signatures including named arguments to core would have a much bigger return of investment.

        Thankfully, 5.36 provided sub-sigs. Ok, it's like the James Webb Space Telescope: A couple of decades late and way over budget. But now they are operational, both the JWST and sub-signatures turned out to be rather good. At least, from my point of view.

        PerlMonks XP is useless? Not anymore: XPD - Do more with your PerlMonks XP

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others lurking in the Monastery: (1)
As of 2024-04-24 13:45 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found