Encode::Mapper(3pm)

πŸ“¦
version 14.2 Β· 3 versions
, in ubuntu noble, ubuntu resolute, ubuntu stonking

πŸ¦‹ Encode::Mapper πŸ’…

okay bestie, sit down, hydrate, and let me tell you about Encode::Mapper(3pm) β€” it's a Rewrite rules compiler and interpreter, and it is genuinely so much cooler than the name makes it sound. This is a user-contributed Perl doc, and it's giving "the author was a math gremlin and I respect it" energy. Let's GO. ✨


✨ SYNOPSIS ✨

use Encode::Mapper;     ############################################# Enjoy the ride ^^

use Encode::Mapper ':others', ':silent';    # syntactic sugar for compiler options ..

Encode::Mapper->options (                   # .. equivalent, see more in the text
        'others' => sub { shift },
        'silent' => 1,
    );

Encode::Mapper->options (                   # .. resetting, but not to use 'use' !!!
        'others' => undef,
        'silent' => 0
    );

## Types of rules for mapping the data and controlling the engine's configuration #####

@rules = (
    'x',            'y',            # single 'x' be 'y', unless greediness prefers ..
    'xx',           'Y',            # .. double 'x' be 'Y' or other rules

    'uc(x)x',       sub { 'sorry ;)' },     # if 'x' follows 'uc(x)', be sorry, else ..

    'uc(x)',        [ '', 'X' ],            # .. alias this *engine-initial* string
    'xuc(x)',       [ '', 'xX' ],           # likewise, alias for the 'x' prefix

    'Xxx',          [ sub { $i++; '' }, 'X' ],      # count the still married 'x'
);

## Constructors of the engine, i.e. one Encode::Mapper instance #######################

$mapper = Encode::Mapper->compile( @rules );        # engine constructor
$mapper = Encode::Mapper->new( @rules );            # equivalent alias

## Elementary performance of the engine ###############################################

@source = ( 'x', 'xx', 'xxuc(x)', 'xxx', '', 'xx' );    # distribution of the data ..
$source = join '', @source;                             # .. is ignored in this sense

@result = ($mapper->process(@source), $mapper->recover());  # the mapping procedure
@result = ($mapper->process($source), $mapper->recover());  # completely equivalent

$result = join '', map { ref $_ eq 'CODE' ? $_->() : $_ } @result;

    # maps 'xxxxxuc(x)xxxxx' into ( 'Y', 'Y', '', 'y', CODE(...), CODE(...), 'y' ), ..
    # .. then converts it into 'YYyy', setting $i == 2

@follow = $mapper->compute(@source);    # follow the engine's computation over @source
$dumper = $mapper->dumper();            # returns the engine as a Data::Dumper object

## Module's higher API implemented for convenience ####################################

$encoder = [ $mapper, Encode::Mapper->compile( ... ), ... ];    # reference to mappers
$result = Encode::Mapper->encode($source, $encoder, 'utf8');    # encode down to 'utf8'

$decoder = [ $mapper, Encode::Mapper->compile( ... ), ... ];    # reference to mappers
$result = Encode::Mapper->decode($source, $decoder, 'utf8');    # decode up from 'utf8'

πŸŽ€ ABSTRACT πŸŽ€

Encode::Mapper serves for intuitive, yet efficient construction of mappings for Encode.
The module finds direct application in Encode::Arabic. It provides an object-oriented
programming interface to convert data consistently, follow the engine's computation,
dump the engine using Data::Dumper, etc.

πŸ’œ DESCRIPTION πŸ’œ

ngl, the author of this extension ... ;) lowkey preferred giving formal and terse examples over writing English, and honestly? Respect. But if you want the real-world tea, go look at Encode::Arabic, where Encode::Mapper gets used to solve complex real-world problems. That's where it pops off. πŸ’…

🌈 INTRO AND RULE TYPES 🌈

The core of this module is an algorithm that takes the rules you give it and builds a finite-state transducer β€” aka an "engine" β€” which does greedy search through your input stream and produces output data plus side effects that line up with the search results. And the slay part? Transducers can be linked one to another, so you can stack them into multi-level devices for nontrivial encoding/decoding tasks. Main character energy, fr fr. πŸ‘‘

So the rules are basically saying "search for this input sequence of bytes, and when you see it, do this." If the left-hand side (LHS) of a rule is the longest left-most string out of all the ones that apply on the input, then the right-hand side (RHS) of that rule gets evaluated. The RHS gives you the output string, and it can also control the engine β€” as if that extra text were prepended before the rest of the input:

$A => $X            # $A .. literal string
                    # $X .. literal string or subroutine reference
$A => [$X, $Y]      # $Y .. literal string for which 'length $Y < length $A'

The order of the rules does not matter β€” EXCEPT when several rules share the same LHS. In that case a redefinition warning is usually issued before the RHS gets overridden. So like, heads up bestie. πŸ™ˆ


🧚 LOW-LEVEL METHODS 🧚

compile ($class, @rules)

compile ($class, $opts, @rules)

This is the constructor of an Encode::Mapper instance. First argument = the name of the class, everything after = the list of rules ... LHS odd elements, RHS even elements β€” that is, unless the first element is a reference to an array or a hash, in which case it becomes $opts. 🫢

If $opts is recognized, it's used to modify the compiler "options" locally, just for the engine being constructed. And if an option isn't overridden? Then its global setting holds. Simple as that.

Now, the compilation algorithm and the search algorithm itself were inspired by the Aho-Corasick and Boyer-Moore algorithms, plus studies of finite automata with the restart operation. The engine is implemented in the classical sense β€” using hashes for the transition function, for instance. The plan is to improve this to Perl code evaluation if the speed-up turns out to be significant.

There's also a whole "let's see how Perl's regular expressions would cope with the task" situation going on β€” i.e. verifying the initial doubts that stopped them from trying in the first place. Since Encode::Mapper's functionality is much richer than pure search, simulating it completely might be resource-expensive and non-elegant. So yeah, experiment reports are welcome. ✨

new ($class, @list)

Just a name alias to the compile constructor. Same vibe, different name. πŸŽ€

process ($obj, @list)

This processes the input list with the engine. There is no resetting within the call of the method. Internally, the text in the list is split into bytes, so you genuinely don't need to join your strings or lines of data yourself. And because the author loves you, here are the unveiled properties of the Encode::Mapper class too:

sub process ($@) {          # returns the list of search results performed by Mapper
    my $obj = shift @_;
    my (@returns, $phrase, $token, $q);

    use bytes;              # ensures splitting into one-byte tokens

    $q = $obj->{'current'};

    foreach $phrase (@_) {
        foreach $token (split //, $phrase) {
            until (defined $obj->{'tree'}[$q]->{$token}) {
                push @returns, @{$obj->{'bell'}[$q]};
                $q = $obj->{'skip'}[$q];
            }
            $q = $obj->{'tree'}[$q]->{$token};
        }
    }

    $obj->{'current'} = $q;

    return @returns;
}

recover ($obj, $r, $q)

Okay so, the search algorithm is greedy and the engine has no idea when the end of the data arrives, so there has to be a way to tell it. Normally you call recover on the object without those other two optional parameters β€” they set the initial and the final state, respectively.

sub recover ($;$$) {        # returns the 'in-progress' search result and resets Mapper
    my ($obj, $r, $q) = @_;
    my (@returns);

    $q = $obj->{'current'} unless defined $q;

    until ($q == 0) {
        push @returns, @{$obj->{'bell'}[$q]};
        $q = $obj->{'skip'}[$q];
    }

    $obj->{'current'} = defined $r ? $r : 0;

    return @returns;
}

compute ($obj, @list)

This one tracks down the computation over the list of data, resetting the engine to its initial state before and after. Developers might like this ;) β€” and honestly, devs, you will. πŸ’–

local $\ = "\n";    local $, = ":\t";           # just define the display

foreach $result ($mapper->compute($source)) {   # follow the computation

    print "Token"   ,   $result->[0];
    print "Source"  ,   $result->[1];
    print "Output"  ,   join " + ", @{$result->[2]};
    print "Target"  ,   $result->[3];
    print "Bell"    ,   join ", ", @{$result->[4]};
    print "Skip"    ,   $result->[5];
}

dumper ($obj, $ref)

Individual instances of Encode::Mapper can be stored as revertible data structures. And for minimalistic reasons, the dumping needs to include explicit short-identifier references to the empty array and the empty hash of the engine. For details, go read Data::Dumper. 🌸

sub dumper ($;$) {
    my ($obj, $ref) = @_;

    $ref = ['L', 'H', 'mapper'] unless defined $ref;

    require Data::Dumper;

    return Data::Dumper->new([$obj->{'null'}{'list'}, $obj->{'null'}{'hash'}, $obj], $ref);
}

describe ($obj, $ref)

Describes the Encode::Mapper object and returns a hash of the characteristics. If $ref is defined, the info also gets printed into the $referenced stream β€” or to STDERR if $ref is not a filehandle. πŸ¦‹


πŸ‘‘ HIGH-LEVEL METHODS πŸ‘‘

In the Encode world, you can work with different encodings, and you're also handed a function that tells you whether the data is in Perl's internal utf8 format or not. In the Encode::Mapper business, you're encouraged to compile different mappers and stack them on top of each other, which gets you an easy-to-work-with filtering device. Stacked. Slay. πŸ’…

And together? This module offers the following encode and decode methods. In their prototypes, $encoder/$decoder represent merely a reference to an array of mappers β€” although mathematics might do more than that in future implementations ;)

⚠️ Heads up bestie: currently, the mappers involved are not reset with recover before the computation. See the --join option for more comments on the code:
foreach $mapper (@{$_[2]}) {    # either $encoder or $decoder
    $join = defined $mapper->{'join'} ? $mapper->{'join'} :
            defined $option{'join'} ? $option{'join'} : "";
    $text = join $join, map {
                UNIVERSAL::isa($_, 'CODE') ? $_->() : $_
            } $mapper->process($text), $mapper->recover();
}

encode ($class, $text, $encoder, $enc)

If $enc is defined, the $text is encoded into that encoding, using Encode. Then the $encoder's engines get applied in series on the data. The returned text should have the utf8 flag off. πŸ’–

decode ($class, $text, $decoder, $enc)

okay bestie, here's the decode method: you feed it $text, and it gets run through the sequence of engines in $decoder. If the result doesn't have the utf8 flag on, then Encode does an extra decoding pass using $enc. And if $enc is not defined? utf8 is assumed. πŸ’–


✨ OPTIONS AND EXPORT ✨

The language that the Encode::Mapper engine works on isn't just determined by the rules you pass as parameters to the "compile" or "new" constructor methods. The vibe of the compilation is also influenced by the current settings of these options:

  • --complement β€” bestie, this one takes a reference to an array of rules that complement the rules from the constructor. Redefinition warnings? They only pop off if you redefine something inside this option's list, not when a rule gets overridden during compilation. Slay. πŸ’…
  • --override β€” this overrides the constructor's rules. But heads up: redefinition warnings will be issued. Say you wanna preserve all XML markup in the data you're about to process through your encoders/decoders β€” iconic move, honestly:
'override' => [             # override rules of these LHS .. there's no other tricks ^^

        (                   # combinations of '<' and '>' with the other bytes
            map {

                my $x = chr $_;

                "<" . $x, [ "<" . $x, ">" ],    # propagate the '>' sign implying ..
                ">" . $x, [ $x, ">" ],          # .. preservation of the bytes

            } 0x00..0x3B, 0x3D, 0x3F..0xFF
        ),

            ">>",           ">",                # stop the whole process ..
            "<>",           "<>",               # .. do not even start it

            "><",           [ "<", ">" ],       # rather than nested '<' and '>', ..
            "<<",           [ "<<", ">" ],

            ">\\<",         [ "<", ">" ],       # .. prefer these escape sequences
            ">\\\\",        [ "\\", ">" ],
            ">\\>",         [ ">", ">" ],

            ">",            ">",                # singular symbols may migrate right ..
            "<",            "<",                # .. or preserve the rest of the data
    ]
  • --others β€” if it's defined, this controls how to deal with 'others' (aka bytes of input that have no rule) by defining rules for them. If this option gets a code reference, that subroutine gets called with the 'other' LHS parameter to get the rule's RHS. Otherwise, a defined scalar value becomes the RHS of each 'other' LHS.

To preserve the 'other' bytes, you can use:

'others' => sub { shift }   # preserve every non-treated byte

which is basically the same effect as including the "map" in the --complement rules:

'complement' => [ ( map { ( chr $_ ) x 2 } 0x00..0xFF ), ... ]  # ... is your rules

And you might wanna return undefined values if there are any non-treated bytes in the input. But for "undef" to be a correct RHS, you have to protect it once more with a "sub" like this:

'others' => sub { sub { undef } }
  • --silent β€” set it to a true value and you'll prevent any warnings during the engine's compilation. Those warnings mostly mean you did something incorrect or dubious with a rule. πŸ™ˆ
  • --join β€” this option enables a less memory-hungry representation of the engines. If it's defined when the constructor is called, the setting is stored internally in the instance. Any lists of literal RHS that are meant to be emitted simultaneously from the engine get joined into a string using the option's value, and empty lists turn into empty strings. If an engine was compiled with this option defined, that value will also be used to join the output of "encode" and "decode". If not, either the current value of the option or the empty string will help instead. 🫢

The option keywords can be in mixed case and/or start with any number of dashes, and the next element in the list is taken as the option's value. But there are special keywords that begin with a colon and do not gulp down the next element:

  • :others β€” equivalent to the code 'others' => sub { shift } we explained above. ✨
  • :silent β€” equivalent to 'silent' => 1, or rather to the maximum silence if more degrees of it get introduced in the future. πŸ’€
  • :join β€” equivalent to 'join' => ''. Use this if you're gonna dump and load the new engine often, and if you don't miss the list-supporting uniformity of "process" and "recover". πŸ’…

Compiler options are associated with package names in the %Encode::Mapper::options variable, and they're confined to those packages. While "options" and "import" perform the setting with respect to the caller package, accessing the hash directly is neither recommended nor restricted. You do you, bestie. 😌

There's a nice compile-time invocation of "import" with the use Encode::Mapper LIST idiom, which you might prefer over explicit method calls. And if you wanna locally modify the package's global setting just for the engine being constructed, you supply the options as an extra parameter to "compile". So easy. πŸŽ€

use Data::Dump 'dump';                  # pretty data printing is below

$Encode::Mapper::options{'ByForce'} = { qw ':others - silent errors' };

package ByMethod;                       # import called at compile time
                                        # no warnings, 'silent' is true
Encode::Mapper->options('complement' => [ 'X', 'Y' ], 'others' => 'X');
use Encode::Mapper 'silent' => 299_792_458;

package main;                           # import called at compile time
                                        # 'non-existent' may exist once
print dump %Encode::Mapper::options;
use Encode::Mapper ':others', ':silent', 'non-existent', 'one';

# (
#   "ByMethod",
#   { complement => ["X", "Y"], others => "X", silent => 299_792_458 },
#   "ByForce",
#   { ":others" => "-", silent => "errors" },
#   "main",
#   { "non-existent" => "one", others => sub { "???" }, silent => 1 },
# )
  • options ($class, @list) β€” if $class is defined, this enforces the options in the list globally for the calling package. The return value is the state of the options before the proposed changes were set. If $class is undefined, nothing gets set β€” only the canonized forms of the declared keywords and their values are returned. πŸ”₯
  • import ($class, @list) β€” this module does not export any symbols. This method just calls "options", as long as there are some elements in the list. πŸ‘‘

🌈 SEE ALSO 🌈

There are related theoretical studies that this implementation may have touched. You might be interested in Aho-Corasick and Boyer-Moore algorithms, plus finite automata with the restart operation. Big brain energy. 🧠

  • Encode
  • Encode::Arabic
  • Data::Dumper

And this paper: Encode Arabic: Exercise in Functional Parsing β€” <http://ufal.mff.cuni.cz/padt/online/2006/06/encode-arabic.html> πŸ“š


πŸ’– AUTHOR πŸ’–

Otakar Smrz <otakar-smrz users.sf.net>, <http://otakar-smrz.users.sf.net/> β€” iconic. πŸ‘‘


Copyright (C) 2003-2014 Otakar Smrz

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. So go off, bestie. πŸ’…


This page came from perl v5.36.0, dated 2022-10-16, and it's Encode::Mapper(3pm) β€” k bye! πŸ¦‹βœ¨

πŸ“¦ where this page comes from