Perl-Apollo / Corinna

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

Attribute feedback #4

Closed Ovid closed 3 years ago

Ovid commented 4 years ago

Use this ticket to leave feedback on the Cor attributes proposal

autarch commented 4 years ago

One question I had on reading this latest draft is whether there have been any thoughts on MOP extensions, particularly extensions that change the behavior or roles or classes.

IMO, one of the best things about Moose is that the entirety of its behavior in terms of code generation is written in Perl and can be extended (and the worst part is that writing these extensions requires deep digging into the Moose internals and much black magic). This has allowed for a rich ecosystem of MooseX extension distributions on CPAN, and it's freed up the Moose core from having to be everything for everyone.

Without this sort of system, I'd expect this new core OO to constantly come up short for many people's needs.

Here's some suggestions for things that I think this new system should support somehow:

My big concern is that this will be implemented in a way that either makes extensions impossible or requires you to write XS code to create an extension. This should be part of the spec. The MOP distro on CPAN is not at all sufficient for this, since it's just a read-only view of your class.

I would note that at least for Getopt and Clone, all you need is a way to add arbitrary metadata to slots, but with Moose you also get typo detection, so if you write traits => ['Getopts'] you will get an error telling you that no such metaclass exists. You still don't get typo detection on the additional metadata key names, so you can write cmd_flags and this will be silently ignored.

Ovid commented 4 years ago

@autarch Yes, it will be extensible. The combination of the MOP and extensibility via attributes should help with much of that.

However, we need to get the core prototype one before we dig into that too much.

autarch commented 4 years ago

@Ovid I'm not sure how MOP helps here. It doesn't do any codegen, nor does it provide any way to modify the class via its methods. It's just read-only meta-info.

Also, I really hope that we don't use Perl's attributes (:foo) unless some work is put into making those nicer too. Last I tried to use them, the parser had a lot of issues, like not allowing the parenthesized portion of an attribute (:foo(stuff in here)) span multiple lines. There's also the fact that the contents of the parenthesized block are not parsed as Perl, so if you have :foo( size => 42 ) you just get " size => 42 " to deal with in your attribute handler.

There's the Attribute::Handlers CPAN module to make this less horrible, so maybe that could be built into this approach, as it's part of the core.

cxw42 commented 4 years ago

@autarch Since Cor will be implemented in C, as far as I know we could update perly.y to define these attributes as taking a full expression as a parameter. That would take care of the parsing. It would certainly be nice to expose those in an MOP, but if the defined attributes are those consumed by Cor itself, that might not be as much of an issue.

cxw42 commented 4 years ago

:new(...) reads strangely to me. For example, has $x :new could be read as "automatically $x->new" just as well as "$x: pass a value to new()". I though the draft syntax from one of your earlier comments (and at the top of Deconstructing Constructors) was easier for me to understand. (Of course, others may disagree!)

I think that :req/:required, :opt/:optional, and :priv/:private are reasonable forms. The first two have Huffman merit over :new(something).

I see in Deconstructing Constructors that you are "trying to make it hard for developers to write invalid OO code for the attributes." I looked back through the gist discussion and didn't see any details on this point. Could you give an example in which :new(required) is preferable to letting people say :req :opt and get a compile-time error? I am having a hard time seeing it :) .

Ovid commented 4 years ago

@autarch Stevan has already assured me that we'll have extension mechanisms in place. However, he's directing his efforts towards the prototype first.

Ovid commented 4 years ago

@cxw42 wrote:

Could you give an example in which :new(required) is preferable to letting people say :req :opt and get a compile-time error? I am having a hard time seeing it :) .

Sure. For :new(required), we merely check to see if the value in the parenthesis is allowed or not (makes syntax highlighting easier, too). By breaking that out into separate attributes for :required, :optional, and :forbidden , we have to check all illegal combinations. Further, it won't always be clear to a new developer that those are not composable, but there are zero composability issues with :new.

Since I've gone down the rabbit hole of trying to ensure maximum composibility of attributes, I don't want to start picking and choosing which attributes the "composibility" rules do and do not apply to.

That being said, I've never liked the name :new, so suggestions for a better name are welcome.

HaraldJoerg commented 4 years ago

Will there be some coercion mechanism for an attribute? During construction I could achieve the same using CONSTRUCT - but what if I have a read/write attribute?

An example shows a safety issue:

class Coordinate {
    has ($x, $y, $z) :new(required) :reader :isa(Num);
}

class Sphere {
    has $radius :new(required) :reader :isa(Num);
    has $center :new(required) :reader :isa(InstanceOf['Coordinate']) 
}

# This is safe
my $sphere1 = Sphere->new(radius => 1,
                          center => Coordinate->new(x => 0, y => 0, z => 0));

# This isn't
class MyCoordinate isa Coordinate {
    has $x :new(required) :reader :writer(set_x) :isa(Num);
}

my $center = MyCoordinate->new(x => 0, y => 0, z => 0);
my $sphere2 = Sphere->new(radius => 1,
                          center => $center);
$center->set_x(1); # Oops: $sphere just moved

With Moose, I'd write a well-hidden class ImmutableCoordinate and then coerce Coordinate objects into that class by cloning them, making sure that the sphere's center does not move. This is a bit ugly, but can be extended so that I can also pass an array reference for the center coordinates.

How would I declare this with Cor?

Ovid commented 4 years ago

@HaraldJoerg We currently do not have plans for coercion for the attributes. This may come later. The reason we're trying to avoid adding too many extra features is:

  1. Easier to get buy-in from P5P when presented.
  2. We're currently striving for an MVP (minimum viable product)

The latter point is important and is explained in the "Goals" section of the rationale.

I suspect that later we will be adding coercions after we have all of the moving bits settling into place and if P5P accepts the proposal.

haarg commented 4 years ago

To me, has $foo :new(forbidden); would imply different constructor behavior than not having the attribute at all, but that doesn't seem to be the case.

Non-lazy defaults prevent roles from being fully commutative.

While lazy attributes are certainly useful, I really hate the idea of a variable that runs code on read.

haarg commented 4 years ago

This doesn't seem to provide a way for constructor arguments to be named differently from the variables they will be stored in.

How would duplicate/conflicting attributes be handled by the constructor? If attributes with the same name are used in a parent and child class, or in a class and a role?

haarg commented 4 years ago

It's probably worth clarifying the scope of attributes. If you declare:

class Foo {
    {
        has $foo;
        method foo1 { $foo }
    }
    {
        has $foo;
        method foo2 { $foo }
    }
}

Are the two $foo variables distinct?

Is there any way to have share a slot between parent and child classes aside from using their accessors?

iamalnewkirk commented 4 years ago

I'd like to see (and read) a brief on the why behind "slots". My main takeaway after browsing https://metacpan.org/pod/slots was %HAS and reflection/introspection.

Ovid commented 4 years ago

@iamalnewkirk "slots" is simply another word for instance data. This declares a slot stored in the variable $x:

has $x;

All that does is declare the variable; nothing else. There are no side effects.

The attributes provide additional behavior, like setting the slot via the constructor (:new), providing builders (:builder), readers and writers (:reader, :writer), and so on.

In short, we're decoupling the data from the interface.

Ovid commented 4 years ago

@haarg That's a good question about scope. I'm not sure of the answer yet.

As for sharing a slot between parents and children, no, we deliberately don't do that. The child has to use the accessors like everyone else. This minimizes the chance that a poorly implemented child can violate the parent's contract.

Ovid commented 4 years ago

@haarg:

This doesn't seem to provide a way for constructor arguments to be named differently from the variables they will be stored in.

This is a v2 feature, if it's needed at all. Generally, for Moose-like init_arg behavior, I find that we have two common cases:

  1. init_arg => undef (prevents it from being set via constructor).

We don't need that because we simply omit the :new attribute.

  1. has _name => (..., init_arg => 'name')

This is to "fake" private data you can pass via the constructor, but Moo/se still allows someone to call $object->_name, thus violating encapsulation. For Cor, we simply omit a :reader attribute and we're good to go.

How would duplicate/conflicting attributes be handled by the constructor? If attributes with the same name are used in a parent and child class, or in a class and a role?

The slots are per class and not available in subclasses. However, the attributes would need to be handled via the CONSTRUCT method. I'm less sure about this, but I could see this (the following assumes that $x is not shared between parent and child):

class Parent {
    has $x :new; # required in constructor
}

class Child isa Parent {
    has $x :new;

    method CONSTRUCT (%arg_for) {
        if (defined $arg_for{x}) {
            $x = $arg_for{x};
            $arg_for{x} = ...; # child decides what parent receives
        }
        return \%arg_for;
    }
}

This is a problem with any OO language and not unique to Cor. The solution, of course, will likely be unique to Cor.

I could very well be dead wrong about this.

(And per other discussions in the #cor IRC channel, I agree that many CONSTRUCT should be similar to BEGIN, END, etc., and not require/use a method keyword.)

Ovid commented 4 years ago

@HaraldJoerg regarding this and the discussion of coercions:

# This isn't
class MyCoordinate isa Coordinate {
    has $x :new(required) :reader :writer(set_x) :isa(Num);
}

my $center = MyCoordinate->new(x => 0, y => 0, z => 0);
my $sphere2 = Sphere->new(radius => 1,
                          center => $center);
$center->set_x(1); # Oops: $sphere just moved

I've realized (again) that I was thinking Perl instead of OO in general. Every OO language shares these traps, so we need to find a way to minimize those traps. In this case, the above is simply bad OO design and that's part of why we're striving to encourage immutability as the default behavior. Drop the :writer(set_x) and you're fine. Instead, the sphere should control its movement, possibly by allowing it to internally have a mutable center, or simply creating new MyCoordinate objects with updated data.

haarg commented 4 years ago

The slots are per class and not available in subclasses. However, the attributes would need to be handled via the CONSTRUCT method.

And what would happen if the child class didn't try to handle it via CONSTRUCT? And what would roles be expected to do?

Actually, possibly a better question is how is the default construction (the NEW method) going to handles roles at all?

HaraldJoerg commented 4 years ago

@Ovid:

Every OO language shares these traps, so we need to find a way to minimize those traps. In this case, the above is simply bad OO design and that's part of why we're striving to encourage immutability as the default behavior. Drop the :writer(set_x) and you're fine.

I'm OK with not having coercions: I fell into the "how I might solve that with Moo*" trap when I wrote that. However, as the author of the Sphere class you don't always have the option to drop the writer in your caller's code, and it doesn't really help to blame them for bad OO design. If you want an immutable Sphere class (or rather spheres where the class controls their movement), you need to take care for that yourself, and Cor's answer to that would be to CONSTRUCT safe attributes.

One difference between Moo* / Raku / Cor and OO languages like Java and C++ is that the latter force you to write down constructor methods if you want to pass parameters to new objects. The parameter list for the constructor methods is completely decoupled from the object's attribute list, and in the constructor methods you need to take care for your object's safety. In Cor et al., the framework creates constructor methods, and it offers CONSTRUCT to close the gap between the claim we're decoupling the data from the interface and the restriction you cannot declare constructor arguments unless they have a corresponding slot. Coercion is just a convenient and declarative shortcut for the maybe not-so-rare special case where you have a 1:1 relation between a constructor parameter and an object atttribute which allows you to get away without writing code to CONSTRUCT.

Ovid commented 4 years ago

@HaraldJoerg: It sounds like we're mostly on the same page and yes, maybe coercions might come in with a later release (as you point out, CONSTRUCT means we don't need it for the first pass. However, there's one tiny point I want to clarify:

One difference between Moo* / Raku / Cor and OO languages like Java and C++ is that the latter force you to write down constructor methods if you want to pass parameters to new objects. The parameter list for the constructor methods is completely decoupled from the object's attribute list, and in the constructor methods you need to take care for your object's safety.

I know it can seem like that, but once you look at Cor for what it is, instead of in terms of other OO languages/modules, it's not really different. (except that in Cor, like all good Perl code, "forced to" does not apply). Consider the following:

class Point {
    has ($x, $y) :reader :isa(PositiveNum);

    method CONSTRUCT (%arg_for) {
        $x = delete $arg_for{x};
        $y = delete $arg_for{y};
        # extra args? Handle 'em here
    }

    method to_string () {
        return "[$x, $y]";
    }
}

Let's look at the Java version:

public class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    @Override
    public String toString() {
        return ("[" + x + "," + y + "]"); 
    }
}

In Java, you're forced to manually wire together positional arguments (hope the caller doesn't get 'em wrong!) and the internal data.

In Cor, you can do the same thing, but you only get one constructor, CONSTRUCT, and we use named arguments instead of positional. If $arg_for{x} doesn't exist, the assignment to something requiring a PositiveNum should blow up (if you want it to be allowed, you'd use :isa(Maybe[PositiveNum])).

The Cor version has the advantage of readability:

# Perl
my $box = Box->new( height => 7, width => 3, depth => 3.4 ); 

// Java
Box box = new Box( 7, 3.4, 3 ); // what do those mean?

Cor also gives you extra flexibility in case you want to add more to that list, but there's a small price to pay in terms of the constructor being a touch more clumsy.

Ovid commented 4 years ago

@haarg wrote:

The slots are per class and not available in subclasses. However, the attributes would need to be handled via the CONSTRUCT method.

First, I think it's better to say that slots are per class and accessible per scope (though I'm less clear on this).

And what would happen if the child class didn't try to handle it via CONSTRUCT?

Maybe I'm misunderstanding, but I don't see this as fundamentally different from a parent using $self->{name} and the child also wanting to use $self->{name}. If a class is designed to be subclassed, it should be documented as such and the person writing the child should respect that. If it's not documented, the person writing the child will need to read the parent code. I don't see this as being too different from the situation we have today.

And what would roles be expected to do?

Same thing as for parent classes: their use is documented. You can't have a collision with slot declaration since those are scoped. Thus, we fall back to the same semantics that we use for method collision.

That does bring us back to a fundamental problem with roles: aliasing or excluding anything that the role depends on internally hits the early/late-binding problem whereby the role can't be guaranteed that late-bound methods are correct. However, we have this issue with roles in Perl now and it doesn't appear to have caused too many issues. Maybe it's worrying too much over nothing?

Actually, possibly a better question is how is the default construction (the NEW method) going to handles roles at all?

Yes, that is a better question. NEW can't assign to slots it doesn't have direct access to. This might be a design flaw. Let me come back to you. Alternative suggestions welcome.

iamalnewkirk commented 4 years ago

I must say, I really like the way Moose attribute declarations use key-value pairs with fat-commas, and how complex attribute declarations can be cleanly and clearly expressed across multiple lines. I think we should consider this when designing the attribute declaration syntax.

Ovid commented 4 years ago

@haarg: regarding an earlier comment of yours:

It's probably worth clarifying the scope of attributes. If you declare:

class Foo {
    {
        has $foo;
        method foo1 { $foo }
    }
    {
        has $foo;
        method foo2 { $foo }
    }
}

Are the two $foo variables distinct?

I would say that has, in this case, is like our, but for instances. Each $foo refers to the same instance variable, but access is scoped to the block. This seems (to me) to be very Perlish. However, it means that the slot attributes would have to "stack". You can't have one $foo be :isa(Int) and the other $foo be :isa(DateTime).

So either we require one declaration for $foo plus attributes and other uses are simply has $foo; (with no attributes) or we try to "stack" all of the attribute values. I think the "only one can have attributes" is much safer.

Is there any way to have share a slot between parent and child classes aside from using their accessors?

The intent is to have complete encapsulation be the default, so a child can never see the parent slots (and vice versa, obviously).

Grinnz commented 4 years ago

So either we require one declaration for $foo plus attributes and other uses are simply has $foo; (with no attributes) or we try to "stack" all of the attribute values. I think the "only one can have attributes" is much safer.

What about, every declaration's attributes are considered as if they were all one declaration. So doing "has $foo :foo" and "has $foo :bar" would be the same as doing "has $foo :foo :bar" including any errors that would trigger due to incompatible attributes.

Ovid commented 4 years ago

@Grinnz: I thought about that (that's what I meant by "stacking"), but I was concerned it would be a maintenance nightmare if you have a large class and you don't notice the :isa(...) which was slapped on the attribute a few hundred lines later.

Grinnz commented 4 years ago

It seems like that would be a concern either way unless you require all the same things to be specified every time you declare it.

Ovid commented 3 years ago

For anyone following this discussion, the :new(optional) syntax has been removed.

If you want to make a :new slot optional, just specify a default or builder:

has $x :new = 0;
#
has $x :new :builder;
method _build_x ($self) {...}

If it's optional and genuinely not required, you can specify undef as the default.

has $title :new = undef;

This may ultimately have impacts on the meta-protocol. For example, there's a difference between undefined and uninitialized, but this might blur that difference. Or if undef is a legal value, what should predicate methods return?

has $frobnitz = :new :predicate = undef;

In the above, if you don't pass in a value for frobnitz, it's explicitly set to undef and it's undefined, but not initialized via a contructor arg. But what if you pass frobnitz => undef in the constructor? It's still undefined, but now it is initialized via a constructor arg. Or do we special-case undef and say that it always means unintialized? (I think the answer to that is "no").

The reason it's potentially important is because you might want to distinguish whether a slot is undef because the class doesn't know what it should be, or if it's undef because that was the supplied value. Having undef simply mean false is problematic because it might also mean "unknown" (which isn't false) or "not applicable" (which is also often different from false).

Grinnz commented 3 years ago

In my opinion the distinction between undef and unset must be maintained.

Ovid commented 3 years ago

@Grinnz: I would love to see that, but we usually don't have it now, so I'm unsure that not having in Cor V1 is a showstopper.

Some initial thoughts:

duncand commented 3 years ago

Its good that one can pick their own names for reader/writer methods, but I would prefer the default writer name is not set_foo(); even if that is a common practice, it really grates against my sensibilities as the word "set" should be left to just refer to its namesake collection type. I instead propose any of assign_foo() or update_foo() or store_foo() or write_foo() etc. Remember Corinna's goal is to be better not just the same, and we can do much better here than "set" names.

duncand commented 3 years ago

When using blessed-hashref type OO, its easy to distinguish a non existing slot from an existing slot whose value is undef.

In the general case I see this issue not being specific to slots but it can also apply to regular my variables or parameters or anything else, and so a solution should be the same for all of them.

I propose that for Corrina version 1 there isn't any real value to distinguishing these cases and we should just formally say that there's no such thing as a non existing or unset slot.

We should simply say that slots always exist / are set and that they have undef as their value until something else is explicitly assigned to them. The exact same behaviour as my variables and such have.

Where there is any business case for the concept of not assigned yet, there are other ways to provide that.

In the normal sense, every slot has a meaningful value once at the latest the new/builder has run.

If logic in a class requires a concept of a slot that is only set some time after object creation, then they should manage that explicitly in some way, such as recognizing undef to mean not set yet, or having an extra companion slot that is a boolean which says whether the regular slot is considered set; generally you only need this if the regular slot allows undef as a normal value or if the normal slot has a type that doesn't allow undef at all (it might default to say zero or the empty string in that case whatever is appropriate for the type).

duncand commented 3 years ago

I support the proposal of "slot" being the keyword instead of "has" for its purpose.

duncand commented 3 years ago

There should be some permission mechanism akin to Raku's "trusts" or .NET's "internal" scope (or Java's package-private scope, although that's broken) etc that allows a class to declare that certain other classes are allowed to see certain internals while the general public is not. This is a similar idea to "protected" except that it has nothing to do with inheritance and is not tied to inheritance. An example use is say a graph module implemented in terms of Node and Side classes, where conceptually its just one module and the fact its implemented as 2 classes is just an implementation detail.

In fact I would consider this feature critical for my own projects, with the only workaround making everything public and hoping no one takes advantage.

duncand commented 3 years ago

Further to my comment about how the concept of not-is-set shouldn't exist: An aspect of a good built-in OO system is that where possible it can easily map directly to the same concepts as regular variables and slots in typical compiled languages. Being able to count on a slot always existing when its parent object or class exists makes it much easier to optimize in a variety of underlying implementations including those where the slots always exist. As a tangent, the concept of undef should just be considered part of the domain or type of the slot/variable in question, an alternate valid value, rather than something particularly special, like with a union type. Same as a variable/slot may allow just numbers or just character strings or it may allow a union of both, undef is just a singleton type that can be unioned with others optionally; it is allowed if the slot may be undefined, and not allowed if the slot must always be defined. In this sense actually, if users need some way to indicate not set yet as distinct from the system-provided undef, they can roll their own in the way best for their circumstance, and there isn't really a generic way to do this that is worth Corrina building in or not a code smell.

duncand commented 3 years ago

Another thing I recommend is omitting the whole lazy/eager thing from Corinna version 1 in regards to attribute helpers.

While laziness/eagerness control is an important thing to have, I feel that this is something better implemented ad-hoc by users in normal methods so that the behavior they get is the best fit for each individual scenario where laziness etc is needed.

I feel that attaching :lazy subs to slot definitions would frequently encourage poor design or code smells.

Part of the reason for this is that there are several different kinds of laziness that need very different treatments.

One is where the slot value is calculated entirely when it is first requested, which is a boon if it often would never be needed at all. Rather than a :lazy trait, the developer can just write their getter method explicitly and it would invoke the value producing code if it needs to before returning it.

Another kind of laziness is substitutive. For example, say the class implements a Set type. Conceptually a second contains only unique elements, but its implementation can be lazy by not checking for duplicates up front, because the applicable comparisons or indexing can be the most costly thing, and just storing what it is given which is inexpensive, and only checking for duplicates later if it actually needs to, such as if asked for a count of its elements. In such a case, the count() method would invoke the expensive deduplication work if called.

Another kind of laziness is cumulative, such as a collection whose elements are generated, often the elements would individually be generated as needed, so if the slot in question had the lazily generated collection, you wouldn't want to generate the entire collection on first access to the collection, just the elements needed, but :lazy couldn't handle this.

Another key reason why :lazy trait doesn't work in general is that often when you're dealing with laziness, the conditions for generation or access often involve multiple slots working in concert, and so there are inter-dependencies between multiple slots to do it properly, and this is best managed by ad-hoc user code rather than trying to define complicated declarative rules in the OO framework.

For example, say a slot lazily held a DBI connection, well in order for that to connect, it would probably need inputs stored in other slots.

Or you may want to lazily generate or modify multiple slots together as a set rather than them being individual.

So I say avoid the complexity of a lazy trait in Corrina version 1 and leave this to the users.

duncand commented 3 years ago

I recommend certain shorthand traits for common cases, inspired by Raku. Context is that all methods default to private, and some people complain about having to write more to make them public. Lets say if a slot has just :ro then a public reader will be declared with the default name, and if they instead have just :rw then public readers and writers will be declared with default names. One must still use :reader or :writer if they want to declare alternate names for generated accessors, but people who care about typing fewer characters probably wouldn't do that.

duncand commented 3 years ago

Alternately to my proposal of :ro plus :rw, we could have aliases for :reader and :writer themselves, so one can alternately spell them :r and :w and they otherwise have all the same features and behavior as spelling them out in full. So then someone who wants to be terse just puts :r for a slot they want to have a public read-only method, and :r:w if they want public read/write.

Ovid commented 3 years ago

@duncand for ideas such as :ro, :rw, or :r and friends, I suggest that those go to V2. For now, we need to identify an MVP and see what people do with it and what they really need. I've already pushed all of the method modifiers (override, private, etc.) to v2 because for the most part, we don't have those in core Perl anyway (though I think @leonerd may have already implemented parts of this and it could change).

If anything, I think Corinna should currently shoot for a v0.01 to keep scope under control.

duncand commented 3 years ago

@Ovid Re :r and friends, not a problem. Personally I'm quite happy with the full :reader etc. I just suggested this because in discussion you seemed to be saying there was a lot of pushback from people disliking private-by-default semantics, and I wanted to offer this as a better alternative to making slots public by default instead etc.

duncand commented 3 years ago

I would actually prefer that for all methods it is mandatory to mark them with a visibility, and that there is no default, then there's no ambiguity on what an absence of a visibility marker means. But if omitting that is allowed, I still think private should be the implicit default for everything, for safety and avoiding bugs.

iamalnewkirk commented 3 years ago

Thinking towards the future, if Cor eventually supports type constraints and typed attributes, how would attribute variable assignment maintain the type constraint, and should that consideration affect the design? e.g. has $x :isa(Str); ... $x = {}

duncand commented 3 years ago

@iamalnewkirk I would expect type constraints to be applied evenhandedly across the whole language. So when they are added to attributes they are also added to all kinds of subroutine parameters and also to regular lexical/global variables. This is one reason its logical to leave them out of Corinna version 1, because the type constraints are more orthogonal to the OO system.

Ovid commented 3 years ago

@iamalnewkirk What @duncand said ^^. I had originally wanted types and had even started on them, but I was looking at type constraints and not considering a type system (big mistake!). Plus, it was risking having incompatible type systems throughout the Perl language. Corinna would be shot down quickly by P5P if I show up with types.

But I hear you! And I want them. But without being able to predict the future, I have no idea how they'd look in Corinna.

duncand commented 3 years ago

Regarding the behavior of :writer generated mutator methods, I strongly recommend that the mutators return nothing at all / have a void return type. This is this the simplest and least surprise and least confusing and is how a lot of languages work already.

If people really want to set multiple slots at once then probably the best way is to supply them all to the constructor. You could also provide a special built-in analogous to the clone() you already plan which takes a list of name-value pairs like with new() but sets those members in the current object, that should nip all the normal chained-setter use cases in the bud, and for edge cases users can define their own methods.

How about call it multi_assign() perhaps.

Also returning nothing in version 1 is also forwards compatible with a possible design change that returns something later, as code expecting nothing to be returned and not using a return value won't break if it starts returning something.

duncand commented 3 years ago

The way I figure it, the real proper way to have an OO type system is that the parent-most type(s) define selectors/constructors suitable for making every possible object of that class AND every possible object of all of its subclasses that could ever exist, and child types are nothing but added constraints on their parents. So to represent a typical hierarchy, the root universal type is characterized by a set of attributes such that the set can have any cardinality and the attributes can have any distinct names and any values, like a plain old Perl hashref. Then every other type is defined as a constraint on this universal type, the constraint often saying, the set of attributes must have a subset that has particular names and types and/or the set of attributes must consist solely of those named attributes and must not have any extras. So to be a valid value/object of a type/class, it is characterized by a hashref which satisfies all of the constraints defined by that class and its parents. In this context, all attributes are fundamentally public, but that's not a problem because the constraint definitions prevent those attributes from being anything that don't satisfy the constraint. I could go into more detail but that's the main idea.

iamalnewkirk commented 3 years ago

I completely agree with this idea and approach. Furthermore, at the risk of going completely off-topic, I think that a type system such as the one described, as well as a simple standard library, could also serve as a demonstration of what’s possible using this system with best practices.

On Sat, Feb 27, 2021 at 7:17 AM Darren Duncan notifications@github.com wrote:

The way I figure it, the real proper way to have an OO type system is that the parent-most type(s) define selectors/constructors suitable for making every possible object of that class AND every possible object of all of its subclasses that could ever exist, and child types are nothing but added constraints on their parents. So to represent a typical hierarchy, the root universal type is characterized by a set of attributes such that the set can have any cardinality and the attributes can have any distinct names and any values, like a plain old Perl hashref. Then every other type is defined as a constraint on this universal type, the constraint often saying, the set of attributes must have a subset that has particular names and types and/or the set of attributes must consist solely of those named attributes and must not have any extras. So to be a valid value/object of a type/class, it is characterized by a hashref which satisfies all of the constraints defined by that class and its parents. In this context, all attributes are fundamentally public, but that's not a problem because the constraint definitions prevent those attributes from being anything that don't satisfy the constraint. I could go into more detail but that's the main idea.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Ovid/Cor/issues/4#issuecomment-787064251, or unsubscribe https://github.com/notifications/unsubscribe-auth/AGCJIZ2FUUXY6XTJE2J2DK3TBDPFDANCNFSM4LRIHJKA .

--

Al Newkirk 267 225 0655 (office) 267 601 4046 (mobile)

iamalnewkirk.com al@iamalnewkirk.com linkedin.com/in/alnewkirk https://www.linkedin.com/in/alnewkirk

duncand commented 3 years ago

Al Newkirk said:

I completely agree with this idea and approach. Furthermore, at the risk of going completely off-topic, I think that a type system such as the one described, as well as a simple standard library, could also serve as a demonstration of what’s possible using this system with best practices. On Sat, Feb 27, 2021 at 7:17 AM Darren Duncan wrote:

Still risking going off topic...

Thank you Al for your vote of confidence and interest in my larger idea.

I have two main answers for your request for demonstration.

First answer, Raku already has a working implementation in a sense in its subset type declaration syntax, and AFAIK Raku is already an inspiration for Corinna's design.

https://docs.raku.org/language/typesystem#subset

The given example they have subset Foo of List where (Int,Str); loosely illustrates the concept I proposed here.

Second answer, I have a work in progress new language type system which is entirely based on this design. It is not an OO language/system itself, it is actually more functional, but all the important OO concepts have direct mappings. This doesn't run yet and is mostly just a small collection of specs of different ages.

See https://github.com/muldis/Muldis_Object_Notation for the most up to date though still outdated partial documentation of that system.

See also https://github.com/muldis/Muldis_Data_Language/blob/master/lib/Muldis/D/Package/System.pod and https://github.com/muldis/Muldis_Data_Language/blob/master/lib/Muldis/D/Foundation.pod for older partial documentation of that system.

damil commented 3 years ago

@cxw42 wrote (24 Mar 2020) :

:new(...) reads strangely to me.

@Ovid wrote (24 Mar 2020) :

That being said, I've never liked the name :new, so suggestions for a better name are welcome.

Sorry, coming a bit late to that discussion. I also find that :new is very ambiguous : just reading the english, it could wrongly convey the idea that the slot is "new" with respect to some previous context. What we really want to say here is that this slot receives its initial value through the constructor, and it happens only by convention that this constructor is called "new" -- in this case a sensible name because it is used to create new instances.

I also dislike :name(foo), because we are not talking about the name of the slot, but about the named argument passed to the constructor that will fill this slot.

So my proposal would be : has $foo :constructed, or, if we want a different named argument to the constructor : has $foo :constructed(bar) . If this is too verbose, :constructed could be abbreviated into something like :constr or :ctr.

leonerd commented 3 years ago

@damil

Sorry, coming a bit late to that discussion. I also find that :new is very ambiguous : just reading the english, it could wrongly convey the idea that the slot is "new" with respect to some previous context. What we really want to say here is that this slot receives its initial value through the constructor, and it happens only by convention that this constructor is called "new" -- in this case a sensible name because it is used to create new instances.

I also dislike :name(foo), because we are not talking about the name of the slot, but about the named argument passed to the constructor that will fill this slot.

Yes - I find myself in a similar boat in fact. I've just been thinking about adding this behaviour to Object::Pad, and while I like the idea of an attribute on the slot to say "assign this from a named parameter to the constructor; named like the slot unless I say otherwise", I don't like the word :new for it.

Maybe we can find a better word?

damil commented 3 years ago

So what do you think of my proposals :constructed or :constr ? A slot named "foo" with named constructor parameter "foo" would be declared as has $foo :constr, and the same but with named parameter "bar" instead of "foo" would be has $foo :constr(bar)