Сделать пакет как функциональным, так и ОО - PullRequest
0 голосов
/ 11 октября 2018

Я видел модули CPAN Perl, которые можно использовать функционально или OO.Я обычно пишу OO и функциональные пакеты в зависимости от того, что мне нужно, но я все еще не умею писать модули, которые могут использоваться обоими способами.

Может кто-нибудь дать мне простой пример пакета, который можно использовать в функционалеи / или ОО способ?Я очевидно заинтересован в частях, которые позволяют использовать пакет в обоих направлениях.

Спасибо

Ответы [ 3 ]

0 голосов
/ 11 октября 2018

Пример ядра: File :: Spec , который имеет обертку File :: Spec :: Functions .Он не столько объектно-ориентирован, но использует объектно-ориентированный принцип наследования, поэтому его основной API использует вызовы методов, но ему не нужно сохранять какое-либо состояние.

use strict;
use warnings;
use File::Spec;
use File::Spec::Functions 'catfile';

print File::Spec->catfile('/', 'foo', 'bar');
print catfile '/', 'foo', 'bar';

Другой пример - Sereal , чей кодер и декодер можно использовать как в качестве объектов, так и через экспортируемые функции, которые обертывают их.

use strict;
use warnings;
use Sereal::Encoder 'encode_sereal';

my $data = {foo => 'bar'};

my $encoded = Sereal::Encoder->new->encode($data);
my $encoded = encode_sereal $data;

Помимо Sereal, обычно хорошей организационной практикой является сохранение классов объектов и модулей экспорта.отдельный.Особенно не пытайтесь сделать так, чтобы одна и та же функция вызывалась как метод или экспортируемая функция;основная проблема заключается в том, что она не отличается от самой подпрограммы, независимо от того, была ли она названа $obj->function('foo') или function($obj, 'foo').Как заметил @choroba, CGI.pm пытается это сделать, и это беспорядок.

0 голосов
/ 11 октября 2018

Как уже говорилось, есть много модулей, которые делают это, и некоторые из них были названы.Хорошей практикой является написание отдельного модуля для функционального интерфейса, который use присваивает классу и экспортирует его (выборочные) функции как таковые.

Но возможно иметь оба интерфейса в одном пакете с одинаковыми именами методов / функций, если в этом есть особая необходимость.См. Раздел в конце для одного очень конкретного и редкого варианта использования, который не будет обработан в следующем базовом примере, и как его решить.

Вот базовый пакет, которыйимеет оба интерфейса

package Duplicious;  # having interfaces to two paradigms may be confusing

use warnings;
use strict;
use feature 'say';

use Scalar::Util qw(blessed);    
use Exporter qw(import);

our @EXPORT_OK = qw(f1);

my $obj_cache;  # so repeated function calls don't run constructor

sub new {
    my ($class, %args) = @_; 
    return bless { }, $class;
}

sub f1 {
    say "\targs in f1: ", join ', ', @_;  # see how we are called

    my $self = shift;
    # Functional interface
    # (first argument not object or class name in this or derived class)
    if ( not ( (blessed($self) and $self->isa(__PACKAGE__)) 
            or (not ref $self  and $self->isa(__PACKAGE__)) ) )
    { 
        return ($obj_cache || __PACKAGE__->new)->f1($self, @_);
    }   

    # Now method definition goes
    # ...
    return 23;
}

1;

Абонент

use warnings;            # DEMO only --
use strict;              # Please don't mix uses in the same program
use feature 'say';

use Duplicious qw(f1);

my $obj = Duplicious->new;

say "Call as class method: ";
Duplicious->f1("called as class method");

say "Call as method:";
my $ret_meth = $obj->f1({}, "called as method");

say "\nCall as function:";
my $ret_func = f1({}, "called as function");

В принципе неудобно использовать Exporter в модуле, который определяет класс (но я не знаю ни одного фактическогопроблемы с этим);это приводит к потенциально запутанному интерфейсу.Это само по себе является хорошей причиной для разделения интерфейсов, так что функциональный должен загрузить определенный модуль.

Есть также одна деталь, которая требует внимания.Вызов метода

($obj_cache || __PACKAGE__->new)->f1(...)

использует кешированный $obj_cache (если этот подпрограмма уже был вызван) для выполнения вызова.Таким образом, сохраняется состояние объекта, которое могло или не было изменено в предыдущих вызовах f1.

Это довольно нетривиально в вызове, предназначенном для использования в не объектно-ориентированном контексте, и его следует тщательно изучить.Если есть проблемы с ним, либо отбросьте это кэширование, либо разверните его в полный оператор if, где состояние может быть сброшено при необходимости.

Эти два использования должны абсолютно не смешиваться в одной и той же программе.

Вывод

Call as class method: 
    args in f1: Duplicious, called as class method
Call as method:
    args in f1: Duplicious=HASH(0x21b1b48), HASH(0x21a8738), called as method
Call as function:
    args in f1: HASH(0x21a8720), called as function
    args in f1: Duplicious=HASH(0x218ba68), HASH(0x21a8720), called as function

Вызов функции отправляется методу, таким образомдве строки (обратите внимание на аргументы).


Для тестирования с производным классом я использую минимальный

package NextDupl;

use warnings;
use strict;
use feature 'say';

use parent 'Duplicious';

1;

и добавляю к основной программе выше следующее

# Test with a subclass (derived, inherited class)
my $inh = NextDupl->new;

say "\nCall as method of derived class";
$inh->f1("called as method of derived class");

# Retrieve with UNIVERSAL::can() from parent to use by subclass    
my $rc_orig = Duplicious->can('f1');

say "\nCall via coderef pulled from parent, by derived class";
NextDupl->$rc_orig("called via coderef of parent by derived class");

Дополнительный вывод:

Call as method of derived class
    args in f1: NextDupl=HASH(0x11ac720), called as method of derived class

Call via coderef pulled from parent, by derived class
    args in f1: NextDupl, called via coderef of parent by derived clas

Это включает в себя тест с использованием UNIVERSAL::can, как он появился в комментарии.


Существует одно конкретное ограничение (которое яосведомлен о), поднят и обсужден в комментариях.

Представьте, что мы пишем метод, который принимает объект (или имя класса) в качестве первого аргумента, чтобы его вызывали как ->func($obj);далее - и это важно - этот метод допускает любой класс, так как он работает таким образом, что ему все равно, какой у него класс.Это было бы очень конкретно, но это возможно, и это поднимает следующую проблему.

Вызов функции, соответствующий этому методу, будет func($obj), и когда $obj окажется в иерархии этого класса, что приведет к неправильному вызову метода ->func(), .

Нет способа устранить неоднозначность в коде, который решает, вызывается ли он как функция или как метод, поскольку все, что он делает, это смотрит на первый аргумент.Если это объект / класс в нашей собственной иерархии, он решает, что это был вызов метода для этого объекта (или вызов метода класса), и в данном конкретном случае это неправильно.

Есть два простых способа:и, возможно, еще один, для автора модуля, чтобы решить это

  • Не предоставлять функциональный интерфейс для этого весьма специфического метода

  • Дайте емуотдельное (явно связанное) имя

  • Условие if, которое определяет способ вызова, проверяя первый аргумент, является постоянным, но все же записывается для каждого метода, имеющего этот интерфейс.Поэтому в этом методе проверьте еще один аргумент: если первый - это объект / класс этого класса и , то следующий - (любой) объект / класс, тогда это вызов метода. Это не работает, если второй аргумент является необязательным.

Все это совершенно разумно.В классе, который использует свою определяющую черту - иметь и использовать данные («атрибуты»), скорее всего, будут методы, которые нельзя преобразовать в вызовы функций.Это связано с тем, что одна программа должна использовать только один интерфейс, а у функций нет состояния, поэтому методы, полагающиеся на это, не будут работать.(Использование кэшированного объекта для этого крайне коварно.)

Так что всегда нужно будет тщательно определиться с интерфейсом и выбирать и выбирать.

Благодаря Grinnz для комментариев.


Обратите внимание, что существует совершенно другая парадигма «функционального программирования», и название оставляет это немного неясным.Все это о функциональном интерфейсе в процедурном подходе.

0 голосов
/ 11 октября 2018

Мой WiringPi :: API дистрибутив написан таким образом.Обратите внимание, что в данном случае сохранение состояния не требуется, поэтому, если сохранение состояния необходимо, этот способ не будет работать как есть.

Вы можете использовать его функционально:

use WiringPi::API qw(:all)

setup_gpio();
...

Или используйте его объектно-ориентированный интерфейс:

use WiringPi::API;

my $api = WiringPi::API->new;
$api->setup_gpio();
...

Для работы я использую @EXPORT_OK, чтобы пространство имен пользователя не было излишне загрязнено:

our @EXPORT_OK;

@EXPORT_OK = (@wpi_c_functions, @wpi_perl_functions);
our %EXPORT_TAGS;

$EXPORT_TAGS{wiringPi} = [@wpi_c_functions];
$EXPORT_TAGS{perl} = [@wpi_perl_functions];
$EXPORT_TAGS{all} = [@wpi_c_functions, @wpi_perl_functions];

... и несколько примеров функций / методов.По сути, мы проверяем количество входящих параметров, и, если есть дополнительный (который будет классом / объектом), мы вручную просто shift выводим его:

sub serial_open {
    shift if @_ > 2;
    my ($dev_ptr, $baud) = @_;
    my $fd = serialOpen($dev_ptr, $baud);
    die "could not open serial device $dev_ptr\n" if $fd == -1;
    return $fd;
}
sub serial_close {
    shift if @_ > 1;
    my ($fd) = @_;
    serialClose($fd);
}
sub serial_flush {
    shift if @_ > 1;
    my ($fd) = @_;
    serialFlush($fd);
}

Как правило, я бы сделал несколькопроверка параметров, чтобы убедиться, что мы отказываемся от правильной вещи, но при тестировании это было быстрее, чтобы позволить внутреннему C / XS-коду беспокоиться об этом за меня.

...