Perl-Apollo / Corinna

Corinna - Bring Modern OO to the Core of Perl
Artistic License 2.0
156 stars 19 forks source link

How to declare class data and methods? #35

Closed Ovid closed 3 years ago

Ovid commented 3 years ago

Note: this is closely related to https://github.com/Ovid/Cor/issues/10

Class data and methods represent data and methods that all instances of a class share. They're widely supported in OO languages, though some developers are quite insistent that class data and methods are a terrible, horrible, no-good idea because, internal to the class, they're effectively global data and behavior. It gets much worse if the data is mutable. So we don't want that. However, Corinna reluctantly assumes that class data and methods are desired, but we've had a lot of internal disagreement about naming.

Here's some Corinna pseudo-code as an example. It's based on some real-world code I've written but cannot release. It exists in a hot path for a client and must be very performant. Further, it needs to encapsulate the data it uses because this is related to security.

class Custom::Router {
    use Carp 'croak';
    use Our::Config;

    common $config :handles(list of methods) = Our::Config->new;
    common @routes = expensive_function_to_read_and_validate_routes($config->fetch('routes'));
    common %routes = map { $_ => 1 } @routes;

    has $route :param;
    has $user  :param;

    common method handles_route($route) {
        return exists $routes{$route};
    }

    ADJUST {
        unless ($class->handles_route($route) ) {
            croak("We need real exceptions. Oh, and we don't handle $route");
        }
    }

    method is_authorized () {
        ....
    }

    # more code here
}

In the above, because this code is security related, to avoid any chance of bugs, we die in the ADJUST phaser if we cannot handle the route. We have expensive class data, so we compute it once at compile time. Externally, the code should do this:

my $router;
if ( Custom::Router->handles_route($route) ) {
    $router = Custom::Router->new( route => $route, $user->user );
}
else {
    $router = # default router
}

So there's the setup. Pretty clear, eh? Except that if someone writing code forgets to check the handles_route, our ADJUST ensures we don't allow things to move forward.

But how do we declare class data and methods? Here are the options:

Class Data

Very clear meaning, but this is objected to because it overloads the meaning of class.

This is used in Java, but in C, static isn't quite the same thing. So this was objected to.

I don't think anyone liked that, but it's very clear.

That works because the data/methods are shared across all instances of the class. Objected to because it might imply threading.

Because this class data/methods is common across all instanes. Not great. No one really likes it, but aside from aesthetics, there seems to be no serious objection.

This essentially means no code needs to be written for it as it fits current semantics. However, as you can see from our original example, we had this:

    common $config :handles(list of methods) = Our::Config->new;

That becomes:

    my $config :handles(list of methods) = Our::Config->new;

So now we have special attributes we can attach to my variables in a class and not outside of them. This might mean that if we later decide to no longer require the postfix block for Corinna, we could potentially have confusion:

class Some::Class;
my $answer :reader = 42;

# more code

package Bar;

my $thing = ...; # do the attributes no longer exist?

The syntax changes for Corinna would no longer be lexically scoped, so what does that mean for the my $thing in the package? And if future work allows us to eliminate namespaces for classes and make them first class citizens, would a "no postfix" parser handle that gracefully? I don't know.

At the end of the day, I am extremely uncomfortable with use my to declare class data because this seems to violate one of the key benefits of the principle of parsimony: avoiding decisions we can't easily walk back.

However, I don't know that using my to declare class data is a bad thing here. It does seem to fit well, even if we slap attributes on it.

Class Methods

We have similar concerns for class methods. I think what whatever keyword we use to declare class data should be used for class data, but I'm not 100% convinced.

Very clear meaning, but this is objected to because it overloads the meaning of class.

This is used in Java, but in C, static isn't quite the same thing. So this was objected to.

I don't think anyone liked that, but it's very clear.

That works because the data/methods are shared across all instances of the class. Objected to because it might imply threading.

Because this class data/methods is common across all instanes. Not great. No one really likes it, but aside from aesthetics, there seems to be no serious objection.

OK, this doesn't work at all. This suggests that this method is private, but that's not what we want. If we want a class method to be private, we should be able to declare it as private:

private common method foo () { ... }

So there you have it. We've nailed down a lot of the semantics (see here for private methods), but the syntax has been argued about endlessly. So lets hear your take on it.

preaction commented 3 years ago

What about our? Does that fall into the same semantic issues as my?

Grinnz commented 3 years ago

I actually think "class" is the only declarator that explains the distinction in both the slot and method case. But "class $foo" doesn't make much sense, it needs to be a modifier of something else, like with "class method foo"

"class has $foo" - doesn't read well but is consistent "class slot $foo" - reads well, but doesn't match the keyword for slots

I particularly object to "common method foo", because it doesn't explain anything about the distinctive property of that method (that it is a class method rather than an instance method).

Grinnz commented 3 years ago

What about our? Does that fall into the same semantic issues as my?

This would make the class data globally accessible rather than just within the class definition like other slots.

Ovid commented 3 years ago

@Grinnz I much prefer class as the declarator, but then there's the future.

It might be interesting to make named classes at runtime:

my $thingy = class $foo ...;

If that's declared in a class (for example, a factory class) is that going to be a named, runtime-generated class? I think so, but if we use class to avoid shadowing, will that be an issue?

class SomeFactory {
    class $foo = some_func();

    method something ($foo) {
        my $thing = class $foo;
        ...
    }
    ...
}

In the above, is $thing referencing the shadowed class data or is it holding on to a named, dynamic class we're about to build?

I like class, but I'm unsure if overloading the meaning won't bite us in the future.

Update: I misread Grinnz comment. We're on the same page, but I'll leave this here so people can understand the context.

Ovid commented 3 years ago

@Grinnz: you mentioned "class slot $foo - reads well, but doesn't match the keyword for slots", but we've considered renaming has to slot.

Grinnz commented 3 years ago

@Grinnz I much prefer class as the declarator, but then there's the future.

It might be interesting to make named classes at runtime:

my $thingy = class $foo ...;

If that's declared in a class (for example, a factory class) is that going to be a named, runtime-generated class? I think so, but if we use class to avoid shadowing, will that be an issue?

class SomeFactory {
    class $foo = some_func();

    method something ($foo) {
        my $thing = class $foo;
        ...
    }
    ...
}

In the above, is $thing referencing the shadowed class data or is it holding on to a named, dynamic class we're about to build?

I like class, but I'm unsure if overloading the meaning won't bite us in the future.

I agreed that "class $foo" is not suitable anyway since it implies you're declaring a class and not a slot.

alabamenhu commented 3 years ago

If you're uncomfortable using my (which makes perfect sense in Raku and its internals, but I can see why it'd be a bit dicier with Perl), I think common is perfectly cromulent.

I'm assuming here that the desire is to have it always be fully internal, and require a method-based accessor to expose it if necessary. Raku here takes advantage of how it treats our-scoping to expose or not:

class Foo {
    my $foo = 'foo'; 
    our $bar = 'bar'
};

There, $Foo::foo bombs, but $Foo::bar will return 'bar', while internal methods can reference either. Obviously, using those same keywords in Perl would be problematic because of how different our is.

That said, I think requiring it to be fully internal is perfectly fine (possibly even desirable), but if you want to leave open the possibility of exposing it later, that adds another wrinkle into the naming debate 🙂

sscaffidi-chwy commented 3 years ago

I would personally stick with static but then... I am afraid it could be conflated with the semantics of the state keyword... so maybe not (thanks rubber duckie)

The more I think about it, the more I like common and it's almost because it's somewhat imperfect. Hear me out...

If you're using common then you're doing something that people say should be avoided... but if you gotta, then you gotta. Common isn't a keyword in any other languages I can think of right now. If I saw it, I'd have to stop for a moment to think - and realize, "OK, this thing is different. I'd better keep that in mind". Also, it's significantly longer than my, our, has, etc. I tend to agree with Larry's take on "Huffman Coding" the language - to some extent. Since use of common semantics should be relatively, um... uncommon, it makes sense to have a longer keyword that sticks out a bit.

matthewpersico commented 3 years ago

I think Steven hit it on the head:

I like it.

The only thing that’s more Huffman and more exact is class common, but I don’t want , and I would venture that no one else wants either, compound keywords.

daotoad commented 3 years ago

Being lazy, I looked for antonyms of individual and synonyms of common in a thesaurus and did a little browsing from there.

The words that kept popping up were “common”, “joint”, and “mutual”.

Any of these would do.

Does it make sense to have the concept of a member type specifier on fields and methods? That is, any declaration could be specified as “instance” or “common”, with instance being the implied default.

Grinnz commented 3 years ago

The thing that bugs me about "common method" is that it is meaningless. The distinction of class methods is only that they apply to classes, not instances. This has nothing to do with being common or shared, all methods are common and shared.

HaraldJoerg commented 3 years ago

My comment is based on my own experience (code I've written or maintained), so of course, it is subjective. It should explain my bias between "convenience" and "safety" in some places.

TL;DR: I'm going to make a case for my. This article is long, I am not going to repeat my arguments in the future, if the decision goes otherwise, I'll shrug and use whatever gets the vote.

Class Data

  1. Class data are rare, but they do exist.
  2. In almost every case, the class data are initialized at some point and read-only from there on.
  3. In almost every case, the class data are not exposed to the "outside world".

So, I use class data with my. There's no difference between bless-style and Moo objects. Moo offers nothing for class data (I might have overlooked some CPAN module), and my does the trick.

The following should be equivalent:

Traditional

use 5.028;
use experimental 'signatures';
no warnings 'experimental::signatures';

package Wossname {
    my $private = "Class data";

    my $readable = "Visible from " . __PACKAGE__;
    sub readable($class) { $readable };

    my $writable = "Writable";
    sub set_writable ($class,$new) { $writable = $new };

    my $config = Our::Config->new;
    sub foo { shift; $config->foo(@_) }
    sub bar { shift; $config->bar(@_) }
    sub baz { shift; $config->bar(@_) }

    my sub private ($self) { "You rang?" };
};

Corinna:

class Wossname {
    common $private = "Class data";

    common $readable :reader
    = "Visible from " . $class; # does this work?

    common $writable :writer = "Writable";

    common $config :handles(foo bar baz) = Our::Config->new;

    private method private { "You rang?" };
};

Obviously Corinna adds convenience. It also reduces errors for class data, extending what Moo* does for instance data by replacing code with declarations. Less code to write, less tests to write, more productivity, better readabiility, easier to maintain. However: Innocent with regard to the parsimony principle: Drop the common keyword from the MVP and decide later whether you want to add common, use some other keyword, or to extend my so that it accepts the colonized attributes.

:handles is the only attribute which takes a list, so writing this without Corinna can get cumbersome. I'd say it is rarely used, but this might be a wrong assumption since it is explicitly included in the example code. Moo* doesn't have that problem for instance attributes because everything is runtime: handles takes a Perl expression which evaluates to an array reference. But then, I know of a CPAN module which creates dozens of methods on the fly from a list of strings by adding them to the symbol table. That looks ugly, but once thoroughly tested, is rather easy to maintain.

I have hallucinated one use case of class data which can not be implemented with my, but it is not yet specified whether Corinna behaves like that: If class data are defined in a role and class data are initialized at role composition time (which is compile time of the consuming class but not compile time of the role), then my can't help. I have no idea whether this is relevant. Other OO frameworks don't help either, so programmers needed to work around this anyway if they wanted that behavior.

Class Methods

Since new is inevitably a class method, class methods do exist. While new itself is autogenerated from declarations (as it is in Moo*), alternate constructors are not. With no BUILDARGS nor coercions, alternate constructors are the recommended way to allow nonstandard constructor params (as they happen a lot in old applications) in Corinna. Many old classes follow old recipes which allowed to create new objects either as class methods or as instance method, I can't say how relevant this is.

The difference between a class method and an object method is that the first parameter (or invocant) is either a string or an object. I expect Corinna to croak when a class method is called with an object and also when an instance method is called with a string. Noteworthy: Unless with Moo*, $self (resp. $class) is only required if a method calls other instance (or class) methods. Access to slot data in Corinna doesn't need it!

A private common method does not give any advantage over today's Perl code:

my sub private {
    my $class = __PACKAGE__;
    ...;
}

So in my opinion there's not much gain in the combination of keywords private common method. I have also yet to see why private method would be any different from my method.

The Name Of The Game

I guess that every english word has some different meaning in at least one other programming language (COMMON is a keyword in FORTRAN). But still I'd like to make a point in favor of not using common: I am slightly sceptical about using the same keyword for class data and class methods in their current understanding. Class data are private per default, they need a :reader to be accessible, whereas class methods are public per default, they need a private keyword otherwise. In old-style Perl, data and subs without any keyword are public (no strict implied here), and both can be restricted in scope to the surrounding block or file by the same keyword my. Current practice is to make data lexical with my, but lexical subroutines aren't used much (it makes testing a pain, for a start). So, my covers a lot of ground: Perl programmers are familiar with its use, new programmers need to learn my anyway. The fact that my leaks across namespaces within a file scope is not new. If you feel you must have different namespaces in one file, the block syntax is recommended anyway. If you have just one package (or class) in a file, which is almost "the normal situation", then the syntax without block gains one indentation length of space on every line. Also, there's no need to ponder when to use common and when to use my (I consider it impossible to remove my within a class) and why the previous maintainer might have chosen whatever he used. With regard to huffmanization "make frequent patterns short", my $class_data; beats common $class_data. Access to class data from the outside is the exception, not the rule. So my makes simple things easy and hard things possible.

What is left is one new keyword to differentiate between class and instance methods if you want Corinna to throw up early when a method is called in the wrong way. When an instance method is called with a class name as invocant, it will throw up when an instance slot is used. I doubt whether a keyword is to distinguish between the methods is good enough to detect bad calls at compile time. So, for the name of that keyword: Throw dice, it doesn't matter for me.

Grinnz commented 3 years ago

What is left is one new keyword to differentiate between class and instance methods if you want Corinna to throw up early when a method is called in the wrong way. When an instance method is called with a class name as invocant, it will throw up when an instance slot is used. I doubt whether a keyword is to distinguish between the methods is good enough to detect bad calls at compile time. So, for the name of that keyword: Throw dice, it doesn't matter for me.

I agree substantially with this whole comment and just want to add: Class methods actually cannot access slots, since they have no instance to retrieve the slots from when called. So this is another reason why they need some keyword to differentiate them from instance methods.

duncand commented 3 years ago

Your first sentence Class data and methods represent data and methods that all instances of a class declare. seems to have a typo. Shouldn't the last word be share rather than declare?

duncand commented 3 years ago

Class data and methods represent data and methods that all instances of a class declare. They're widely supported in OO languages, though some developers are quite insistent that class data and methods are a terrible, horrible, no-good idea because, internal to the class, they're effectively global data and behavior. It gets much worse if the data is mutable. So we don't want that. However, Corinna reluctantly assumes that class data and methods are desired, but we've had a lot of internal disagreement about naming.

@Ovid Practically speaking, there is a very simple alternate design which can get all the benefits of static class stuff without any of the downsides.

The answer is that Corinna does NOT itself have any concept of static anything.

Instead, when users want to design a class X whose instances all share some slots in common, they employ a second class Y to declare those slots, then instantiate 1 instance of Y, and use that same Y object as the value of a slot in every X object.

Usually this would be combined with some kind of factory pattern, they instantiate a Y object first and then use factory methods on it to create every X instance, and behind the scenes that X instance takes a constructor parameter that is the Y object.

Look no further than the Perl DBI for this very pattern in action; you instantiate connection handles first, the Y object, and use factory methods on those to make the statement handles X.

I also use that pattern in my own project, a Y object is instantiated first representing a virtual machine, and it is a factory for X objects representing values or routines or other things in that virtual machine.

This design also leaves users free to have multiple completely independent sets of X instances that share within the set and not within other sets.

The way I figure it, it is an exceedingly rare situation where one needs a true singleton Y over the whole program guaranteed, and in that rare situation, they can use an actual Perl global variable for Y that they make sure to define once.

So if this design pattern which is good anyway is adopted, Corinna can completely skip all the static stuff as features, and just make everything instance slots/methods, the end.

What do you think of that?

duncand commented 3 years ago

The way I figure it, my proposal, eschewing actual static stuff and using shared objects instead by instances, is so compelling that it should be considered as a default solution, and then people who think that is not good enough have to justify what value one actually gains with true global 'static' stuff that is worth all the downsides of such.

In my experience, basically all the advanced application frameworks eg doing automatic constructor injection and stuff, they expect any design that conceptually has singletons is still using an object instance to represent the singletons, eg an actual object representing the "shared global" application state.

A lot of advanced functionality requires actual object instances to work, and doesn't work with true static things.

Again, I challenge people to justify the need for true static stuff that isn't better accomplished using only object instances, even classes you just have a single instance of.

duncand commented 3 years ago

A further thing in favour of my proposal is it involves simply leaving features out of the Corinna MVP. Similar to a lot of other things we're leaving out, we could still be free to add true static/common stuff later, but in the meantime we launch without them, which I propose will still do everything we need, but much more simply. So simplicity also is a key benefit of my proposal while still being possible to change later. Whereas if the MVP has static/common stuff, it is more complex out of the gate, and harder to walk back.

Ovid commented 3 years ago

@duncand: thank you for the great thought you've put into this! I really appreciate it. However, I have to disagree :/

Instead, when users want to design a class X whose instances all share some slots in common, they employ a second class Y to declare those slots, then instantiate 1 instance of Y, and use that same Y object as the value of a slot in every X object.

Usually this would be combined with some kind of factory pattern, they instantiate a Y object first and then use factory methods on it to create every X instance, and behind the scenes that X instance takes a constructor parameter that is the Y object.

It's entirely possible we'll go this route. Moo/se has done without native class data for a long time. That they're available as Moo/seX modules still shows that some people have a desire for them.

If you look at my original Custom::Router example (which is a real-world example modified and simplified for here), I need:

  1. The data to be read quickly (hot path)
  2. Only once (hot path)
  3. Encapsulated (this is security-related code)

Your example can hit points one and two, but not point 3 because Corinna does not have a concept of "trusts" and thus, you couldn't cleanly encapsulate the data.

Second, I don't need a factory pattern for a single class. I have one class, currently instantiated in two places in the code (many more if you count the tests) and there's a good chance that it's going to take over routing duties in other areas of the code. Thus, in multiple classes handling different portions of the code base, this custom router would need to be instantiated.

In your scheme, I would then need to have a secondary object, practically a global, that would need to be passed around everywhere I need to instantiate my Custom::Router. And worse, it's now a second class that's tightly coupled to my actual class and if I discover I need to make changes, I now have two classes I need to change and not just one. Or I could just use class data and encapsulate both data and behavior.

But as a digression, I want to consider why you would want to declare that second class. What you're describing is a design pattern. We use design patterns to work around structural limitations of programming languages so that we developers can focus on the business of getting stuff done. Wouldn't it be rubbish in Perl if we couldn't natively iterate over lists? Or what about linked lists? We usually don't need them in Perl because Perl arrays are so dead easy to work with, growing as needed and having useful functions like splice to manipulate them. If we need linked lists in Perl, we can use them if we need to, but the language should not force us to use them as the default.

Much of the appeal of Moo/se and Corinna is that they remove so much of the structural grunt work and let us focus on getting stuff done. In Damian Conway's thoughts on Corinna, he gives great examples of that.

So yeah, maybe we will punt on class data for the MVP (we can't skip class methods). We don't need class data, but it's like using goto, you usually don't want it until you do. Then it can take a messy situation and make it clean again.

Ovid commented 3 years ago

@HaraldJoerg One interesting comment I want to make:

Class data are private per default, they need a :reader to be accessible, whereas class methods are public per default, they need a private keyword otherwise.

One thing which was suggested early on, which I kinda regret not adopting, was that all methods are private by default and you'd have to use public to make them public. Thus, everything in the class, data and methods, are private. That would be excellent for achieving encapsulation. I didn't go that route because ...

There were loud squawks when I brought this up. "Having to type more for the common case is rubbish!", or things to that effect. Never mind that the common case is the common case because it's simply easier and more natural to write than this:

my $frobnicator = sub {...};

sub whizzit {
    my $self = shift;
    if ( $self->$frobnicator ) {
        ...
    }
}

That's great encapsulation, but it's harder to write and the order of declaration matters—don't put whizzit above the $frobnicator declaration. (Plus, a lot of my personal tooling is less useful when subs aren't, well, subs).

If Corinna opted to be private by default, how do you share these "private" methods between classes, roles, subclasses, etc? We'd need the "trusts" which Darren advocates. I think those are a great idea (roles could require "trusted" methods and the class could still encapsulate its logic), but we didn't get there. And now methods are public by default.

I hope I didn't make a mistake in doing that.

duncand commented 3 years ago

I hope I didn't make a mistake in doing that.

Nothing's stopping you from changing your mind. Until a production Perl ships with a Corinna implementation, anything could be changed with justification.

duncand commented 3 years ago

@Ovid Thank you for your quick and detailed response.

It's entirely possible we'll go this route. Moo/se has done without native class data for a long time. That they're available as Moo/seX modules still shows that some people have a desire for them.

They may have a desire but it could be due to being stuck in their ways rather than changing to something better. A question is how commonly used those modules are and how important is it to support that use case.

If you look at my original Custom::Router example (which is a real-world example modified and simplified for here), I need:

  1. The data to be read quickly (hot path)
  2. Only once (hot path)
  3. Encapsulated (this is security-related code)

Your example can hit points one and two, but not point 3 because Corinna does not have a concept of "trusts" and thus, you couldn't cleanly encapsulate the data.

About point 3 I say:

  1. If we agree about leaving class slots out of the MVP, then by the time you get to version 2 you could have "trusts" in version 2 instead of adding class slots. Either way, leaving it for version 2 gives more time for deciding on a good solution for the use case.

  2. Using 2 classes is NOT meaningfully breaking encapsulation or being a security problem. Practically speaking your own source code has no meaningful security against itself. You can have features which discourage insecure practices, but anyone wanting to violate them anyway has access to the source code and can change whatever is stopping them. Security only means anything against people without physical access to your machine or otherwise that don't have control over your application process. Once they're inside your source code you can just be advisory not absolute.

Second, I don't need a factory pattern for a single class. I have one class, currently instantiated in two places in the code (many more if you count the tests) and there's a good chance that it's going to take over routing duties in other areas of the code. Thus, in multiple classes handling different portions of the code base, this custom router would need to be instantiated.

In your scheme, I would then need to have a secondary object, practically a global, that would need to be passed around everywhere I need to instantiate my Custom::Router. And worse, it's now a second class that's tightly coupled to my actual class and if I discover I need to make changes, I now have two classes I need to change and not just one. Or I could just use class data and encapsulate both data and behavior.

I don't consider this tight coupling because it is conceptually a single feature that just happens to be implemented in 2 classes.

Do you worry about tight coupling between DBI connection handles and statement handles? Probably not.

This is a case where 2 classes are SUPPOSED to share duties.

Tight coupling being a problem is a concept when things that are supposed to be arms length or unrelated know too much about each others' internals. That isn't the case in my proposal.

The 2 classes live together and not far apart in the system, one is effectively maintaining a single thing in 2 pieces. You could even declare them both in the same Perl source file if you choose.

But as a digression, I want to consider why you would want to declare that second class. What you're describing is a design pattern. We use design patterns to work around structural limitations of programming languages so that we developers can focus on the business of getting stuff done. Wouldn't it be rubbish in Perl if we couldn't natively iterate over lists? Or what about linked lists? We usually don't need them in Perl because Perl arrays are so dead easy to work with, growing as needed and having useful functions like splice to manipulate them. If we need linked lists in Perl, we can use them if we need to, but the language should not force us to use them as the default.

Your analogy is apples and oranges with this situation.

You presume that having the shared state inside a common class with non-shared state is somehow a better design just because it involves a single class rather than 2.

I'm arguing that using 2 classes is actually a cleaner design that provides greater composability and reusability and cuts down on redundancy. You don't have to have separate class and instance versions of the same functionality.

Using 2 classes is not like having to use linked lists versus static slots being like not having to use linked lists.

Much of the appeal of Moo/se and Corinna is that they remove so much of the structural grunt work and let us focus on getting stuff done. In Damian Conway's thoughts on Corinna, he gives great examples of that.

Having read Damian's post after I wrote here, I didn't see anything in his example code that pointed to static slots being better. If anything, Damian's post exemplified why my shared object approach is better.

Most of the time, if people conceive they just need 1 of something, sooner or later they will find, actually they want more than one of those. They want to take the whole system they have and have more than one at a time.

Seeing Damian's example, maybe you decide you want to represent 2 independent banks with their own account id pools, the account ids only need to be unique within an individual banks but 2 different ones can have their own.

Or even if you only have one bank, the 'static' details for the Account actually aren't about the Account at all, they are about the Bank entity the set of Account relate to.

Consider if you were to have a relational database backing your classes/instances. You wouldn't store the static slot data in the same database relation/table as your instance ones would you? They clearly are 2 separate though related relations/tables. You would have a Bank tuple/record and multiple Account tuple/records related to it.

So yeah, maybe we will punt on class data for the MVP (we can't skip class methods). We don't need class data, but it's like using goto, you usually don't want it until you do. Then it can take a messy situation and make it clean again.

Like with goto you can make it possible to do something without making it too easy so people are discouraged from bad practices. I'm saying actual static slots are a bad practice even if supported, because their use prevents a whole bunch of things.

An analogy for static slots is like making regular Perl subs that are always invoked via the package name or have to be imported versus invoking them off of objects.

Ovid commented 3 years ago

Some emailed comments from Damian on using my or state for class data (shared with permission):

The optimal keyword linguistically is almost certainly shared, but I understand why that might not be feasible, given its previous thread-related connotations within the language. Failing that, I suspect common is the least worst alternative. But reusing my and/or state by permitting :reader or :writer attributes on them would, in my opinion, be a huge mistake.

Psychologically, it's a mistake because it's a repeat of the now-acknowledged error of overloading the purpose of a keyword.

Just as it was a mistake to overload package to mean class, or sub to mean method.

We're finally fixing those, so it would be folly to introduce another overloading: my to mean common (or whatever keyword is chosen).

The package/class and sub/method conflations already make it vastly harder to build automated code analysis, manipulation, refactoring, and translation tools. Giving my two different meanings and two behaviours in two different contexts will only make that problem far worse (given the much higher frequency of my compared to package or sub).

Apart from that, you now have a situation where there is a new set of valid attributes applicable to my variables, but only in certain contexts. Which is inelegant and confusing and makes Perl OO harder to teach.

Oh, and reusing my for class-level slots doesn't solve the problem of how to specify class-level methods, because you can't use my for that, as it already has a different meaning (effectively private) when applied to subroutine-like constructs.

So you still need a new keyword for class-level methods, and you might as well be consistent and use that for class-level slots as well.

duncand commented 3 years ago

To be brief, a key benefit of making basically all methods instance methods rather than static methods (you only have static methods where technically required like constructors) is that in practice invoking them is much more terse.

Real life class names in non-example projects tend to be multiple package levels deep like Foo::Bar::Bar::Math and not just Math like example code.

So imagine having to write out that whole class name everywhere you want to create a new object of it, or alternately every time you want to invoke a static method of it. Presumably the methods are static because you want to call it without having an object to call it on.

But if you're writing a lot, what would you rather do?

Have one $math = Foo::Bar::Bar::Math->new() and then have lots of $math->plus($x,$y)?

Or would you instead rather write a lot of Foo::Bar::Bar::Math->plus($x,$y)?

That may not be the best example but pretend you wanted to do all your code in Corinna classes and you didn't want to use Perl importing and pollute namespaces and not have $math just be the class name.

By similar logic, using factory classes rather than not can frequently cut down on the verbosity of your code.

For example, one $maker = Foo::Bar::Bar::Maker->new() and a bunch of $maker->thing() rather than lots of Foo::Bar::Bar::Thing->new().

And then you can get extras like $maker providing shared state to all the $thing without anything beyond the $maker->thing() you're already writing, in which case from the user code point of view having static slots in $thing rather than non-static slots in $maker doesn't make the code any more complex, and you have the option to have more than one $maker or do some other advanced things.

duncand commented 3 years ago

I should say this too...

I recognize that providing static slots doesn't in any way prevent one from using the design patterns I recommend, so in that sense I'm not opposed to statics as they don't prevent what I see as doing it right.

However I do see front-loading static slots as potentially doing a lot of extra work up front for Corinna version 1 that could be put off for version 2, and having it might encourage bad habits.

So I concede putting in static isn't bad per se but that often people shouldn't be using it.

So as having static slots doesn't get in my way I shouldn't oppose them too strongly, and I can mainly argue that they aren't really necessary to have in version 1 so if providing them is non-trivial it may be better to save the effort for more important things and come back to it later.

duncand commented 3 years ago

@Ovid I realized I didn't address one or more of your points as well as I could have, here's another go.

If you look at my original Custom::Router example (which is a real-world example modified and simplified for here), I need:

  1. The data to be read quickly (hot path)
  2. Only once (hot path)
  3. Encapsulated (this is security-related code)

Your example can hit points one and two, but not point 3 because Corinna does not have a concept of "trusts" and thus, you couldn't cleanly encapsulate the data.

So I assume the issue here with point 3 is that you're talking about a shared secret.

You're concerned that using my 2-class method X and Y, the class containing the shared slots Y is exposed to the user because they created that object first and its slots must be public in order for the class X to access them, and those shared slots are sensitive so the public isn't also allowed to see them.

To that I say an effective solution is to add a third class Z. Then Z is used as an inner class, having the sensitive shared slots. The Y class and the X class each have an instance slot of Z, those slots do NOT have :reader or :writer. The Y constructor instantiates a new Z object and initializes/sets its sensitive slot values. The Y class is a factory class for X and the method of Y to create an X instance passes the Y object's Z instance as an argument to the X object's constructor which then assigns it to its own Z instance slot.

Thus you have a set of X instances sharing common slots and the public is blocked from them, you have the clean encapsulation and security.

This is also a workaround of sorts for when Corinna doesn't natively support "trusts" you can sort of fake that shared private secret using the method I described.

But this does better than plain "trusts" or plain static slots in that you can partition which sets of X objects share a secret, where plain static slots require all the objects to share the secret, so what I proposed is even BETTER security.

More broadly speaking, you can use this kind of design to effectively layer a multi-class "thing" into an outer public API shell and various inner implementing classes that can see each other's privates but users of the "thing" as a whole can not, they are secured away. The "trusts" can do the same thing, though not partitioning sets from each other.

As an example of the latter that I'm actually working on, I could have a Machine class for a virtual machine and is a factory producing Value objects, these 2 together being the public API of the virtual machine; there is a third or several inner classes including Memory which provides a cache of Value, but the Memory object is a shared secret and external users can't access it directly.

duncand commented 3 years ago

And another practical example of why you may want to have a shared secret that is not static? DBI again. Lets say you have multiple $dbh connection handle objects, each with multiple $sth statement handle objects. There would be shared secrets related to internal workings of DBI that are common to all $sth sharing a $dbh but that are NOT shared with $sth on a different $dbh. You can not implement those as static slots of $sth. My 3-class method, a third being say $inner, described in my previous post, would solve this. You only need instance slots in Corinna to implement this, and static slots can't handle the requirements. And its secure and encapsulated.

duncand commented 3 years ago

Following up my prior 2 comments...

Actually you can get the secure encapsulation I mention without static slots and also without any extra classes beyond X if each of your shared slots is a Perl reference type and you have a constructor parameter in X for each one that is optional.

In that case, you instantiate your first X object of the set without supplying those constructor parameters, and the X constructor will initialize the shared reference slots with new references to new whatever they are.

And X has an instance method which is a factory method for other X objects, you use that to make all the X objects of the common shared secret set after the first one, and this one will pass the existing shared reference values to the constructor so the newer X instances use those rather than making their own.

So I have shown you can get the shared secret in an encapsulated secure way without having static class slots, and its more secure than the static method because X objects can be in partitioned groups.

Ovid commented 3 years ago

Everyone, let's try to keep this back on track.

We have class methods and can't avoid them. At the very least, internally, new is a class method. Externally, alternate constructors should be used in place of BUILDARGS and friends.

So we can't skip class methods. However, that means anyone slapping a my variable at the top of the class declaration automatically gets class data. There's no mechanism to skip that.

So let us please avoid the distraction of other ways of approaching the problem.

seav commented 3 years ago

Whatever keyword is decided, I wonder if it makes sense to make this like an attribute of the slot or method similar to :param, :reader, and :writer?

Or are class/static/common slots/methods important enough to highlight that they shouldn't be attributes?

Grinnz commented 3 years ago

method foo :class seems as reasonable as class method foo to me. No opinion on syntax for class slots.

abraxxa commented 3 years ago

Don‘t forget that there are use-cases for additional constructors beside the autogenerated ‚new‘, for example ->from_string (thinking about datetimes and IP addresses).

Ovid commented 3 years ago

TL;DR: We're going with common $x and common method foo () {...} for declaring class data and methods.

There's been a lot of debate about this and there are no great answers. Here's what it comes down to.

It's been pointed out that all attributes in Corinna add behaviors, but :class would changing behavior (or some might argue "restricting" behavior). Thus, using attributes in this way might be a bit odd—not to mention that this seems fundamentally more important than most attributes, suggesting new keyword might be appropriate, putting in in front of the thing thing it modifies.

Second, there's been a lot of dispute about whether class data and class methods should have the same initial declarator. Given that we have modified Corinna heavily on the grounds that different things should look different, it doesn't seem unreasonable to argue that similar things should look similar. So I'm going to go with "we want the same keyword for both," but what should that keyword be?

We don't want to overload the meaning of class (class $x and class method foo () { ... }), especially since if can later get to inner classes, declaring a "shared" inner-class as class class MyInner::Class {...} seems silly and confusing. shared is great, but implies threads to many people. Java uses static but that was almost universally shot down as a bad idea.

There's the question of using my and state variables. Both could be subtly different in this context and I think state would be more accurate, but in either case, suddenly adding attributes to my and state variables means we'd need a different keyword for declaring shared methods, but we'd also have to teach new programmers to memorize when they can and cannot attach attributes to these keywords.

So we're going with common. Amongst those who've agreed with it, the response has generally been "meh." No one is happy about it, but it seems to be the least bad option.