Начался треп^Wразговор про умирающий перл и это подтолкнуло меня
заняться небольшим исследованием вместо написания комментариев.

Moose клевый? ДА! Что там там клевого? РОЛИ, C3, ДЕКЛАРАТИВНОСТЬ!
Зачем все эти вкусности? НЕ ЗНАЕМ! Вы используете? НЕТ!

Тут встретился с проблемой абстракции и перегруженностью модуля
опциональным функционалом. Подумал и понял, что C3 - решение моей
проблемы.

Задача: есть коллекция с встроенным итератором next:

package MyApp::Collection;
use base 'MyApp::Base';

sub next {
    my $self = shift;

    if ( $self->{'i'} >= @{ $self->{'list'} } ) {
        $self->{'i'} = 0;
        return undef;
    } else {
        return $self->{'list'}[$self->{'i'}++];
    }
}

Так я и написал в test.pl. От нее наследуется пяток другой
непосредственных коллекций с различными типами объектов по которым
ходим "кругами". Парочке моделей нужно фильтровать коллекцию при
выдаче. Возникает дружный вопрос. А как это сделать?

Первое решение - ALL_IN_ONE:

package MyApp::Collection;
use base 'MyApp::Base';

sub filter { return 0 }

sub next {
    my $self = shift;

    my $res;
    do {
        if ( $self->{'i'} >= @{ $self->{'list'} } ) {
            $self->{'i'} = 0;
            return undef;
        } else {
            $res = $self->{'list'}[$self->{'i'}++];
        }
    } while $self->filter( $res );
    return $res;
}

Просто и аккуратно, но тут появляется задача не только фильтрации, но
и каких-то действий, например что-то типа map. Это только для примера,
но можно смыслить абстрактно. Логично делаем следующее:

sub wrap { return $_[1] }

sub next {
    ...
    return $self->wrap($res);
}

Клево, но с каждым таким изменением код все больше и сложнее. И кто
сказал, что мы всегда хотим wrap'нуть после фильтрации? Может нужно в
другом порядке?

Переходим к подклассам и линейному наследованию. Возвращаем начальную
реализацию коллекции и добавляем:

package MyApp::Collection::WithFilter;
use base 'MyApp::Collection';

sub filter { return 0 }

sub next {
    my $self = shift;
    my $res;
    AGAIN:
    $res = $self->SUPER::next(@_);
    goto AGAIN if defined $res && $self->filter( $res );
    return $res;
}

Где нужен фильтр, мы наследуем этот класс, получаем: Nodes isa
Collection::WithFilter isa Collection. Как сюда добавить WithWrap? А
никак. Во всяком случае проблему вариации последовательности фильтра и
обертки нам не решить без игры с @ISA и/или генерации мини классов.

Переходим к C3:

package MyApp::Collection::WithWrapper;
use Class::C3;

sub wrap { return $_[1] }

sub next {
    my $self = shift;
    my $res = $self->next::method(@_);
    return undef unless defined $res;
    return $self->wrap($res);
}

package MyApp::Nodes;
use base 'MyApp::Collection::WithWrapper',
'MyApp::Collection::WithFilter', 'MyApp::Collection';
use Class::C3;

sub filter { return $_[1] % 2 }
sub wrap { return "'$_[1]'" }

И оно работает. Основное отличие $self->next::method(@_); Нужен другой
порядок? Переставили классы местами.

Зачем все это? Код класса меньше, код имеет меньше условий, легче
следить и расширять. И нет нагрузки на объекты, где функционал не
используется. А велика ли нагрузка? А сколько мы выиграли? Проиграли?

Создаем класс без фильтра, обертки и тестируем. С реализацией "все в
одном" он проигрывает классу, где используется фильтрация. Почему?
Меньше вызовов метода wrap так как часть объектов отфильтрована
простым фильтром. Класс без наворотов с C3 в ~1.7 раз быстрее чем
all-in-one реализация.

Хм, а что с C3? А в C3 класс без фильтров и оберток получает
заслуженный прирост производительности, но класс со всеми наворотами
получает пенальти и становиться в 3-6 раз медленнее. О УЖОСС! Кто
сказал? Берем линейное наследование и стандартный вариант поиска
методов и получаем схожее замедление. Разница между C3 и стандартным
поиском методов остается только в ГИБКОСТИ решений. Хотя С3
проигрывает all-in-one в 3.44 раза.

Выводы: производительность в микро-тестах это конечно хорошо, клевые
циферки и прочее, но мы в реальном мире, решаем различные веселые
задачи каждый день, а для этого чаще нужны гибкие инструменты нежели
циферки. Больше циферок мы проигрываем в других местах и алгоритмах
более высокого уровня. Решил остановиться на all-in-one в моей задаче
так как объекты с фильтрами используются 80% времени. Если будет
наоборот или какая-то система с линейным наследованием потребует
больше гибкости, то я перейду на C3.

В атачах тесты которые вы можете запустить, поиграться, выкинуть и забыть :)

-- 
Best regards, Ruslan.
#!/usr/bin/perl

package MyApp::Base;

sub new {
    my $proto = shift;
    return bless { @_ }, ref($proto)||$proto
}

package MyApp::Collection;
use base 'MyApp::Base';

sub filter { return 0 }
sub wrap { return $_[1] }

sub next {
    my $self = shift;

    my $res;
    do {
        if ( $self->{'i'} >= @{ $self->{'list'} } ) {
            $self->{'i'} = 0;
            return undef;
        } else {
            $res = $self->{'list'}[$self->{'i'}++];
        }
    } while $self->filter( $res );
    return $self->wrap($res);
}

package MyApp::Nodes;
use base 'MyApp::Collection';

sub filter { return $_[1] % 2 }
sub wrap { return "'$_[1]'" }

package MyApp::Articles;
use base 'MyApp::Collection';

package main;

use Benchmark qw(timethis);

my $time = -10;

my $nodes = MyApp::Nodes->new( list => [1..100] );
while ( my $node = $nodes->next ) {
    print "$node, ";
}
print "\n";

timethis($time, sub {
    my $res = '';
    my $nodes = MyApp::Nodes->new( list => [1..100] );
    while ( my $node = $nodes->next ) {
        $res .= $node;
    }
} );

my $nodes = MyApp::Articles->new( list => [1..100] );
while ( my $node = $nodes->next ) {
    print "$node, ";
}
print "\n";

timethis($time, sub {
    my $res = '';
    my $nodes = MyApp::Articles->new( list => [1..100] );
    while ( my $node = $nodes->next ) {
        $res .= $node;
    }
} );

#!/usr/bin/perl

package MyApp::Base;

sub new {
    my $proto = shift;
    return bless { @_ }, ref($proto)||$proto
}

package MyApp::Collection;
use base 'MyApp::Base';

sub next {
    my $self = shift;

    if ( $self->{'i'} >= @{ $self->{'list'} } ) {
        $self->{'i'} = 0;
        return undef;
    } else {
        return $self->{'list'}[$self->{'i'}++];
    }
}

package MyApp::Collection::WithFilter;
use base 'MyApp::Collection';

sub filter { return 0 }

sub next {
    my $self = shift;
    my $res;
    AGAIN:
    $res = $self->SUPER::next(@_);
    goto AGAIN if defined $res && $self->filter( $res );
    return $res;
}

package MyApp::Collection::WithWrapper;
use base 'MyApp::Collection::WithFilter';

sub wrap { return $_[1] }

sub next {
    my $self = shift;
    my $res = $self->SUPER::next(@_);
    return defined $res? $self->wrap($res) : undef;
}

package MyApp::Nodes;
use base 'MyApp::Collection::WithWrapper';

sub filter { return $_[1] % 2 }

sub wrap { return "'$_[1]'" }

package MyApp::Articles;
use base 'MyApp::Collection';

package main;

use Benchmark qw(timethis);

my $time = -10;

my $nodes = MyApp::Nodes->new( list => [1..100] );
while ( my $node = $nodes->next ) {
    print "$node, ";
}
print "\n";

timethis($time, sub {
    my $res = '';
    my $nodes = MyApp::Nodes->new( list => [1..100] );
    while ( my $node = $nodes->next ) {
        $res .= $node;
    }
} );

my $nodes = MyApp::Articles->new( list => [1..100] );
while ( my $node = $nodes->next ) {
    print "$node, ";
}
print "\n";

timethis($time, sub {
    my $res = '';
    my $nodes = MyApp::Articles->new( list => [1..100] );
    while ( my $node = $nodes->next ) {
        $res .= $node;
    }
} );

#!/usr/bin/perl

package MyApp::Base;
use Class::C3;

sub new {
    my $proto = shift;
    return bless { @_ }, ref($proto)||$proto
}

package MyApp::Collection;
use base 'MyApp::Base';
use Class::C3;

sub next {
    my $self = shift;

    if ( $self->{'i'} >= @{ $self->{'list'} } ) {
        $self->{'i'} = 0;
        return undef;
    } else {
        return $self->{'list'}[$self->{'i'}++];
    }
}

package MyApp::Collection::WithFilter;
use Class::C3;

sub filter { return 0 }

sub next {
    my $self = shift;
    my $res;
    do {
        $res = $self->next::method(@_);
        return undef unless defined $res;
    } while $self->filter( $res );
    return $res;
}

package MyApp::Collection::WithWrapper;
use Class::C3;

sub wrap { return $_[1] }

sub next {
    my $self = shift;
    my $res = $self->next::method(@_);
    return undef unless defined $res;
    return $self->wrap($res);
}

package MyApp::Nodes;
use base 'MyApp::Collection::WithWrapper', 'MyApp::Collection::WithFilter', 'MyApp::Collection';
use Class::C3;

sub filter { return $_[1] % 2 }

sub wrap { return "'$_[1]'" }

package MyApp::Articles;
use base 'MyApp::Collection';
use Class::C3;

package main;

# initializez the C3 module
# (formerly called in INIT)
Class::C3::initialize();

use Benchmark qw(timethis);

my $time = -10;

my $nodes = MyApp::Nodes->new( list => [1..100] );
while ( my $node = $nodes->next ) {
    print "$node, ";
}
print "\n";

timethis($time, sub {
    my $res = '';
    my $nodes = MyApp::Nodes->new( list => [1..100] );
    while ( my $node = $nodes->next ) {
        $res .= $node;
    }
} );

my $nodes = MyApp::Articles->new( list => [1..100] );
while ( my $node = $nodes->next ) {
    print "$node, ";
}
print "\n";

timethis($time, sub {
    my $res = '';
    my $nodes = MyApp::Articles->new( list => [1..100] );
    while ( my $node = $nodes->next ) {
        $res .= $node;
    }
} );

--
Moscow.pm mailing list
[email protected] | http://moscow.pm.org

Ответить