# # Copyright (c) 2019-2021 Christian Jaeger, copying@christianjaeger.ch # # This is free software, offered under either the same terms as perl 5 # or the terms of the Artistic License version 2 or the terms of the # MIT License (Expat version). See the file COPYING.md that came # bundled with this file. # =head1 NAME FP::PureHash =head1 SYNOPSIS use FP::PureHash; use FP::Show; use FP::Predicates; my $h = purehash hi => 123, there => "World"; my $h2 = $h->set("hi", "again"); is $h->ref("there"), "World"; is $$h{there}, "World"; is show($h), "purehash('hi' => 123, 'there' => 'World')"; is show($h2), "purehash('hi' => 'again', 'there' => 'World')"; is is_pure($h2), 1; like( (eval { purehash hi => 1, hi => 1, there => 2 } || $@), qr/^duplicate key: 'hi' at/ ); like( (eval { $$h{there_} || 1} || $@), # the detailed exception message may change! qr/^Attempt to access disallowed key 'there_' in a restricted hash/ ); =head1 DESCRIPTION Hash tables, currently based simply on Perl's internal hashes. They are immutable, and restricted which means that accessing non-existing keys yields an exception. =head1 TODO - performant functional updates (currently the `set` method simply copies the whole table) - more methods, move/adapt set functionality from FP::Hash and FP::HashSet - a maybe_ref that returns FP::Failure or FP::Maybe ? - non-string keys? =head1 SEE ALSO Implements: L<FP::Abstract::Pure>, L<FP::Abstract::Map>, L<FP::Abstract::Equal>, L<FP::Abstract::Show> =head1 NOTE This is alpha software! Read the status section in the package README or on the L<website|http://functional-perl.org/>. =cut package FP::PureHash; use strict; use warnings; use warnings FATAL => 'uninitialized'; use Exporter "import"; our @EXPORT = qw(purehash); our @EXPORT_OK = qw(hash_clone_to_purehash hash_to_purehash); our %EXPORT_TAGS = (all => [@EXPORT, @EXPORT_OK]); use FP::Docstring; use FP::Show; use Scalar::Util qw(blessed); use FP::Carp; our $immutable = 1; sub purehash { __ 'convert key/value pairs to an immutable hash; re-use of keys is an error'; die "uneven number of arguments" if @_ & 1; my %out; for (my $i = 0; $i < @_; $i += 2) { my $k = $_[$i]; if (exists $out{$k}) { die "duplicate key: " . show($k); } $out{$k} = $_[$i + 1]; Internals::SvREADONLY $out{$k}, 1 if $FP::PureHash::immutable; } my $res = bless \%out, "FP::_::PureHash"; Internals::SvREADONLY %out, 1 if $FP::PureHash::immutable; # XX ^ this also changes the behaviour accessing a non-existing key, yeah; # why not just overload? oh that was said to be slow or was it Tie ? $res } sub hash_clone_to_purehash { @_ == 1 or fp_croak_arity 1; FP::_::PureHash->new_from_hash({ %{ $_[0] } }) } sub hash_to_purehash { @_ == 1 or fp_croak_arity 1; FP::_::PureHash->new_from_hash($_[0]) } sub is_purehash { @_ == 1 or fp_croak_arity 1; my ($v) = @_; my $r = blessed($v) // return; $v->isa("FP::_::PureHash") } package FP::Hash::Mixin { use FP::Equal 'equal'; use Chj::NamespaceCleanAbove; sub FP_Show_show { my ($s, $show) = @_; $s->constructor_name . "(" . join(", ", map { &$show($_) . " => " . &$show($$s{$_}) } sort keys %$s) . ")" } sub FP_Equal_equal { my ($a, $b) = @_; keys(%$a) == keys(%$b) and do { for my $key (keys %$a) { exists $$b{$key} or return 0; equal($$a{$key}, $$b{$key}) or return 0; } 1 } } _END_ } package FP::PureHash::autobox { our $AUTOLOAD; sub AUTOLOAD { my $methodname = $AUTOLOAD; $methodname =~ s/.*:://; my $v = FP::_::PureHash->new_from_hash($_[0]); if (my $m = $v->can($methodname)) { goto $m } else { die "no method '$methodname' found for object: $v"; } } } package FP::_::PureHash { use base "FP::Hash::Mixin"; use FP::Interfaces; use FP::Carp; use Chj::NamespaceCleanAbove; sub new_from_hash { @_ == 2 or fp_croak_arity 2; my ($class, $out) = @_; if ($FP::PureHash::immutable) { for my $k (keys %$out) { Internals::SvREADONLY $out->{$k}, 1 } } my $res = bless $out, "FP::_::PureHash"; Internals::SvREADONLY %$out, 1 if $FP::PureHash::immutable; $res } sub constructor_name {"purehash"} # XX why not get, again? set and get? If ref, then what for set? sub ref { @_ == 2 or fp_croak_arity 2; my ($s, $key) = @_; $$s{$key} } sub perhaps_ref { @_ == 2 or fp_croak_arity 2; my ($s, $key) = @_; exists $$s{$key} ? $$s{$key} : () } sub set { @_ == 3 or fp_croak_arity 3; my ($s, $key, $val) = @_; # XX the inefficient approach... to be replaced with new impl. my %out = %$s; $out{$key} = $val; if ($FP::PureHash::immutable) { for my $k (keys %out) { Internals::SvREADONLY $out{$k}, 1 } } my $res = bless \%out, "FP::_::PureHash"; Internals::SvREADONLY %out, 1 if $FP::PureHash::immutable; $res } sub key_value_pairs { @_ == 1 or fp_croak_arity 1; my ($h) = @_; map { [$_, $h->{$_}] } sort keys %$h } sub array { @_ == 1 or fp_croak_arity 1; my ($h) = @_; [$h->key_value_pairs] } sub purearray { @_ == 1 or fp_croak_arity 1; my ($h) = @_; # XX load FP::PureArray or not, here or always? FP::_::PureArray->new_from_array($h->array) } sub list { @_ == 1 or fp_croak_arity 1; my ($h) = @_; # XX load FP::List or not, here or always? FP::List::array_to_list($h->array) } # Should we add this? Auto-choose the best sequence for the task? sub sequence { @_ == 1 or fp_croak_arity 1; my ($h) = @_; $h->purearray } FP::Interfaces::implemented qw( FP::Abstract::Pure FP::Abstract::Map FP::Abstract::Equal FP::Abstract::Show); _END_ } 1