Raku의 패키지(package)와 이름 조회

Raku의 패키지(package)와 이름 조회

패키지는 이름 붙은 프로그램 요소들의 중첩된 네임스페이스(namespace)예요. 모듈, 클래스, 문법(grammar) 등이 패키지의 한 종류죠. 디렉터리 안의 파일처럼, 로컬에 있으면 짧은 이름으로 부를 수 있고, 스코프가 허락하는 한 네임스페이스를 포함한 긴 이름으로 모호함을 없애며 참조할 수 있어요.

출처: Raku 공식 문서 — Packages

본문

이름 (Names)

패키지 이름은 변수 이름으로 쓸 수 있는 합법적인 부분(sigil 제외)이면 무엇이든 돼요. 다음을 포함해요:

class Foo {
    sub zape () { say "zipi" }
    class Bar {
        method baz () { return 'Þor is mighty' }
        our &zape = { "zipi" };
        our $quux = 42;
    }
}

my $foo;                # simple identifiers
say Foo::Bar.baz;       # calling a method; OUTPUT: «Þor is mighty␤»
say Foo::Bar::zape;     # compound identifiers separated by ::; OUTPUT: «zipi␤»
my $bar = 'Bar';
say $Foo::($bar)::quux; # compound identifiers with interpolations; OUTPUT: «42␤»
$42;                    # numeric names
$!;                     # certain punctuation variables

::는 중첩된 패키지 이름을 구분하는 데 쓰여요.

패키지는 사실상 정체성(identity)이 없어요. 모듈이나 클래스 이름의 일부일 뿐이죠. 네임스페이스라기보다는 모듈에 가까워요. 같은 이름의 모듈이 있으면, 패키지가 존재할 때 그 모듈이 패키지의 정체성을 *차지(capture)*해요.

package Foo:ver<0> {};
module Foo:ver<1> {};
say Foo.^ver; # OUTPUT: «1␤»

선언된 패키지 문법은 버전과 권위(authority) auth도 쓸 수 있게 허용하지만, 실제로는 무시돼요. 정체성에 authver를 포함할 수 있는 것은 모듈·클래스 같은 고차 타입 객체뿐이에요.

package Foo:ver<0>:auth<bar> {};
say Foo.^auth;
# OUTPUT: «(exit code 1) No such method 'auth' for invocant of type
# 'Perl6::Metamodel::PackageHOW' ...

패키지 한정 이름 (Package-qualified names)

일반적인 패키지 한정 이름은 이렇게 생겼어요: $Foo::Bar::quuxFoo::Bar 패키지 안의 $quux 변수이고, Foo::Bar::zape는 같은 패키지 안의 &zape 변수를 뜻해요.

때로는 sigil을 변수 이름과 함께 두는 게 더 명확할 때가 있어요. 그럴 땐 이렇게 써도 돼요:

Foo::Bar::<$quux>

이 방식은 Foo«&zape» 변수에는 안 돼요. 서브(sub)는 기본적으로 어휘 스코프(lexical scope)를 갖기 때문이죠. 변수 이름이 상수라서 이름은 컴파일 타임에 해석돼요. 클래스는 기본적으로 패키지 스코프를 갖기 때문에 Bar의 나머지 변수들(위 예시처럼)에는 접근할 수 있어요.

:: 앞의 이름 부분이 비어 있으면 패키지가 지정되지 않았다는 뜻이고 검색해야 해요. 일반적으로 주 sigil 바로 다음의 ::는 컴파일 타임에 알려진 이름에 대해선 no-op이지만, ::()는 보간(interpolation)을 도입하는 데 쓸 수 있죠. 또한 다른 sigil이 없을 때 ::는 아직 선언되지 않은 패키지 이름을 의도적으로 쓰겠다는 뜻의 sigil 역할을 할 수 있어요.

의사 패키지 (Pseudo-packages)

이름의 맨 앞에 예약된 의사 패키지 이름은 다음과 같아요:

MY 현재 어휘 스코프의 기호들
OUR 현재 패키지의 기호들
CORE 가장 바깥 어휘 스코프, 표준 Raku의 정의
GLOBAL 인터프리터 전역 패키지 기호들, 실제로는 UNIT::GLOBAL
PROCESS 프로세스 관련 전역(superglobal). 동적 변수 조회가 마지막으로 찾는 곳.

이름 어디에서든 쓸 수 있는 상대 이름들도 예약돼 있어요:

CALLER 즉시 호출자의 어휘 스코프에 있는 동적 기호
CALLERS 어떤 호출자의 어휘 스코프에든 있는 동적 기호
DYNAMIC 나 또는 어떤 호출자의 어휘 스코프에 있는 동적 기호
OUTER 다음 바깥 어휘 스코프의 기호
OUTERS 어떤 바깥 어휘 스코프의 기호
LEXICAL 나 또는 어떤 바깥 어휘 스코프의 동적 기호
UNIT 컴파일 단위의 가장 바깥 어휘 스코프의 기호
SETTING 그 단위의 DSL(보통 CORE)에 있는 어휘 기호
PARENT 이 패키지의 부모 패키지(또는 어휘 스코프)의 기호
CLIENT 다른 패키지에서 온 가장 가까운 CALLER

파일의 스코프는 UNIT이라고 불러요. 하지만 그 밖에도 언어 설정에 해당하는 어휘 스코프가 하나 이상 있어요(다른 문화권에서는 보통 prelude라고 부르죠). 따라서 SETTING 스코프는 UNIT::OUTERS와 같아요. 표준 Raku 프로그램에서 SETTINGCORE와 같지만, -n이나 -p 같은 시작 옵션은 도메인 특화 언어(DSL)로 들어가게 할 수 있어요. 그 경우 CORE는 표준 언어의 스코프로 남고, SETTING은 현재 파일의 설정 역할을 하는 DSL을 정의하는 스코프가 돼요. 이름 중간에서 검색어로 쓰일 때 SETTINGCORE까지 모든 바깥 스코프를 포함해요. 설정의 가장 바깥 스코프만 얻으려면 UNIT::OUTER를 쓰세요.

이름 조회하기 (Looking up names)

이름으로 보간하기 (Interpolating into names)

패키지나 변수 이름을 놓을 자리에 ::($expr)를 써서 문자열을 보간할 수 있어요. 그 문자열에는 ::를 더 포함할 수 있는데, 패키지 중첩으로 해석돼요. 이 구조는 ::로 시작하고 즉시 끝나거나 괄호 밖에서 또 다른 ::로 이어지기 때문에, 이름 전체만 보간할 수 있어요. 대부분의 상징적 참조가 이 표기로 이뤄져요:

my $foo = "Foo";
my $bar = "Bar";
my $foobar = "Foo::Bar";
$::($bar)              # lexically-scoped $Bar
$::("MY::$bar")        # lexically-scoped $Bar
$::("OUR::$bar")       # package-scoped $Bar
$::("GLOBAL::$bar")    # global $Bar
$::("PROCESS::$bar")   # process $Bar
$::("PARENT::$bar")    # current package's parent's $Bar
$::($foobar)           # $Foo::Bar
@::($foobar)::baz      # @Foo::Bar::baz
@::($foo)::Bar::baz    # @Foo::Bar::baz
@::($foobar)baz        # ILLEGAL at compile time (no operator baz)
@::($foo)::($bar)::baz # @Foo::Bar::baz

::가 전역을 뜻하지는 않아요. 여기서는 보간 문법의 일부로, 패키지를 뜻하지도 않아요. ::() 구성요소를 보간한 뒤에는 간접 이름이 원래 소스 코드에 그대로 있었던 것처럼 조회돼요. 우선순위는 먼저 선두 의사 패키지 이름, 그 다음 어휘 스코프의 이름(밖으로 검색해 CORE에서 끝), 마지막으로 현재 패키지예요.

MY 의사 패키지로 현재 어휘 스코프에 조회를 제한하고, OUR로 현재 패키지 스코프에 제한할 수 있어요.

마찬가지로 클래스·메서드 이름도 보간할 수 있어요:

role with-method {
    method a-method { return 'in-a-method of ' ~ $?CLASS.^name  };
}

class a-class does with-method {
    method another-method { return 'in-another-method' };
}

class b-class does with-method {};

my $what-class = 'a-class';

say ::($what-class).a-method; # OUTPUT: «in-a-method of a-class␤»
$what-class = 'b-class';
say ::($what-class).a-method; # OUTPUT: «in-a-method of b-class␤»

my $what-method = 'a-method';
say a-class."$what-method"(); # OUTPUT: «in-a-method of a-class␤»
$what-method = 'another-method';
say a-class."$what-method"(); # OUTPUT: «in-another-method␤»

직접 조회 (Direct lookup)

스캔 없이 패키지의 기호 테이블에서 직접 조회하려면 패키지 이름을 해시처럼 다루면 돼요:

Foo::Bar::{'&baz'}  # same as &Foo::Bar::baz
PROCESS::<$IN>      # same as $*IN
Foo::<::Bar><::Baz> # same as Foo::Bar::Baz

::() 상징적 참조와 달리, 이 방식은 인자를 ::로 파싱하지 않고 그 지점에서부터 네임스페이스 스캔을 시작하지도 않아요. 또한 상수 첨자에 대해서는 컴파일 타임에 기호 해석이 보장돼요.

정규식 안에서는 MONKEY-SEE-NO-EVAL 프라그마를 쓰지 않는 한 이런 직접 조회를 쓸 수 없어요. 이 조치의 주된 의도는 사용자 입력이 외부에서 실행되는 것을 피하려는 거예요.

null 의사 패키지는 일반 이름 검색과 같은 검색 목록이에요. 즉 다음은 모두 의미가 같아요:

$foo
::{'$foo'}
::<$foo>

각각 어휘 스코프를 바깥으로 스캔한 뒤 현재 패키지 스코프를 스캔해요(use strict가 적용 중이면 패키지 스코프는 허용되지 않죠).

패키지 조회 (Package lookup)

패키지 객체 자체를 해시 객체처럼 첨자화해요. 키는 sigil을 포함한 변수 이름이에요. 패키지 객체는 타입 이름 뒤에 :: 접미사를 붙여 얻을 수 있어요:

MyType::<$foo>

클래스 멤버 조회 (Class member lookup)

메서드 — 공용 속성의 접근자 같은 자동 생성 메서드 포함 — 는 클래스 메타객체에 저장되며 lookup 메서드로 조회할 수 있어요.

Str.^lookup('chars')

전역 (Globals)

인터프리터 전역은 GLOBAL 패키지에 살아요. 사용자 프로그램은 GLOBAL 패키지에서 시작하므로 메인라인 코드의 "our" 선언은 기본적으로 그 패키지로 들어가요. 프로세스 전역 변수는 PROCESS 패키지에 있어요. $*UID$*PID 같은 대부분의 미리 정의된 전역은 사실 프로세스 전역이에요.

모듈의 프로그램적 사용 (Programmatic use of modules)

모듈을 프로그램적으로 정의하고 사용할 수 있으면 편리할 때가 있어요. 실제 예시를 보죠.

일련의 모듈 파일이 이렇게 디렉터리 트리에 있다고 해요:

lib/
    TomtomMaps/
        Example/
            # a directory of example file modules programmatically
            # created from the Tomtom Maps SDK
            A01.rakumod
            #...
            C09.rakumod
            #...

TomtomMaps::Example::C09 모듈은 이렇게 생겼어요:

unit module TomtomMaps::Example::C09;

# ensure you use the 'our' declarator
our sub print-example($fh, # filehandle open for writing
                      :$api-maps-key
) { # do not need to export the sub
    $fh.print: qq:to/HERE/;
    # ... the example html file
    HERE
}

이 서브루틴들은 이렇게 접근해 사용할 수 있어요:

use lib 'lib';
my $module-dir = 'TomtomMaps::Example';
my $example    = 'C09';
my $m          = "{$module-dir}::{$example}";

# you must use the runtime 'require', not the compile-time 'use'
require ::($m);

my $fh = open "./example-{$example}.html", :w;
my $api-maps-key = 'ghghfxnnhrgfsWE.mmn';

# execute the subroutine
&::($m)::print-example($fh, :$api-maps-key); # the map's html file is written

$fh.close;

여기서 핵심은 모듈 이름이 런타임에 결정되므로 컴파일 타임 use가 아니라 런타임 require를 써야 한다는 점이에요. ::($m)로 모듈을 보간하고, &::($m)::print-example처럼 실행할 서브까지 마찬가지로 보간해서 호출하는 거죠.

더 알아보기 (Learn more)