Skip to content

Questions

Ovid edited this page Sep 14, 2021 · 11 revisions

Please see the main page of the repo for the actual RFC. As it states there:

Anything in the Wiki should be considered "rough drafts."

Open Questions About Cor

1. Unitialized versus Undefined

Leave feedback here.

It's rare, but if a slot (has $foo :predicate) can be undef, we probably want the predicate method to return true for has_foo. Otherwise, we can't distinguish between an intentional undefined value and and an unset value.

2. Readers With Custom Writers

Resolved, for now.

In general, Corinna wants immutable objects. In practice, sometimes this is hard. For example, in the case of a Binary Tree, you want to be able to add a new node. Let's imagine an integer tree. How might we use that?

my $tree  = BinaryTree->new( value => 10 );
my $left  = $tree->left(3);
my $right = $tree->right(17);
$left->left(1);
$left->right(17);

And that might represent a tree like this:

         10
        /  \
       3   17
      / \
    1   17

So far, so good. If you're traversing the tree, you might want to check out a node's value:

if ( $node->left->value == $target ) {
    ...
}

In other words, this fits the common Perl paradigm of "faking" method overloading. For other language, we might have this:

method left () {
    # just return the left node
    return $left;
}

method left ($child) {
    # Set the value of the left node. If it doesn't exist,
    # create it and and set the parent to $self
    # if $child is already a binary tree, ensure that it doesn't
    # already have a parent
}

But we don't have method overloading in Perl. So we do variants of this:

sub left {
    my ( $self, $child ) = @_;
    if ( @_ > 1 ) {
        if ( blessed $child && $child->isa('BinaryTree') ) {
            croak(...) if $child->has_parent; # we don't reparent binary trees
        }
        else {
            $child = BinaryTree->new( value => $child );
        }
        $child->parent($self);
    }
    else {
        return $self->{left};
    }
}

That's very easy to get wrong. For example, you might test if $child is defined instead of how many arguments are passed to the method. That means you're never allowed to pass in an undefined value.

It's also the case that $child->left(...) is acting as a contructor and maybe it should have named arguments, but we'll ignore that for now.

Now how do we declare the left and right slots and attributes?

Failure #1: being too simple

This doesn't work:

has $left :reader :writer :isa('BinaryTree');

For the above, we have to allow the writer to accept a raw value or a binary tree. Corinna is not (at this time) offering coercions or triggers. Further, given that so many languages have no problem with this, it's clearly not a requirement that we have coercions or triggers.

Failure #2: avoid overloading the reader/writer attributes

The following also seems like a non-starter:

has $left :reader :writer(set_left) :isa('BinaryTree');

method set_left($child) { ... }

I mean, that's clear, but Perl developers really want to overload attributes to be both readers and writers. Having left() and set_left() isn't popular at all and Corinna isn't going to satisfy anyone if we forbid one of the most common Perl idioms.

Failure #3: Argument List checking

We could also punt:

has $left :isa('BinaryTree');

method left ($value=undef) {
    return $left unless defined $value;
    # set value
}

Except that breaks in this case because if we want to be able to set a node to undef, we can't!

$node->left(undef);

So for that case, we need to be able to check the number of arguments we had:

method left($value=undef) {
    return $left unless @_ > 1; # NO!
    # set value
}

But that's falling back to the @_ array. This is legacy cruft that Corinna is trying to leave behind. It's a constant source of bugs in Perl and having to check the number of arguments is ridiculous in 2020 (the year I'm writing this).

A Solution?

So what about this?

has $left :reader :isa('BinaryTree');

method left ($child) {
    if ( blessed $child && $child->isa('BinaryTree') ) {
        croak(...) if $child->has_parent; # we don't reparent binary trees
    }
    else {
        $child = BinaryTree->new( value => $child );
    }
    $child->parent($self);
    return $left = $child;
}

Well, that's method overloading in disguise. What if Corinna had a special-case just this once (famous last words)? If left is called without arguments, we received the value of $left, but if it's called with arguments, we try to dispatch it to the left method.

Internally that might be what Corinna does anyway, but in this case, we're explicit about overloading the writer by providing our own implementation.

And that might imply that this is illegal (supplying the method when you already have a writer attribute). That's because we don't want people accidentally overriding the :writer attribute. Or maybe we do want this behavior. I'm undecided.

has $left :reader :writer :isa('BinaryTree');

method left ($child) {
    ...
}

3. Querying an object

All the time we see variants of this:

if ( blessed $thing && $thing->can($method) ) {
    ...
}

Or, instead of can, we have isa, or some other method we want to call on the object. And blessed is imported via Scalar::Util, so that's an extra dependency.