# # Copyright (c) 2015-2019 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::Abstract::Sequence - functional sequence protocol =head1 SYNOPSIS use FP::Predicates "is_sequence"; # since we can't have it in # FP::Abstract::Sequence use FP::PureArray; use FP::StrictList; use FP::List; use FP::Stream; use FP::Array 'array'; use FP::Equal 'is_equal'; is_equal list(purearray(3,4), strictlist(3,4), list(3,4), stream(3,4), cons(3,4), # ok this can't really count as a sequence, # what to do about it? array(3,4), # Could `autobox` change this? 3, {3=>4})->map(*is_sequence), list(1,1,1,1,1,0,0,0); my $ns= purearray(FP::Abstract::Sequence->fp_interface_method_names); # The methods you can count on being supported by sequences. is_equal $ns->sort->take(5), purearray('any', 'append', 'array', 'cons', 'drop'); =head1 DESCRIPTION This is a functional protocol, i.e. its use does not exert any side effects (that are visible to the user). It does *not* imply `FP::Abstract::Pure`, though; impure data structures could implement it all the same (in addition to a mutation protocol). =head1 SEE ALSO L =head1 NOTE This is alpha software! Read the package README. =cut package FP::Abstract::Sequence; use strict; use warnings; use warnings FATAL => 'uninitialized'; require FP::List; # "use"ing it would create a circular dependency use FP::Array_sort qw(on_maybe); use FP::Lazy; use FP::Ops qw(add mult); use FP::Predicates qw(complement); use Chj::NamespaceCleanAbove; sub fp_interface_method_names { my $class= shift; (# base definition in this file hence not necessary to specify # here except for the sake of safety in case the base implementation is # removed: qw( flatten join extreme min max minmax subsection make_reduce reduce reduce_right sum product none ), # virtual methods: grep { not /^TODO/ } qw( is_null first rest first_and_rest maybe_first maybe_rest perhaps_first perhaps_rest perhaps_first_and_rest map map_with_islast filter TODO_filter_with_tail drop_while TODO_rtake_while_and_rest TODO_rtake_while TODO_take_while_and_rest take_while every any find fold fold_right preferred_fold append reverse take drop xone perhaps_one zip for_each join strings_join length second cons array list stream sort ), # $class->SUPER::fp_interface_method_names ) } #XXX different protocol for random access ones: #hmm add ref here too? vs. efficient_ref etc. *?* or slow_ref or somthing? # InefficientSequence # ref # last # butlast # also sub subsection slice ? # *set= blessing \&array_set; # *push= blessing \&array_push; # *pop= blessing_snd \&array_pop; # *shift= blessing_snd \&array_shift; # *unshift= blessing \&array_unshift; #XXX other # group group_by # zip2 # XXX these don't weaken the caller arguments, thus will leak for # streams. How to solve this (and not copy-paste-adapt the methods # manually) without fixing perl? sub flatten { @_==1 or @_==2 or die "wrong number of arguments"; my ($self, $perhaps_tail)=@_; $self->fold_right (sub { my ($v, $rest)=@_; $v->append($rest) }, @_==2 ? $perhaps_tail : FP::List::null()); } # XXX and on top of that, these return a lazy result even if the input # isn't; related to the above issue. Find solution for both. # unlike strings_join which returns a single string, this builds a new # sequence with the given value between all elements of the original # sequence # (XX only works computationally efficient for *some* sequences; # introduce an FP::Abstract::IterativeSequence or so and move it # there?) sub join { @_==2 or die "wrong number of arguments"; my ($self, $value)=@_; # (should we recurse locally like most sequence functions? Or is # it actually a good idea to call the method on the rest (for # improper_listS, but once we introduce a `cons` method on # PureArray etc. that won't happen anymore?)?) lazy { $self->is_null ? $self : do { my ($v,$rest)= $self->first_and_rest; $rest->is_null ? $self : FP::List::cons($v, FP::List::cons($value, $rest->join($value))) } } } # XX better name? sub extreme { @_==2 or die "wrong number of arguments"; my ($self, $cmp)=@_; # XXX: fold_right is good for FP::Stream streaming. left fold will # be better for FP::List. How? Add fold_left for explicit left # folding and make fold chose the preferred solution for # order-irrelevant folding? $self->rest->fold (sub { my ($v, $res)=@_; &$cmp($v, $res) ? $v : $res }, $self->first); } sub min { @_==1 or @_==2 or die "wrong number of arguments"; my ($self, $maybe_extract)=@_; $self->extreme(on_maybe $maybe_extract, sub { $_[0] < $_[1] }) } sub max { @_==1 or @_==2 or die "wrong number of arguments"; my ($self, $maybe_extract)=@_; $self->extreme(on_maybe $maybe_extract, sub { $_[0] > $_[1] }) } sub minmax { @_==1 or @_==2 or die "wrong number of arguments"; my ($self, $maybe_extract)=@_; # XXX same comment as in `extreme` @{$self->rest->fold (defined $maybe_extract ? sub { my ($v, $res)=@_; my ($min,$max)= @$res; my $v_ = &$maybe_extract($v); [ $v_ < &$maybe_extract($min) ? $v : $min, $v_ > &$maybe_extract($max) ? $v : $max ] } : sub { my ($v, $res)=@_; my ($min,$max)= @$res; [ $v < $min ? $v : $min, $v > $max ? $v : $max ] }, [$self->first, $self->first])} } sub subsection { @_==3 or die "wrong number of arguments"; my ($self, $i0, $i1)=@_; # XXX same comment as in `extreme` $self->drop($i0)->take($i1 - $i0) } # Reduce is a variant of fold that doesn't require an initial # reduction value but instead takes $seq.first() as the initial # reduction value (it hence only works in general if the types of the # two arguments of $fn are the same). If $seq is empty, produces the # result by calling $fn with no arguments. # Note: this base implementation relies on an efficient ->rest # operation; for types where ->rest can't be made efficient (via # e.g. slices), override this implementation! sub make_reduce { my ($_class, $fold)=@_; sub { @_==2 or die "wrong number of arguments"; my ($seq, $fn)= @_; if ($seq->is_null()) { &$fn() } else { my ($first, $rest)= $seq->first_and_rest; $rest->$fold($fn,$first) } } } # This variant folds from whichever side makes more sense for the # type: # *reduce= __PACKAGE__->make_reduce("preferred_fold"); # But, the question is really eager vs. lazy evaluation, i.e. foldl vs # foldl'. Should we do that? For now just: sub reduce; *reduce= __PACKAGE__->make_reduce("fold"); # These variants are explicit in which side they are folding from: sub reduce_right; *reduce_right= __PACKAGE__->make_reduce("fold_right"); sub sum { @_==1 or die "wrong number of arguments"; $_[0]->reduce(*add) } sub product { @_==1 or die "wrong number of arguments"; $_[0]->reduce(*mult) } sub none { @_==2 or die "wrong number of arguments"; my ($s, $pred)=@_; $s->every (complement $pred) } _END_