#
# 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 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 Scalar::Util qw(blessed);
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';
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_
}
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 FP::Carp;
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