JavaScript(Node.js)에서 Raku로 - 핵심 요약

JavaScript(Node.js)에서 Raku로 - 핵심 요약 (JavaScript (Node.js) to Raku - nutshell)

Node.js에 익숙한 사용자가 Raku를 배울 수 있도록 돕는 페이지예요. 두 언어가 공유하는 기능과, 문법과 기능의 주요 차이점을 함께 설명할게요. 이 문서는 Raku를 처음 배우는 튜토리얼이 아니라, 이미 Node.js 중급~고급 실력인 사용자를 위한 참고 자료예요.

출처: Raku Documentation — JavaScript (Node.js) to Raku - nutshell

본문

기본 문법 (Basic syntax)

"Hello, world!"

새 언어를 배울 때 가장 먼저 쓰는 전형적인 프로그램부터 시작해볼게요. Node.js에서는 헬로월드 프로그램을 이렇게 써요.

console.log('Hello, world!');

Raku에서 같은 일을 하는 방법은 여러 가지가 있어요.

say('Hello, world!');
say 'Hello, world!';

Raku에서는 함수 호출 때 괄호가 선택 사항이에요. 세미콜론은 Node.js에서는 대부분 선택 사항이지만, Raku에서 표현식에는 필수예요.

세상에 인사를 했으니 이제 좋은 친구 Joe에게 인사해볼게요. 다시 Node.js부터 시작할게요.

let name = 'Joe';
console.log('What\'s up,' + name + '?');
console.log(`What's up, ${name}?`);
console.log("What's up, ", name, "?");

그가 우리 목소리를 못 들었으니, 이번에는 Raku로 다시 인사해볼게요.

my $name = 'Joe';
say 'What\'s up, ' ~ $name ~ '?';
say "What's up, $name?";
say "What's up, ", $name, "?";

여기서 차이점은 몇 가지뿐이에요. 대부분의 Raku 변수는 **시길(sigil)**이라는 것을 갖는데, 이름 앞의 $가 바로 그것이에요. 그리고 문자열 연결은 + 대신 ~ 연산자를 써요. 두 언어가 여기서 공유하는 점은 문자열 보간(string interpolation)을 지원한다는 거예요.

기본 예시를 살펴봤으니, 두 언어의 유사점을 더 자세히 설명할게요.

변수 (Variables)

Node.js의 변수는 이렇게 정의할 수 있어요.

var   foo = 1;  // Lexically scoped with functions and modules
let   foo = 1;  // Lexically scoped with blocks
const foo = 1;  // Lexically scoped with blocks; constant

// No equivalent to Raku dynamic variables exists.

global.foo = 1; // Globally scoped
foo = 1;        // Ditto, but implicit; forbidden in strict mode

Raku에는 var에 해당하는 게 없어요. 한 가지 중요한 점은 Raku에는 변수 호이스팅(hoisting)이 없다는 거예요. 변수는 그 줄에서 정의되고 할당돼요. 스코프의 맨 위에서 정의한 뒤 나중에 그 줄에서 할당하는 방식이 아니에요.

일반 변수에 더해, Raku에는 **동적 변수(dynamic variable)**라는 것이 있어요. 동적 변수는 바깥 스코프가 아니라 호출자의 스코프로 찾아져요. Raku에서 동등한 변수 선언은 이렇게 생겼어요.

my $foo = 1; # Lexically scoped
our $foo = 1; # Package scoped
my constant foo = 1; # Lexically scoped; constant
constant foo = 1; # Package scoped; constant
my  $*foo = 1; # Dynamic variable; lexically scoped
our $*foo = 1; # Dynamic variable; package scoped
GLOBAL::<$foo> := 1; # Globally scoped

let을 쓸 자리에는 my를, 가장 바깥쪽에 필요한 스코프에 정의할 변수에는 our를, const를 쓸 자리에는 constant를 쓰면 돼요.

변수 이름 앞에 붙은 $$* 기호를 눈치챘을 거예요. 이것들은 각각 **시길(sigil)**과 **트으질(twigil)**이라고 불리며, 변수가 가진 컨테이너를 정의해요. 시길, 트으질, 컨테이너에 대한 자세한 내용은 변수 관련 문서를 참고하세요.

Node.js의 변수는 바깥 스코프의 다른 변수와 같은 이름을 가져도 충돌하지 않아요 (물론 설정에 따라 린터가 보통 불평하긴 하지만요).

let foo = 1;
function logDupe() {
    let foo = 2;
    console.log(foo);
}

logDupe();       // OUTPUT: 2
console.log(foo); // OUTPUT: 1

Raku도 이것을 허용해요.

my $foo = 1;
sub log-dupe {
    my $foo = 2;
    say $foo;
}

log-dupe; # OUTPUT: 2
say $foo; # OUTPUT: 1

연산자 (Operators)

할당 (Assignment)

= 연산자는 두 언어에서 똑같이 동작해요.

Raku의 := 연산자는 값을 변수에 바인딩해요. 변수를 다른 변수에 바인딩하면 두 변수가 같은 값과 같은 컨테이너를 가져요. 즉 하나의 속성을 변형하면 다른 쪽도 함께 변형돼요. 바인딩된 변수는 =로 재할당하거나 ++, -- 등으로 변형시킬 수 없어요. 하지만 다른 값에 다시 바인딩할 수는 있어요.

my %map;            # This is a hash, roughly equivalent to a JS object or map
my %unbound = %map;
my %bound := %map;
%map<foo> = 'bar';
say %unbound;       # OUTPUT: {}
say %bound;         # OUTPUT: {foo => bar}

%bound := %unbound;
say %bound;         # OUTPUT: {}

동등성 (Equality)

Node.js에는 ===== 두 개의 동등성 연산자가 있어요.

==느슨한 동등성(loose equality) 연산자예요. 피연산자가 같은 타입일 때 둘 다 같으면 true를 반환해요. 하지만 피연산자의 타입이 다르면 비교 전에 둘 다 프리미티브로 캐스팅돼요. 그래서 아래는 전부 true를 반환해요.

console.log(1 == 1);   // OUTPUT: true
console.log('1' == 1); // OUTPUT: true
console.log([] == 0);  // OUTPUT: true

비슷하게 Raku에서도 두 피연산자가 같은 타입을 공유하지 않으면 비교 전에 Numeric으로 캐스팅돼요.

say 1 == 1;       # OUTPUT: True
say '1' == 1;     # OUTPUT: True
say [1,2,3] == 3; # OUTPUT: True, since the array has three elements

==의 역은 !=이에요.

Raku에는 ==와 비슷한 또 다른 연산자 eq가 있어요. 타입이 다를 때 피연산자를 Numeric으로 캐스팅하는 대신, eq는 문자열로 캐스팅해요.

say '1' eq '1'; # OUTPUT: True
say 1 eq '1';   # OUTPUT: True

eq의 역은 ne 또는 !eq이에요.

===엄격 동등성(strict equality) 연산자예요. 두 피연산자가 같은 값이면 true를 반환해요. 객체를 비교할 때는 정확히 같은 객체일 때만 true를 반환해요.

console.log(1 === 1);   // OUTPUT: true
console.log('1' === 1); // OUTPUT: false
console.log({} === {}); // OUTPUT: false

let obj = {};
let obj2 = obj;
console.log(obj === obj2); // OUTPUT: true;

Raku에서는 이 연산자가 예외 하나를 빼고 똑같이 동작해요. 같은 값을 가지지만 다른 컨테이너를 가진 두 객체는 false를 반환해요.

say 1 === 1;                    # OUTPUT: «True␤»
say '1' === 1;                  # OUTPUT: «False␤»
say 'ayy lmao' === 'ayy lmao';  # OUTPUT: «True␤»
say {} === {};                  # OUTPUT: «False␤»

my \hash = {};
my %hash = hash;
say hash === %hash; # OUTPUT: False

마지막 경우는 같은 객체인데 컨테이너가 달라서 False를 반환해요.

===의 역은 !==이에요.

여기서 Raku의 다른 동등성 연산자들이 유용해져요. 값이 다른 컨테이너를 가진다면 eqv 연산자를 쓸 수 있어요. 이 연산자는 깊은 동등성(deep equality) 검사에도 쓸 수 있는데, Node.js에서는 보통 라이브러리가 필요한 일이에요.

say {a => 1} eqv {a => 1}; # OUTPUT: True

my \hash = {};
my %hash := hash;
say hash eqv %hash; # OUTPUT: True

두 변수가 같은 컨테이너와 값을 가지는지 확인해야 할 때는 =:= 연산자를 써요.

my @arr = [1,2,3];
my @arr2 := @arr;   # Bound variables keep the container of the other variable
say @arr =:= @arr2; # OUTPUT: True

스마트매치 (Smartmatching)

Raku에는 값을 비교하는 마지막 연산자가 하나 더 있는데, 정확히 동등성 연산자는 아니에요. ~~ 스마트매치 연산자예요. 용도가 여럿 있어요. Node.js의 instanceof처럼 쓰이거나, 정규식을 매치하거나, 값이 해시·백·셋·맵의 키인지 확인하는 데 쓰여요.

say 'ayy lmao' ~~ Str; # OUTPUT: True

my %hash = a => 1;
say 'a' ~~ %hash; # OUTPUT: True

my $str = 'abc';
$str ~~ s/abc/def/; # Mutates $str, like foo.replace('abc', 'def')
say $str;           # OUTPUT: def

instanceof 이야기가 나왔으니, Node.js 객체의 constructor 속성에 해당하는 Raku의 것은 WHAT 속성이에요.

console.log('foo'.constructor); // OUTPUT: String
say 'foo'.WHAT; # OUTPUT: Str

숫자 (Numeric)

Node.js에는 +, -, /, *, %, (ES6에서)**를 숫자 연산자로 써요. 피연산자의 타입이 다르면, 동등성 연산자처럼, 연산을 진행하기 전에 프리미티브로 캐스팅돼요. 그래서 이런 게 가능해요.

console.log(1 + 2);   // OUTPUT: 3
console.log([] + {}); // OUTPUT: [object Object]
console.log({} + []); // OUTPUT: 0

Raku에서도 역시 Numeric 타입으로 변환돼요.

say 1 + 2;        # OUTPUT: 3
say [] + {};      # OUTPUT: 0
say {} + [1,2,3]; # OUTPUT: 3

추가로 Raku에는 div%%가 있어요. div는 C에서 정수 나눗셈처럼 동작하고, %%는 한 숫자가 다른 숫자로 깔끔하게 나누어지는지 검사해요.

say 4 div 3; # OUTPUT: 1
say 4 %% 3;  # OUTPUT: False
say 6 %% 3;  # OUTPUT: True

비트 연산 (Bitwise)

Node.js에는 비트 연산자로 &, |, ^, ~, <<, >>, >>>, ~가 있어요.

console.log(1 << 1);  // OUTPUT: 2
console.log(1 >> 1);  // OUTPUT: 0
console.log(1 >>> 1); // OUTPUT: 0
console.log(1 & 1);   // OUTPUT: 1
console.log(0 | 1);   // OUTPUT: 1
console.log(1 ^ 1);   // OUTPUT: 0
console.log(~1);      // OUTPUT: -2

Raku에는 >>>에 해당하는 게 없어요. 모든 비트 연산자는 +가 접두사로 붙는데, 비트 부정(negation)은 유일하게 ~ 대신 +^를 써요.

say 1 +< 1; # OUTPUT: 2
say 1 +> 1; # OUTPUT: 0
            # No equivalent for >>>
say 1 +& 1; # OUTPUT: 1
say 0 +| 1; # OUTPUT: 1
say 1 +^ 1; # OUTPUT: 0
say +^1;    # OUTPUT: -2

정의됨 확인하기 (Checking for definedness)

JavaScript에는 null 병합 연산자 ??가 있고, null이나 undefined일 때만 진행돼요.

undefined || null || 0 || 1 ;   // => 1
undefined ?? null ?? 0 ?? 1 ;   // => 0

이것은 Raku // 연산자와 아주 비슷해요.

Any || Nil || 0 || 1 ;   # => 1
Any // Nil // 0 // 1 ;   # => 0

사용자 정의 연산자와 연산자 오버로딩 (Custom operators and operator overloading)

Node.js는 Makefile을 쓰거나 커스텀 V8로 Node.js를 빌드하지 않으면 연산자 오버로딩을 허용하지 않아요. Raku는 네이티브로 사용자 정의 연산자와 연산자 오버로딩을 허용해요! 모든 연산자는 서브루틴이므로, 이렇게 직접 정의할 수 있어요.

# "distance operator": the distance of two numbers is the absolute value
# of their difference
multi infix:<|-|>($a, $b) is equiv(&infix:<->) { abs $a - $b }

say -1 |-| 3; # OUTPUT: 4

연산자는 prefix, infix, postfix, circumfix로 정의할 수 있어요. is tighter, is equiv, is looser 트레잇이 연산자의 우선순위를 선택적으로 정의해요. 여기서 |-|-와 같은 우선순위를 가져요.

연산자 서브루틴을 선언할 때 multi가 쓰이는 점을 주목하세요. 이렇게 하면 서로 다른 시그니처를 가지면서도 같은 이름의 여러 서브루틴을 선언할 수 있어요. 이에 대한 자세한 설명은 Functions 절에 있어요. 지금은, 우리가 원하는 아무 네이티브 연산자나 오버라이드할 수 있게 해준다는 것만 알면 돼요.

# Using the `is default` trait here forces this subroutine to be chosen first,
# so long as the signature of the subroutine matches.
multi prefix:<++>($a) is default { $a - 1 }

my $foo = 1;
say ++$foo; # OUTPUT: 0

제어 흐름 (Control flow)

if/else

JavaScript에서 if/else가 어떻게 생겼는지 익숙할 거예요.

let diceRoll = Math.ceil(Math.random() * 6) + Math.ceil(Math.random() * 6);
if (diceRoll === 2) {
    console.log('Snake eyes!');
} else if (diceRoll === 12) {
    console.log('Boxcars!');
} else {
    console.log(`Rolled ${diceRoll}.`);
}

Raku에서 if/else는 몇 가지 핵심 차이를 빼고는 대체로 같아요. 첫째, 괄호가 필요 없어요. 둘째, else ifelsif로 써요. 셋째, if 절을 문장 뒤에 쓸 수 있어요.

my Int $dice-roll = ceiling rand * 6 + ceiling rand * 6;
if $dice-roll == 2 {
    say 'Snake eyes!';
} elsif $dice-roll == 12 {
    say 'Boxcars!';
} else {
    say "Rolled $dice-roll.";
}

덜 효율적이긴 하지만, 문장 뒤에 if를 쓰는 방식으로도 쓸 수 있어요.

my Int $dice-roll = ceiling rand * 6 + ceiling rand * 6;
say 'Snake eyes!'        if $dice-roll == 2;
say 'Boxcars!'           if $dice-roll == 12;
say "Rolled $dice-roll." if $dice-roll != 2 && $dice-roll != 12;

Raku에는 when도 있는데, if와 비슷하지만 주어진 조건이 참이면, 그 블록 안에서 when 블록 뒤에 오는 코드는 실행되지 않아요.

{
    when True {
        say 'In when block!'; # OUTPUT: In when block!
    }
    say 'This will never be output!';
}

추가로 Raku에는 with, orwith, without이 있는데, 각각 if, else if, else와 비슷해요. 다만 조건이 참인지 확인하는 대신 정의됐는지(defined) 확인해요.

switch

Switch 문은 주어진 값과 값 목록 사이의 동등성을 검사하고, 하나가 매치되면 어떤 코드를 실행하는 방식이에요. case 문이 비교할 각 값을 정의해요. default는 포함될 경우, 주어진 값이 어떤 case와도 매치되지 않을 때의 대체(fallback) 역할을 해요. case가 매치된 뒤에는 보통 break를 써서, 매치된 case 다음에 오는 case들의 코드가 실행되지 않게 해요. 다만 의도적으로 생략하는 경우도 드물게 있죠.

const ranklist = [2, 3, 4, 5, 6, 7, 8, 9, 'Jack', 'Queen', 'King', 'Ace'];
const ranks    = Array.from(Array(3), () => ranklist[Math.floor(Math.random() * ranks.length)]);
let   score    = 0;

for (let rank of ranks) {
    switch (rank) {
        case 'Jack':
        case 'Queen':
        case 'King':
            score += 10;
            break;
        case 'Ace';
            score += (score <= 11) ? 10 : 1;
            break;
        default:
            score += rank;
            break;
    }
}

Raku에서는 given을 switch 문처럼 쓸 수 있어요. when 블록이 주로 case 문처럼 쓰이므로 break에 해당하는 건 없어요. switchgiven의 주요 차이 하나는, switch 문에 전달된 값은 그 값과 정확히 같은 case만 매치한다는 거예요. 반면 given의 값은 when 값들에 대해 **스마트매치(~~)**돼요.

my     @ranklist = [2, 3, 4, 5, 6, 7, 8, 9, 'Jack', 'Queen', 'King', 'Ace'];
my     @ranks    = @ranklist.pick: 3;
my Int $score    = 0;

for @ranks -> $rank {
    # The when blocks implicitly return the last statement they contain.
    $score += do given $rank {
        when 'Jack' | 'Queen' | 'King' { 10                      }
        when 'Ace'                     { $score <= 11 ?? 10 !! 1 }
        default                        { $_                      }
    };
}

given에 전달된 값과 매치되는 when 블록이 여러 개가 있고 그중 둘 이상을 실행하고 싶다면 proceed를 써요. succeed는 그것이 있는 when 블록과 given 블록 둘 다를 빠져나가게 해서, 뒤따르는 어떤 문장도 실행되지 않게 해요.

given Int {
    when Int     { say 'Int is Int';     proceed }
    when Numeric { say 'Int is Numeric'; proceed }
    when Any     { say 'Int is Any';     succeed }
    when Mu      { say 'Int is Mu'               } # Won't output
}

# OUTPUT:
# Int is Int
# Int is Numeric
# Int is Any

for, while, do/while

JavaScript에는 세 종류의 for 루프가 있어요.

// C-style for loops
const letters = {};
for (let ord = 0x61; ord <= 0x7A; ord++) {
    let letter = String.fromCharCode(ord);
    letters[letter] = letter.toUpperCase();
}

// for..in loops (typically used on objects)
for (let letter in letters) {
    console.log(letters[letter]);
}
# OUTPUT:
# A
# B
# C
# etc.

// for..of loops (typically used on arrays, maps, and sets)
for (let letter of Object.values(letters)) {
    console.log(letter);
}
# OUTPUT:
# A
# B
# C
# etc.

Raku의 for 루프는 for..of 루프와 가장 비슷해요. 반복 가능한 것이면 뭐든 동작하니까요. C 스타일 루프는 loop로 쓸 수 있지만, 범위를 쓴 for 루프로 쓰는 편이 더 낫기 때문에 권장되지 않아요. if 문처럼, for는 문장 뒤에 올 수 있고, 현재 반복은 $_ 변수("it"이라고도 함)로 접근할 수 있어요. $_의 메서드는 변수를 지정하지 않고 호출할 수 있어요.

my Str %letters{Str};
%letters{$_} = .uc for 'a'..'z';
.say for %letters.values;
# OUTPUT:
# A
# B
# C
# etc.

while 루프는 JavaScript와 Raku에서 똑같이 동작해요. Raku에는 until 루프도 있는데, 주어진 조건이 거짓이 될 때까지 반복하는 대신 조건이 참이 될 때까지 반복해요.

do/while 루프는 Raku에서 repeat/while 루프로 알려져 있어요. while과 마찬가지로 repeat/until 루프도 존재하며, 주어진 조건이 거짓이 될 때까지 반복해요.

Raku에서 무한 루프를 쓰려면 forwhile 대신 loop를 써요.

JavaScript에서 continue는 루프에서 다음 반복으로 건너뛰는 데 쓰이고, break는 루프를 일찍 빠져나가는 데 쓰여요.

let primes = new Set();
let i      = 2;

do {
    let isPrime = true;
    for (let prime of primes) {
        if (i % prime == 0) {
            isPrime = false;
            break;
        }
    }
    if (!isPrime) continue;
    primes.add(i);
} while (++i < 20);

console.log(primes); # OUTPUT: Set { 2, 3, 5, 7, 11, 13, 17, 19 }

Raku에서는 이것이 각각 nextlast로 알려져 있어요. redo도 있는데, 루프의 조건을 다시 평가하지 않고 현재 반복을 반복해요.

next/redo/last 문장 뒤에는 바깥 루프의 앞에 정의된 레이블을 붙일 수 있어요. 그러면 그 문장이 자신이 속한 루프가 아니라 그 레이블이 가리키는 루프에 대해 동작하게 돼요.

my %primes is SetHash;
my Int $i = 2;

OUTSIDE:
repeat {
    next OUTSIDE if $i %% $_ for %primes.keys;
    %primes{$i}++;
} while ++$i < 20;

say %primes; # OUTPUT: SetHash(11 13 17 19 2 3 5 7)

do

do는 현재 JavaScript의 기능이 아니지만, ECMAScript에 추가하자는 제안이 있었어요. do 표현식은 블록을 평가하고 그 결과를 반환해요.

constant VERSION        = v2.0.0;
constant VERSION_NUMBER = do {
    my @digits = VERSION.Str.comb(/\d+/);
    :16(sprintf "%02x%02x%04x", |@digits)
};
say VERSION_NUMBER; # OUTPUT: 33554432

타입 (Types)

타입 만들기 (Creating types)

JavaScript에서는 클래스(또는 ES5 이하의 생성자)를 만들어 타입을 만듭니다. TypeScript를 써봤다면, 다른 타입들의 부분집합으로 타입을 이렇게 정의할 수 있어요.

type ID = string | number;

Raku에서 클래스, 롤(role), 부분집합(subset), 열거형(enum)이 타입으로 간주돼요. 클래스와 롤 만드는 법은 이 글의 OOP 절에서 다룰게요. ID 부분집합을 만드는 것은 이렇게 해요.

subset ID where Str | Int;

부분집합과 Junction에 대한 자세한 내용은 관련 문서를 참고하세요.

TypeScript 열거형은 값으로 숫자나 문자열을 가질 수 있어요. 값을 정의하는 것은 선택 사항이에요. 기본적으로 첫 키의 값은 0, 다음 키는 1, 그 다음은 2, 이런 식이에요. 예를 들어 (아마 TUI 게임을 위한) 확장 ASCII 화살표 기호의 방향을 정의하는 열거형이 있어요.

enum Direction (
    UP    = '↑',
    DOWN  = '↓',
    LEFT  = '←',
    RIGHT = '→'
);

Raku의 열거형은 키의 값으로 어떤 타입이든 가질 수 있어요. 열거형 키(그리고 선택적으로 값)는 enum, 그 다음 열거형 이름, 그 다음 키(및 선택적으로 값) 목록을 써서 정의할 수 있는데, 목록은 " " > <, « », ( )를 써서 만들 수 있어요. 열거형 키의 값을 정의하려면 ( )를 써야 해요. Raku로 쓴 Direction 열거형을 볼게요.

enum Direction (
    UP    => '↑',
    DOWN  => '↓',
    LEFT  => '←',
    RIGHT => '→'
);

enum에 대한 자세한 내용은 관련 문서를 참고하세요.

타입 사용하기 (Using types)

TypeScript에서는 변수의 타입을 정의할 수 있어요. 변수의 타입과 맞지 않는 값을 할당하려 하면 트랜스파일러가 오류를 내요. 이렇게 해요.

enum Name (Phoebe, Daniel, Joe);
let name: string = 'Phoebe';
name = Phoebe; # Causes tsc to error out

let hobbies: [string] = ['origami', 'playing instruments', 'programming'];

let todo: Map<string, boolean> = new Map([
    ['clean the bathroom', false],
    ['walk the dog', true],
    ['wash the dishes', true]
]);

let doJob: (job: string) => boolean = function (job: string): boolean {
    todo.set(job, true);
    return true;
};

Raku에서는 타입을 선언자(my, our 등)와 변수 이름 사이에 두어 변수를 타입화할 수 있어요. 변수의 타입과 맞지 않는 값을 할당하면, 값이 어떻게 평가되는지에 따라 컴파일 타임 또는 런타임 오류가 던져져요.

enum Name <Phoebe Daniel Joe>;
my Str $name = 'Phoebe';
$name = Phoebe; # Throws a compile-time error

# The type here defines the type of the elements of the array.
my Str @hobbies = ['origami', 'playing instruments', 'programming'];

# The type between the declarator and variable defines the type of the values
# of the hash.
# The type in the curly braces defines the type of the keys of the hash.
my Bool %todo{Str} = (
    'clean the bathroom' => False,
    'walk the dog'       => True,
    'wash the dishes'    => True
);

# The type here defines the return value of the routine.
my Bool &do-job = sub (Str $job --> Bool) {
    %todo{$job} = True;
};

JavaScript와 Raku 타입 비교 (Comparing JavaScript and Raku types)

몇 가지 JavaScript 타입과 Raku에서의 동등 타입을 정리한 표를 볼게요.

JavaScript Raku
Object Mu, Any, Hash
Array List, Array, Seq
String Str
Number Int, Num, Rat
Boolean Bool
Map Map, Hash
Set Set, SetHash

Object는 JavaScript에서 모든 타입의 슈퍼클래스이면서 해시를 만드는 방법이에요. Raku에서 Mu는 모든 타입의 슈퍼클래스인데, 보통은 Mu의 서브클래스이면서도 거의 모든 타입의 슈퍼클래스인 Any를 쓰는 게 좋아요 (Junction은 예외예요). Object를 해시로 쓸 때는 Hash가 바로 그 대상이에요. ObjectHash의 주요 차이 하나는, Object는 키의 순서를 보존하지만 Hash는 기본적으로 보존하지 않는다는 거예요.

Array에 해당하는 타입이 세 가지 있어요. Array는 가변 배열처럼 동작하므로 Array와 가장 비슷해요. ListArray와 비슷하지만 불변이에요. Seq는 느긋한(lazy) 배열을 만드는 데 쓰여요.

StringStr은 대부분 똑같이 쓰여요.

Raku에는 Number에 해당하는 타입이 여럿 있는데, 가장 흔히 보는 건 Int, Num, Rat 세 가지예요. Int는 정수를 나타내요. Num은 부동소수점 수를 나타내며 그래서 Number와 가장 비슷해요. Rat은 두 숫자의 분수를 나타내고, Num이 충분히 정밀한 값을 제공할 수 없을 때 쓰여요.

BooleanBool은 대부분 똑같이 쓰여요.

Map에는 Raku에 가변과 불변 두 가지 동등 타입이 있어요. Map이 불변이고, Hash가 가변이에요. 헷갈리지 마세요! JavaScript의 Map처럼, MapHash는 키나 값으로 어떤 타입이든 가질 수 있어요. 키가 반드시 문자열일 필요는 없어요.

Map처럼 Set에도 Raku에 가변과 불변 두 가지 동등 타입이 있어요. Set이 불변이고, SetHash가 가변이에요.

함수 (Functions)

TBD

객체지향 프로그래밍 (Object-oriented programming)

TBD

비동기 프로그래밍 (Asynchronous programming)

TBD

버퍼 (Buffers)

NodeJS는 BufferBlob 클래스로 원시 이진 데이터를 다뤄요. 반면 Raku는 BufBlob 롤로 다루는데, 각각 가변·불변 버퍼예요. Raku에서 BufBlob을 컴포즈하므로, 모든 Blob 메서드를 Buf 객체에 쓸 수 있어요.

다음 표는 NodeJS와 Raku의 버퍼 구성 사이의 유사점과 차이점을 정리해요.

Class/Role NodeJS Raku
Buffer/Buf 고정 길이 바이트 시퀀스 (push, pop 같은 메서드 없음) 동적으로 늘어나거나 줄어들 수 있는 바이트 시퀀스. push, pop 같은 메서드를 쓸 수 있음
for..of 문법으로 반복 가능 반복 구성을 써서 반복 가능
배열 인덱싱으로 각 바이트 갱신 가능, 예: buf[i]++ NodeJS와 같음
Blob 고정 길이 바이트 시퀀스 (push, pop 같은 메서드 없음) NodeJS와 같음
반복 불가능 반복 구성을 써서 반복 가능
각 바이트 불변 NodeJS와 같음

버퍼 만들기 (Creating buffers)

NodeJS에서 새 버퍼를 만드는 방법은 몇 가지가 있어요. 정적 메서드 Buffer.alloc로 n 바이트를 0으로 할당할 수 있어요. fill 인자를 제공하면 그것으로 채워요.

const zeroBuf = Buffer.alloc(8);
const charBuf = Buffer.alloc(8, 97, 'utf-8');
console.log(zeroBuf); // OUTPUT: «<Buffer 00 00 00 00 00 00 00 00>␤»
console.log(charBuf); // OUTPUT: «<Buffer 61 61 61 61 61 61 61 61>␤»

Raku에서는 allocate 메서드를 쓸 수 있어요.

my $zero-blob = Blob.allocate(8);
my $char-blob = Blob.allocate(8, 97);
say $zero-blob; # OUTPUT: «Blob:0x<00 00 00 00 00 00 00 00>␤»
say $char-blob; # OUTPUT: «Blob:0x<61 61 61 61 61 61 61 61>␤»

my $zero-buf = Buf.allocate(8);
my $char-buf = Buf.allocate(8, 97);
say $zero-buf; # OUTPUT: «Buf:0x<00 00 00 00 00 00 00 00>␤»
say $char-buf; # OUTPUT: «Buf:0x<61 61 61 61 61 61 61 61>␤»

버퍼를 정수 배열의 내용으로 초기화할 수도 있어요.

const buf = Buffer.from([ 114, 97, 107, 117 ]);
console.log(buf); // OUTPUT: «<Buffer 72 61 6b 75>␤»

Raku에서는 new 생성자로 같은 일을 할 수 있어요.

my $blob = Blob.new(114, 97, 107, 117);
say $blob; # OUTPUT: «Blob:0x<72 61 6B 75>␤»

my $buf = Buf.new(114, 97, 107, 117);
say $buf; # OUTPUT: «Buf:0x<72 61 6B 75>␤»

비슷하게, from 메서드로 버퍼를 문자열의 이진 인코딩으로 초기화할 수 있어요.

const buf = Buffer.from('NodeJS & Raku', 'utf-8');
console.log(buf); // OUTPUT: «<Buffer 4e 6f 64 65 4a 53 20 26 20 52 61 6b 75>␤»

Raku에서는 문자열의 encode 메서드를 호출하는데, Blob을 반환해요.

my $blob = "NodeJS & Raku".encode('utf-8');
say $blob; # OUTPUT: «utf8:0x<4E 6F 64 65 4A 53 20 26 20 52 61 6B 75>␤»

참고: Raku에서는 그 blob을 버퍼 관련 메서드에 넘길 때 문자를 명시적으로 인코딩해야 해요.

문자열의 이진 인코딩을 디코딩하려면 버퍼의 toString 메서드를 호출해요.

const buf = Buffer.from([ 114, 97, 107, 117 ]);
console.log(buf.toString('utf-8')); // OUTPUT: «raku␤»

Raku에서는 버퍼의 decode 메서드를 호출해요.

my $blob = Blob.new(114, 97, 107, 117);
say $blob.decode('utf-8'); # OUTPUT: «raku␤»

버퍼에 쓰기 (Writing to a buffer)

NodeJS에서 write 메서드로 버퍼에 써요.

const buf = Buffer.alloc(16);
buf.write('Hello', 0, 'utf-8');
console.log(buf); // OUTPUT: «<Buffer 48 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00>␤»
buf.write(' world!', 5, 'utf-8');
console.log(buf); // OUTPUT: «<Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 00 00 00 00>␤»

Raku에는 write 메서드가 없어요. 하지만 splice 메서드로 버퍼의 요소를 다른 요소로 덮어쓸 수 있어요.

my $buf = Buf.allocate(16);
$buf.splice(0, 5, 'Hello'.encode('utf-8'));
say $buf; # OUTPUT: «Buf:0x<48 65 6C 6C 6F 00 00 00 00 00 00 00 00 00 00 00>␤»
$buf.splice(5, 7, ' world!'.encode('utf-8'));
say $buf; # OUTPUT: «Buf:0x<48 65 6C 6C 6F 20 77 6F 72 6C 64 21 00 00 00 00>␤»

버퍼에서 읽기 (Reading from a buffer)

버퍼의 데이터에 접근하는 방법은 여러 가지가 있어요. 개별 바이트에 접근하는 것부터, 전체 내용을 추출하고 내용을 디코딩하는 것까지요.

const buf = Buffer.from('Hello', 'utf-8');
console.log(buf[0]); // OUTPUT: «72␤»

Raku에서도 []로 버퍼의 바이트를 인덱싱할 수 있어요.

my $blob = 'Hello'.encode('utf-8');
say $blob[0]; # OUTPUT: «72␤»

NodeJS에서 버퍼의 모든 데이터를 가져오는 가장 흔한 방법은 toString 메서드예요 (버퍼가 텍스트로 인코딩돼 있다고 가정).

const buf = Buffer.from('Hello');
const buf = Buffer.alloc(16);
buf.write('Hello world', 0, 'utf-8');
console.log(buf.toString('utf-8')); // OUTPUT: «Hello world!\u0000t␤»

toString에 offset과 length를 제공해 버퍼에서 관련 바이트만 읽을 수 있어요.

console.log(buf.toString('utf-8', 0, 12)); // OUTPUT: «Hello world!␤»

Raku에서는 decode 메서드로 같은 일을 할 수 있어요.

my $buf = Buf.allocate(16);
$buf.splice(0, 12, 'Hello world'.encode('utf-8'));;
say $buf.decode('utf-8').raku; # OUTPUT: «Hello world!\0\0\0\0>␤»

하지만 decode로 버퍼를 슬라이스하고 디코딩하는 일을 동시에 할 수는 없어요. 대신 subbuf로 invocant 버퍼에서 관련 부분을 추출한 다음, 반환된 버퍼를 디코딩하면 돼요.

say $buf.subbuf(0, 12).decode('utf-8').raku; # OUTPUT: «Hello world!>␤»

더 유용한 메서드 (More useful methods)

Buffer.isBuffer

NodeJS에서는 isBuffer 메서드로 객체가 버퍼인지 확인할 수 있어요.

const buf = Buffer.from('hello');
console.log(Buffer.isBuffer(buf)); // OUTPUT: «true␤»

Raku에서는 Blob 또는 Buf에 대해 스마트매치할 수 있어요 (BufBlob을 컴포즈한다는 걸 기억하세요).

my $blob = 'hello'.encode();
my $buf = Buf.allocate(4);
say $blob ~~ Blob; # OUTPUT: «True␤»
say $blob ~~ Buf;  # OUTPUT: «False␤»
say $buf ~~ Buf;   # OUTPUT: «True␤»
say $buf ~~ Blob;  # OUTPUT: «True␤»

Buffer.byteLength

문자열을 인코딩하는 데 필요한 바이트 수를 확인하려면 Buffer.byteLength를 쓸 수 있어요.

const camelia = '🦋';
console.log(Buffer.byteLength(camelia)); // OUTPUT: «4␤»

Raku에서는 bytes 메서드를 쓸 수 있어요.

my $camelia = '🦋';
say $camelia.encode.bytes; # OUTPUT: «4␤»

참고: 바이트 수는 문자열의 길이와 같지 않아요. 많은 문자가 그 길이가 드러내는 것보다 더 많은 바이트로 인코딩되기 때문이에요.

length

NodeJS에서 length 메서드로 버퍼가 할당한 메모리가 얼마나 되는지 알아내요. 이것은 버퍼 내용물의 크기와는 같지 않아요.

const buf = Buffer.alloc(16);
buf.write('🦋');
console.log(buf.length); // OUTPUT: «16␤»

Raku에서는 elems 메서드를 쓸 수 있어요.

my $buf = Buf.allocate(16);
$buf.splice(0, '🦋'.encode.bytes, '🦋'.encode('utf-8'));
say $buf.elems; # OUTPUT: «16␤»

copy

copy 메서드로 한 버퍼의 내용을 다른 버퍼에 복사해요.

const target = Buffer.alloc(24);
const source = Buffer.from('🦋', 'utf-8');
target.write('Happy birthday! ', 'utf-8');
source.copy(target, 16);
console.log(target.toString('utf-8', 0, 20)); // OUTPUT: «Happy birthday! 🦋␤»

Raku에는 copy 메서드가 없지만, splice 메서드로 같은 결과를 얻을 수 있어요.

my $target = Buf.allocate(24);
my $encoded-string = 'Happy birthday! '.encode('utf-8');
$target.splice(0, $encoded-string.bytes, $encoded-string);
my $source = '🦋'.encode('utf-8');
$target.splice(16, $source.bytes, $source);
say $target.subbuf(0, 20).decode('utf-8'); # OUTPUT: «Happy birthday! 🦋␤»

slice

slice 메서드로 버퍼의 부분집합을 슬라이스할 수 있어요. 이 메서드는 메모리 공간의 부분집합에 대한 참조를 반환해요. 그래서 슬라이스를 수정하면 원래 버퍼도 수정돼요.

// setup
const target = Buffer.alloc(24);
const source = Buffer.from('🦋', 'utf-8');
target.write('Happy birthday! ', 'utf-8');
source.copy(target, 16);

// slicing off buffer
const animal = target.slice(16, 20);
animal.write('🐪');
console.log(animal.toString('utf-8'); // OUTPUT: «🐪␤»

console.log(target.toString('utf-8', 0, 20)); // OUTPUT: «Happy birthday! 🐪␤»

여기서 target을 슬라이스하고 결과 버퍼를 animal에 저장한 뒤, 결국 그것을 수정했어요. 그 결과 target도 수정됐어요.

Raku에서는 subbuf 메서드를 쓸 수 있어요.

# setup
my $target = Buf.allocate(24);
my $encoded-string = 'Happy birthday! '.encode('utf-8');
$target.splice(0, $encoded-string.bytes, $encoded-string);
my $source = '🦋'.encode('utf-8');
$target.splice(16, $source.bytes, $source);

# slicing off buffer
my $animal = $target.subbuf(16, 20);
$animal.splice(0, $animal.bytes, '🐪'.encode('utf-8'));
say $animal.decode; # OUTPUT: «🐪␤»

say $target.subbuf(0, 20).decode('utf-8'); # OUTPUT: «Happy birthday! 🦋␤»

다만 NodeJS의 slice 메서드와 달리, subbuf새로운 버퍼를 반환해요. 버퍼의 부분집합에 대한 쓰기 가능한 참조를 얻으려면 subbuf-rw를 사용하세요.

# setup
my $target = Buf.allocate(24);
my $encoded-string = 'Happy birthday! '.encode('utf-8');
$target.splice(0, $encoded-string.bytes, $encoded-string);
my $source = '🦋'.encode('utf-8');
$target.splice(16, $source.bytes, $source);

# slicing off buffer
$target.subbuf-rw(16, 4) = '🐪'.encode('utf-8');

say $target.subbuf(0, 20).decode('utf-8'); # OUTPUT: «Happy birthday! 🐪␤»

네트워킹 API (The networking API)

Net

Raku에는 네트워킹을 다루는 API가 두 가지 있어요. IO::Socket::INET(동기 네트워킹)과 IO::Socket::Async(비동기 네트워킹)예요.

IO::Socket::INET은 현재 TCP 연결만 지원해요. 그 API는 C의 socket API와 비슷해요. C의 socket API에 익숙하다면, 사용법을 이해하는 데 오래 걸리지 않을 거예요. 예를 들어 첫 메시지를 받은 뒤 연결을 닫는 에코 서버가 있어요.

my IO::Socket::INET $server .= new:
    :localhost<localhost>,
    :localport<8000>,
    :listen;

my IO::Socket::INET $client .= new: :host<localhost>, :port<8000>;
$client.print: 'Hello, world!';

my IO::Socket::INET $conn = $server.accept;
my Str $msg               = $conn.recv;
say $msg; # OUTPUT: Hello, world!
$conn.print($msg);

say $client.recv; # OUTPUT: Hello, world!
$conn.close;
$client.close;
$server.close;

기본적으로 IO::Socket::INET 연결은 IPv4 전용이에요. IPv6를 쓰려면 서버나 클라이언트를 구성할 때 :family(PF_INET6)를 넘기세요.

대조적으로 IO::Socket::Async는 어느 패밀리를 쓸지 지정할 필요 없이 IPv4와 IPv6 둘 다 지원해요. UDP 소켓도 지원해요. 위와 같은 에코 서버를 비동기로 쓰는 방법을 볼게요 (Supply.tap은 멀티스레드라는 점을 유의하세요. 이것이 바람직하지 않으면 Supply.act를 대신 쓰세요).

my $supply = IO::Socket::Async.listen('localhost', 8000);
my $server = $supply.tap(-> $conn {
    $conn.Supply.tap(-> $data {
        say $data; # OUTPUT: Hello, world!
        await $conn.print: $data;
        $conn.close;
    })
});

my $client = await IO::Socket::Async.connect('localhost', 8000);
$client.Supply.tap(-> $data {
    say $data; # OUTPUT: Hello, world!
    $client.close;
    $server.close;
});

await $client.print: 'Hello, world!';

Node.js의 동등한 코드는 이렇게 생겼어요.

const net = require('net');

const server = net.createServer(conn => {
    conn.setEncoding('utf8');
    conn.on('data', data => {
        console.log(data); # OUTPUT: Hello, world!
        conn.write(data);
        conn.end();
    });
}).listen(8000, 'localhost');

const client = net.createConnection(8000, 'localhost', () => {
    client.setEncoding('utf8');
    client.on('data', data => {
        console.log(data); # OUTPUT: Hello, world!
        client.end();
        server.close();
    });
    client.write("Hello, world!");
});

HTTP/HTTPS

Raku는 HTTP/HTTPS를 네이티브로 지원하지 않아요. 하지만 Cro 같은 CPAN 패키지가 그 빈자리를 메워줘요.

DNS

Raku는 현재 Node.js의 DNS 모듈이 구현하는 기능의 대부분을 지원하지 않아요. IO::Socket::INETIO::Socket::Async는 호스트 이름을 해석할 수 있지만, DNS 레코드 해석이나 역 IP 조회 같은 기능은 아직 구현되지 않았어요. Net::DNS::BIND::Manage처럼 DNS 지원을 개선하려는 진행 중인 모듈도 몇 개 있어요.

Punycode

Punycode 지원은 CPAN의 Net::LibIDN, Net::LibIDN2, IDNA::Punycode 모듈을 통해 이용할 수 있어요.

파일시스템 API (The filesystem API)

TBD

모듈과 패키지 (Modules and packages)

TBD