package Data::COW; use 5.006001; use strict; no warnings; use Exporter; use Scalar::Util qw; use overload (); # we're not overloading anything, but we'd like to # check if they're already implementing a value type use base 'Exporter'; our @EXPORT = qw; our $VERSION = '0.02'; sub tied_any { my ($ref) = @_; if (ref $ref) { if (reftype($ref) eq 'SCALAR') { tied $$ref; } elsif (reftype($ref) eq 'ARRAY') { tied @$ref; } elsif (reftype($ref) eq 'HASH') { tied %$ref; } } } sub cow_object { my ($ref) = @_; my $tied = tied_any $ref; $tied && $tied->isa('Data::COW') && $tied; } sub make_cow_ref { my ($ref, $parent, $key) = @_; make_cow_ref_nocheck($ref, $parent, $key); } sub make_temp_cow_ref { my ($ref, $parent, $key) = @_; if (my $obj = cow_object $ref) { if ($obj->{parent} == $parent) { $ref; } else { make_cow_ref_nocheck($ref, $parent, $key); } } else { make_cow_ref_nocheck($ref, $parent, $key); } } sub make_cow_ref_nocheck { my ($ref, $parent, $key) = @_; if (ref $ref && # check if they already think they're a value type !(overload::Overloaded($ref) && overload::Method($ref, '='))) { my $ret; if (reftype($ref) eq 'SCALAR') { tie my $it => 'Data::COW::Scalar', $ref, $parent, $key; $ret = \$it; } elsif (reftype($ref) eq 'ARRAY') { tie my @it => 'Data::COW::Array', $ref, $parent, $key; $ret = \@it; } elsif (reftype($ref) eq 'HASH') { tie my %it => 'Data::COW::Hash', $ref, $parent, $key; $ret = \%it; } else { # code and glob are not aggregates that we can take control # of, so punt and just return them like anything else return $ref; } if (blessed($ref)) { bless $ret => blessed($ref); } return $ret; } else { return $ref; } } sub clone_using { my ($self, $copier) = @_; return unless $self->{const}; my $old = $self->{ref}; my $new = $copier->($old); if (blessed $old) { bless $new => blessed $old; } if ($self->{parent}) { my $cnew = make_cow_ref $new, $self->{parent}, undef; tied_any($cnew)->{const} = 0; $self->{parent}->clone($self->{key} => $cnew); } $self->{ref} = $new; $self->{const} = 0; } package Data::COW::Scalar; use Tie::Scalar; use base 'Tie::Scalar'; use base 'Data::COW'; sub TIESCALAR { my ($class, $ref, $parent, $key) = @_; bless { ref => $ref, parent => $parent, key => $key, const => 1, } => ref $class || $class; } sub clone { my ($self, $key, $value) = @_; $self->clone_using(sub { my $v = ${$_[0]}; \$v }); ${$self->{ref}} = $value if defined $key; } sub FETCH { my ($self) = @_; Data::COW::make_temp_cow_ref(${$self->{ref}}, $self, 1); } sub STORE { my ($self, $value) = @_; $self->clone(1 => $value); $value; } package Data::COW::Array; use Tie::Array; use base 'Tie::Array'; use base 'Data::COW'; sub TIEARRAY { my ($class, $ref, $parent, $key) = @_; bless { ref => $ref, parent => $parent, key => $key, const => 1, } => ref $class || $class; } sub clone { my ($self, $key, $value) = @_; $self->clone_using(sub { [ @{$_[0]} ] }); $self->{ref}[$key] = $value if defined $key; } sub FETCH { my ($self, $key) = @_; Data::COW::make_temp_cow_ref($self->{ref}[$key], $self, $key); } sub STORE { my ($self, $key, $value) = @_; $self->clone($key => $value); $value; } sub FETCHSIZE { my ($self) = @_; scalar @{$self->{ref}}; } sub STORESIZE { my ($self, $size) = @_; $self->clone; $#{$self->{ref}} = $size-1; } sub DELETE { my ($self, $key) = @_; $self->clone; delete $self->{ref}[$key]; } sub EXISTS { my ($self, $key) = @_; exists $self->{ref}[$key]; } package Data::COW::Hash; use Tie::Hash; use base 'Tie::Hash'; use base 'Data::COW'; sub TIEHASH { my ($class, $ref, $parent, $key) = @_; bless { ref => $ref, parent => $parent, key => $key, const => 1, } => ref $class || $class; } sub clone { my ($self, $key, $value) = @_; $self->clone_using(sub { my $ret = { %{$_[0]} }; $ret; }); $self->{ref}{$key} = $value if defined $key; } sub FETCH { my ($self, $key) = @_; Data::COW::make_temp_cow_ref($self->{ref}{$key}, $self, $key); } sub STORE { my ($self, $key, $value) = @_; $self->clone($key => $value); $value; } sub EXISTS { my ($self, $key) = @_; exists $self->{ref}{$key}; } sub DELETE { my ($self, $key) = @_; $self->clone; delete $self->{ref}{$key}; } sub CLEAR { my ($self) = @_; $self->clone_using(sub { {} }); (); } sub FIRSTKEY { my ($self) = @_; my $a = keys %{$self->{ref}}; # reset iterator each %{$self->{ref}}; } sub NEXTKEY { my ($self) = @_; each %{$self->{ref}}; } sub SCALAR { my ($self) = @_; scalar %{$self->{ref}}; } 1; =head1 NAME Data::COW - clone deep data structures copy-on-write =head1 SYNOPSIS use Data::COW; my $array = [ 0, 1, 2 ]; my $copy = make_cow_ref $array; push @$array, 3; # $copy->[3] is 3 push @$copy, 4; # $array->[4] is not defined (and doesn't even exist) # $copy is a real copy now push @$array, 5; # $copy is unaffected =head1 DESCRIPTION Data::COW makes copies of data structures copy-on-write, or "lazily". So if you have a data structure that takes up ten megs of memory, it doesn't take ten megs to copy it. Even if you change part of it, Data::COW only copies the parts that need to be copied in order to reflect the change. Data::COW exports one function: C. This takes a reference and returns a copy-on-write reference to it. If you don't want this in your namespace, and you want to use it as C, use the module like this: use Data::COW (); Data::COW won't be able to copy filehandles or glob references. But how do you change those anyway? It's also probably a bad idea to give it objects that refer to XS internal state without providing a value type interface. Also, don't use stringified references from this data structure: they're different each time you access them! =head1 SEE ALSO L =head1 AUTHOR Luke Palmer =head1 COPYRIGHT Copyright (C) 2005 by Luke Palmer This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.3 or, at your option, any later version of Perl 5 you may have available.