Marlin 소개

발행: (2025년 12월 19일 오전 06:44 GMT+9)
9 min read
원문: Dev.to

Source: Dev.to

Introducing Marlin 소개 커버 이미지

Toby Inkster

소개

솔직히 말해서, 아마도 아닐 겁니다.
하지만 Marlin을 시도해 볼 만한 이유는 다음과 같습니다.

  • 대부분의 생성자와 접근자는 XS로 구현되어 매우, 매우 빠릅니다.
  • “속성은 보통 읽기 전용이어야 한다”와 같은 몇 가지 기본 원칙을 받아들인다면, 클래스와 그 속성을 선언하는 것이 정말, 정말 간결해집니다.

예시

use v5.20.0;
use experimental qw(signatures);

# Import useful constants, types, etc.
use Marlin::Util -all, -lexical;
use Types::Common -all, -lexical;

package Person {
    use Marlin
      'given_name!'   => NonEmptyStr,
      'family_name!'  => NonEmptyStr,
      'name_style'    => { enum => [qw/western eastern/], default => 'western' },
      'full_name'     => { is => lazy, builder => true },
      'birth_date?';

    sub _build_full_name ( $self ) {
        return sprintf( '%s %s', uc($self->family_name), $self->given_name )
          if $self->name_style eq 'eastern';
        return sprintf( '%s %s', $self->given_name, $self->family_name );
    }
}

package Payable {
    use Marlin::Role
      -requires => [ 'bank_details' ];

    sub make_payment ( $self ) {
        ...;
    }
}

package Employee {
    use Marlin
      -extends => [ 'Person' ],
      -with    => [ 'Payable' ],
      'bank_details!' => HashRef,
      'employee_id!'  => Int,
      'manager?'      => { isa => 'Employee' };
}

my $manager = Employee->new(
    given_name   => 'Simon',
    family_name  => 'Lee',
    name_style   => 'eastern',
    employee_id  => 1,
    bank_details => {},
);

my $staff = Employee->new(
    given_name   => 'Lea',
    family_name  => 'Simons',
    employee_id  => 2,
    bank_details => {},
    manager      => $manager,
);

printf(
    "%s's manager is %s.\n",
    $staff->full_name,
    $staff->manager->full_name,
) if $staff->has_manager;

눈에 띄는 몇 가지 사항

  • Moose의 대부분 기능을 지원합니다… 혹은 실제로 사용하는 대부분의 기능을 지원합니다.
  • use Marlin 라인에 속성 이름을 적는 것만으로도 속성을 선언할 수 있습니다.
  • 옵션을 뒤에 붙일 수 있지만, Marlin의 기본값(읽기 전용 속성)으로 만족한다면 옵션이 필요 없습니다.
  • ! 를 사용하면 { required => true } 대신 속성을 빠르게 필수로 표시할 수 있습니다.
  • ? 를 사용하면 { predicate => true } 대신 프레디케이트 메서드를 요청할 수 있습니다.

벤치마크

초기 벤치마크 결과 Marlin이 빠르다는 것을 보여줍니다.

생성자

         Rate   Tiny  Plain    Moo  Moose Marlin   Core
Tiny   1317/s     --    -2%   -48%   -53%   -54%   -72%
Plain  1340/s     2%     --   -47%   -53%   -53%   -72%
Moo    2527/s    92%    89%     --   -11%   -12%   -47%
Moose  2828/s   115%   111%    12%     --    -2%   -40%
Marlin 2873/s   118%   114%    14%     2%     --   -39%
Core   4727/s   259%   253%    87%    67%    65%     --

새로운 Perl core class 키워드만이 Marlin보다 빠른 생성자를 만들며, 그 차이는 상당히 큽니다. 하지만 객체 생성은 여러분이 필요로 할 작업의 일부에 불과합니다.

접근자

          Rate   Tiny  Moose  Plain   Core    Moo Marlin
Tiny   17345/s     --    -1%    -3%    -7%   -36%   -45%
Moose  17602/s     1%     --    -2%    -6%   -35%   -44%
Plain  17893/s     3%     2%     --    -4%   -34%   -44%
Core   18732/s     8%     6%     5%     --   -31%   -41%
Moo    27226/s    57%    55%    52%    45%     --   -14%
Marlin 31688/s    83%    80%    77%    69%    16%     --

여기서 “접근자”란 단순히 표준 getter와 setter뿐 아니라 predicate 메서드와 clearers까지 포함합니다. Marlin과 Moo는 가능한 경우 Class::XSAccessor를 사용해 다른 구현체보다 크게 앞서 있습니다. Marlin은 또한 부모 클래스 메서드에 대한 별칭을 자식 클래스의 심볼 테이블에 직접 생성함으로써 Perl이 일반적인 메서드 해석 오버헤드를 많이 생략하도록 합니다.

class가 더 나은 성능을 보일 것으로 기대했지만, 현재 해당 키워드의 reader와 writer는 순수 Perl로 구현되어 있어 향후 릴리스에서 개선 여지가 있습니다.

네이티브 트레이트 / Handles‑Via / 위임

         Rate   Tiny   Core  Plain  Moose    Moo Marlin
Tiny    675/s     --   -56%   -57%   -59%   -61%   -61%
Core   1518/s   125%     --    -4%    -8%   -13%   -13%
Plain  1581/s   134%     4%     --    -4%   -9%   -10%
Moose  1642/s   143%     8%     4%     --    -5%    -6%
Moo    1736/s   157%    14%    10%     6%     --    -1%
Marlin 1752/s   160%    15%    11%     7%     1%     --

네이티브 트레이트가 무엇인지 모른다면, 다음과 같은 작은 헬퍼 메서드를 만들 수 있는 능력을 말합니다:

sub add_language ( $self, $lang ) {
    push $self->languages->@*, $lang;
}

속성 정의의 일부로 사용하기

use Marlin
  languages => {
    is  => 'ro',
    isa => ArrayRef[Str],
    default => sub { [] },
    handles => {
        add_language => 'push',
        all_languages => 'elements',
    },
  };

Source:

코드 예시

=> ArrayRef[Str],
   default      => [],
   handles_via  => 'Array',
   handles      => { add_language => 'push', count_languages => 'count' },
};

대부분의 경우 성능 차이는 크게 나지 않지만 Marlin조금 앞섭니다. Marlin과 Moose는 확장 모듈 없이도 기본적으로 이 기능을 제공하는 유일한 프레임워크입니다.

Note: default => []는 의도된 설정입니다. 빈 배열 레퍼런스나 해시 레퍼런스를 기본값으로 지정할 수 있으며, Marlin은 이를 default => sub { [] }와 동일하게 처리합니다. 코드를 호출하는(느린) 대신 XS에서 새로운 빈 배열에 대한 레퍼런스를 빠르게 생성해 줍니다!

결합 벤치마크

RateTinyPlainCoreMooseMooMarlin
Tiny545/s-48%-56%-58%-60%-64%
Plain1051/s93%-16%-19%-22%-31%
Core1249/s129%19%-4%-8%-18%
Moose1304/s139%24%4%-4%-14%
Moo1355/s148%29%8%4%-11%
Marlin1519/s179%45%22%17%12%

객체를 생성하고 여러 접근자와 위임을 호출하는 현실적인 코드 스니펫에서도 Marlin은 매우 좋은 성능을 보여줍니다.

Lexical Accessors and Private Attributes

Marlin은 렉시컬 메서드에 대한 일류 지원을 제공합니다!

use v5.42.0;

package Widget {
    use Marlin
        name        => { isa => Str },
        internal_id => { reader => 'my internal_id', storage => 'PRIVATE' };

    ...

    printf "%d: %s\n", $w->&internal_id, $w->name;
}

# dies because internal_id is lexically scoped
Widget->new->&internal_id;
  • ->& 연산자는 Perl 5.42에서 추가되었습니다. 오래된 Perl(5.12 이상)에서도 렉시컬 메서드를 지원하지만 함수 호출 구문으로 호출해야 합니다. 예: internal_id($w).
  • storage => "PRIVATE"은 Marlin에게 해당 속성에 대해 인사이드‑아웃 저장 방식을 사용하도록 지시합니다. 따라서 $obj->{internal_id}와 같이 직접 접근할 수 없습니다. 이는 진정한 프라이빗 속성을 제공합니다.
  • Perl 5.18 이상에서는 일반적인 my sub foo 구문으로도 렉시컬 메서드를 선언할 수 있으며, 이를 통해 프라이빗 속성뿐 아니라 프라이빗 메서드도 사용할 수 있습니다.

Constant Attributes

package Person {
    use Marlin
        name         => { isa => Str, required => true },
        species_name => { isa => Str, constant => "Homo sapiens" };
}
  • 상수 속성은 일반 속성처럼 선언되지만 항상 읽기 전용이며 생성자에 전달할 수 없습니다.
  • 위임을 지원하지만, 위임된 메서드가 값을 변경하려고 하지 않을 경우에만 가능합니다.

Perl 버전 지원

일부 렉시컬 기능은 최신 Perl 버전을 필요로 하지만, Marlin은 Perl 5.8.8 이상에서 실행됩니다.

향후 방향

제가 생각한 몇 가지 아이디어:

  • Moose가 로드된 경우, Moo가 하는 것과 유사하게 Marlin 클래스와 역할에 대한 메타 객체 프로토콜을 생성합니다.
Back to Blog

관련 글

더 보기 »