Moose, laziness, and weak refrences

Last night, I was making some improvements to some work code, and trying to make it easier to use in places where I wasn’t using it yet. Part of this involved allowing an important attibute to be a weak reference. I really wanted to do something like this:

my $child  = $parent->child;
my $parent = $child->parent;

In this code, $parent and $child may be expensive to create, so I want both to have a reference to each other, without worrying about cyclical references, which will prevent either variable from ever being garbage collected. That means one of the references has to be weak. I picked the reference from $child to $parent. Unfortunately, this means that $parent can be garbage collected before $child, which can lead to this problem:

my $parent = Parent->new(...);
my $child  = $parent->child;

$child->parent; # returns $parent

undef $parent;

$child->parent; # returns undef, which may violate the attribute's type

Alternately, here’s a runnable example that demonstrates just the problematic behavior.

use 5.12.0;
our $parent;

package Child;
use Moose;

has parent => (
  is   => 'ro',
  isa  => 'Ref',
  lazy => 1,
  default => sub {
    $parent = {};
    return $parent;
  },
  weak_ref => 1,
);

package main;
my $child = Child->new;

use Test::More;
is($parent, undef, '$parent begins undef');
is_deeply($child->parent, {}, '$child->parent returns {}');
is_deeply($parent, {}, 'now $parent is set');

undef $parent;

is($parent, undef, '$parent has been undefined');
is_deeply($child->parent, {}, '$child->parent returns {}');

done_testing;

I ended up hiding the reader method for the parent variable and replacing it with one that checks the definedness of the attribute. If it’s not defined, it clears the attribute and re-accesses it, which causes the lazy constructor to fire again.

has parent => (
  is        => 'bare',
  reader    => '_parent',
  isa       => 'Parent',
  lazy      => 1,
  weak_ref  => 1,
  clearer   => '_clear_parent',
  default   => sub {
    my ($self) = @_;
    Parent->get_parent_of($self);
  },
);

sub parent {
  my ($self) = @_;
  my $value = $self->_parent
  return $value if defined $value;
  $self->_clear_parent;
  return $self->_parent;
}

This is already pretty complicated, but it’s still not enough. When the value in $parent gets garbage collected, the next time we try to read it, our method notices that it’s undef and gets a new value. Unfortunately, because nothing else refers to the new value it is immediately garbage collected. We need to only weaken references that were set during construction.

has parent => (
  is        => 'bare',
  reader    => '_parent',
  isa       => 'Parent',
  lazy      => 1,
  predicate => '_has_parent',
  clearer   => '_clear_parent',
  default   => sub {
    my ($self) = @_;
    Parent->get_parent_for($self);
  },
);

sub parent {
  my ($self) = @_;
  my $value = $self->_parent
  return $value if defined $value;
  $self->_clear_parent;
  return $self->_parent;
}

sub BUILD {
  my ($self) = @_;
  Scalar::Util::weaken $self->{parent} if $self->_has_parent;
}

Weak references are often a lot more complicated than they might seem. It can be attractive to try and solve a problem by “just weakening” one half of a relationship. Very often, it doesn’t solve a problem, it just replaces it with a much weirder or more subtle one.

Written on May 6, 2010
🐫 perl
🧑🏽‍💻 programming