-
Notifications
You must be signed in to change notification settings - Fork 0
Alternatives
Have a sequence of records that need different handling depending on what's in them? You can go the dispatch way, or you can read on...
The factory function alternatives lets you provide a list of tubes that will be tried in order until one of them returns something (tube-wise, i.e. a record, a list of records or an iterator).
It lets you craft different tubes with different behaviour, and give them a try at handling a record. The first that succeeds stops this trial-and-error process.
As an example, we have too many friends (good for us!) and we want to give two parties, one for couples and one for singles. We will start from the following data:
my $input = <<'END';
couple:Carla e Franco
couple:Mario e Francesco
single:Marcello
single:Federica
ENDWe already know how to start:
pipeline(
'Source::iterate_files',
'Reader::by_line',
['Parser::by_format' => 'status:name'],
...
['Writer::to_files', interlude => "\n---\n"],
{tap => 'sink'},
)->([\$input]);At this point, we want to use the following template for couples:
my $template_for_couples = <<'END';
Hi [% name %]!
We'll be giving a party on the coming Sunday! Would you be with us?
Yours,
S&F.
END
and this other template for singles:
my $template_for_singles = <<'END';
Hi [% name %]!
We'll be giving a party on the coming Saturday! Would you join us?
Cheers,
S&F
END
Last, we want to write out one single file per invitation, but separated into two different directories.
Here's how we can use alternatives to choose either one (you can also get the full example):
pipeline(
'Source::iterate_files',
'Reader::by_line',
['Parser::by_format' => 'status:name'],
[
'Plumbing::alternatives',
pipeline(
sub { # filter only couples
my $record = shift;
return $record if $record->{structured}{status} eq 'couple';
return;
},
['Renderer::with_template_perlish' => $template_for_couples],
# we have to avoid falling in the "iterator" pitfall
{tap => 'bucket'},
),
# as a fallback, we render as singles
['Renderer::with_template_perlish' => $template_for_singles],
],
['Writer::to_files', interlude => "\n---\n"],
{tap => 'sink'},
)->([\$input]);It's very easy to use alternatives: just throw a bunch of tubes in the order you want them to be tried. If a tube does not return anything back, then it is skipped and the next one is tried; otherwise, its return value is considered good as the return value for alternatives too, and no more are tried.
In our example, the first tube is a compound tube with two sub-tubes: the first
is a simple selection function, that stops the record if it's not right (i.e.
if it's not related to a couple). Any record with status equal to single
will cause nothing to be returned, interrupting this alternative. Otherwise,
the rendering with $template_for_couples will be done, and the record will be
passed on.
You will notice that the tap for this pipeline is set to bucket. As you
might remember, a pipeline always returns an iterator, unless you set a
tap. In most of the case we set it to be a sink, because we don't care of the
output record; on the other hand, in this case we do care about it, so we
can't just throw it into the sink! An iterator would be bad too, because it
would mean that something is returned even though actually nothing should
be returned. With the bucket tap we can unroll the iterator and see what's
inside, an return accordingly (the bucket tap returns either nothing, or a
single record, or records followed by an anonymous array).
The second alternative always succeeds in our case; if it's not a couple, we'll just assume that we want them for our Saturday's party.
Alas, in addition to a lot of friends (and an incredibly small house!), we want to also give a party for our relatives on Friday. This can be done easily!
Let's consider the following example:
#!/usr/bin/env perl
# vim: sts=3 ts=3 sw=3 et ai :
use strict;
use warnings;
use Data::Dumper;
$Data::Dumper::Indent = 1;
use Data::Tubes qw< pipeline >;
use IO::Handle;
my $input = <<'END';
couple:Carla e Franco
single:Marcello
couple:Mario e Francesco
relative:Aunt Mirella
single:Federica
relative:Uncle Ferdinando
relative:Elisabetta e Giulio
acquaintance:Vladimiro
END
my %template_for = (
couple => <<'END',
Hi [% name %]!
We'll be giving a party on the coming Sunday! Would you be with us?
Yours,
S&F.
END
single => <<'END',
Hi [% name %]!
We'll be giving a party on the coming Saturday! Would you join us?
Cheers,
S&F
END
relative => <<'END',
Hi [% name %]!
We'll be giving a party on the coming Friday! Please come!
Cheers,
S&F
END
);
my @alternatives = map {
my $status = $_;
pipeline(
sub {
my $record = shift;
return $record
if $record->{structured}{status} eq $status;
return;
},
['Renderer::with_template_perlish' => $template_for{$status}],
{tap => 'bucket'},
);
} keys %template_for;
pipeline(
'Source::iterate_files',
'Reader::by_line',
['Parser::by_format' => 'status:name'],
[
'Plumbing::alternatives',
@alternatives,
sub {
my $record = shift;
my $line = $record->{source}{fh}->input_line_number();
warn "$record->{source}{name} line $line <$record->{raw}>:\n"
. " -> unknown status $record->{structured}{status}\n";
return; # nothing more
},
],
['Writer::to_files', interlude => "\n---\n"],
{tap => 'sink'},
)->([\$input]);You can see the output here, note the warning message at the end!
We did a bit of restructuring so that each sub-tube is generated starting from
a hash of metadata (mapping statuses and templates). In this way, each status
gets its filtering function and can be considered self-contained. We also
changed the fallback behaviour to printing a warning and stopping the record,
so nothing will be printed for the last input line about aquaintances. If we
just didn't care about printing a warning, we might just avoid putting a
fallback and nothing will be returned.
Apart from the restructuring, adding a new status relative was as easy as
adding the key/value pair in %template_for. You know what to do if you want
to host a party for aquaintances now!
What is it useful for? When you want to have independent tubes, i.e. tubes that carry their full logic (including the acceptance of a record), this is the right approach. This also lets you grow your tubes with additional behaviours in time, without the need to keep a dispatch table.
In this sense, this tool is quite similar to dispatch, in that both allow you to select one tube among alternatives. The main difference is that:
- dispatch relies on a selection function to figure out which tube to call for a given input record;
- alternatives tries all the options in sequence, until one succeeds.
The advantage of dispatch is that it only takes one step to figure out what to call next, while with alternatives you might go through several tubes until you find what's good for your record. On the other hand, it's much simpler to just throw an additional alternative (it's just one more tube) at the place you want, while using dispatch would require you to rething the selection logic.
As a rule of thumb, if you have a few alternatives that might grow in time, and you want to keep the full logic inside the tubes, go for alternatives especially if one of them is much more likely to be hit. On the other hand, if you have several possibilities and you want to minimize overheads related to trying them over and over, go for dispatch.