本页面旨在为熟悉 Node.js 的用户提供学习 Raku 的途径。我们将解释两种语言的共同特性,以及语法和功能上的主要差异。

这不是学习 Raku 的教程;这是针对已经具备 Node.js 中级到高级技能的用户提供的参考。

基本语法§

"Hello, world!"§

让我们从学习新语言时通常编写的第一个程序开始。在 Node.js 中,一个 hello world 程序将这样编写

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 中的大多数变量都有所谓的 sigils,即其名称前面的 $,字符串连接使用 ~ 运算符而不是 +。这两种语言的共同点是支持字符串插值。

现在基本示例已经介绍完毕,让我们更详细地解释这两种语言之间的相似之处。

变量§

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 中没有变量提升;变量在它们所在的代码行上定义和赋值,而不是在作用域顶部定义,然后在该行上赋值。

除了普通变量之外,Raku 中还有所谓的动态变量。动态变量使用调用者的作用域进行查找,而不是外部作用域。以下是 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

您可能已经注意到变量名前面的 $$* 符号。这些分别被称为 sigils 和 twigils,它们定义了变量所属的容器。有关 sigils、twigils 和容器的更多信息,请参阅有关 变量 的文档。

Node.js 中的变量可以与来自外部作用域的其他变量具有相同的名称,而不会发生冲突(尽管 linter 通常会根据其配置方式对此进行抱怨)

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

logDupe(2);       // 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 

运算符§

赋值§

= 运算符在两种语言中都具有相同的作用。

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: {} 

相等性§

Node.js 有两个相等运算符:=====

== 是松散相等运算符。当比较类型相同的操作数时,如果两个操作数相等,它将返回 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(如果它们类型不同),而是将它们强制转换为字符串

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

eq 的反义词是 ne!eq

=== 是严格相等运算符。如果两个操作数的值相同,则它将返回 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 运算符。此运算符也可用于检查深度相等性,通常需要在 Node.js 中使用库来实现。

say {=> 1} eqv {=> 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 

智能匹配§

Raku 还有一个用于比较值的运算符,但它不完全是相等运算符。这就是 ~~,智能匹配运算符。它有几种用途:它可以像 Node.js 中的 instanceof 一样使用,匹配正则表达式,以及检查值是否为哈希、包、集合或映射中的键。

say 'ayy lmao' ~~ Str# OUTPUT: True 
 
my %hash = => 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,Raku 中等效于 Node.js 对象上的 constructor 属性的是 WHAT 属性。

console.log('foo'.constructor); // OUTPUT: String

say 'foo'.WHAT# OUTPUT: Str 

数字§

Node.js 有 +-/*% 和(在 ES6 中)** 作为数字运算符。当操作数类型不同时,与相等运算符类似,它们会在进行操作之前转换为其基本类型,从而使这成为可能。

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

在 Raku 中,它们再次被转换为数字类型,如前所述。

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

此外,Raku 还有 div%%div 的行为类似于 C 中的 int 除法,而 %% 检查一个数字是否可以被另一个数字整除。

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

按位§

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 中,没有等效于 >>> 的运算符。所有按位运算符都以 + 为前缀,但按位取反使用 +^ 而不是 ~

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 

检查定义§

Javascript 包含一个空值合并运算符 ??,它仅在为空或未定义时才会进行。

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

这与 Raku // 运算符非常相似。

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

自定义运算符和运算符重载§

Node.js 不允许运算符重载,除非使用 Makefile 或使用自定义版本的 V8 构建 Node.js。Raku 本地允许自定义运算符和运算符重载!由于所有运算符都是子例程,因此您可以像这样定义自己的运算符。

# "distance operator": the distance of two numbers is the absolute value 
# of their difference 
multi infix:<|-|>($a$bis equiv(&infix:<->{ abs $a - $b }
 
say -1 |-| 3# OUTPUT: 4 

运算符可以定义为 prefixinfixpostfixis tighteris equivis looser 特性可选地定义运算符的优先级。在本例中,|-|- 具有相同的优先级。

请注意,在声明运算符子例程时如何使用 multi。这允许声明具有相同名称但具有不同签名的多个子例程。这将在 函数 部分中详细解释。现在,我们只需要知道它允许我们覆盖我们想要的任何原生运算符。

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

控制流§

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 === 16) {
    console.log('Boxcars!');
} else {
    console.log(`Rolled ${diceRoll}.`);
}

在 Raku 中,if/else 的工作方式基本相同,但有一些关键区别。第一,不需要括号。第二,else if 写成 elsif。第三,if 子句可以写在语句之后

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

或者,虽然效率较低,但这可以写成在语句之后使用 if

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

Raku 还有 when,它类似于 if,但如果给定的条件为真,则不会执行其所在块中 when 块之后的任何代码。

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

此外,Raku 还有 withorwithwithout,它们分别类似于 ifelse ifelse,但它们不是检查其条件是否为真,而是检查其是否已定义。

switch§

Switch 语句是一种检查给定值与一组值之间是否相等的方法,如果匹配,则运行一些代码。case 语句定义要比较的每个值。default(如果包含)充当给定值与任何情况都不匹配时的后备。匹配一个情况后,通常使用break 来防止执行匹配情况之后的情况的代码,尽管很少有意省略它。

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 语句一样使用。没有等效于 break 的内容,因为 when 块最常像 case 语句一样使用。switchgiven 之间的一个主要区别是,传递给 switch 语句的值只会匹配与该值完全相等的 case;given 值与 when 值进行智能匹配 (~~)。

my     @ranklist = [23456789'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                        { $_                      }
    };
}

如果有多个 when 块与传递给 given 的值匹配,并且您希望运行多个块,请使用 proceedsucceed 可用于退出其所在的 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 循环,因为它们适用于任何可迭代的东西。可以使用 loop 编写 C 样式循环,但这不建议这样做,因为它们最好写成使用范围的 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 中编写无限循环,请使用 loop 而不是 forwhile

在 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 中的功能,但已经提出了一项提案来将其添加到 ECMAScriptdo 表达式评估一个块并返回结果。

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 

类型§

创建类型§

在 JavaScript 中,类型是通过创建类(或 ES5 及更早版本中的构造函数)来创建的。如果您使用过 TypeScript,您可以将类型定义为其他类型的子集,如下所示

type ID = string | number;

在 Raku 中,类、角色、子集和枚举被认为是类型。创建类和角色将在本文的 OOP 部分中讨论。创建 ID 子集可以像这样完成

subset ID where Str | Int;

有关更多信息,请参阅有关subsetJunction 的文档。

TypeScript 枚举可以具有数字或字符串作为其值。定义值是可选的;默认情况下,第一个键的值为 0,下一个键的值为 1,下一个键的值为 2,依此类推。例如,以下是一个枚举,它为扩展 ASCII 箭头符号定义方向(可能用于 TUI 游戏)

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

Raku 中的枚举可以具有任何类型作为其键的值。枚举键(以及可选的值)可以通过编写 enum、枚举的名称,然后是键的列表(以及可选的值)来定义,这可以使用< >« »( ) 来完成。如果您想为枚举的键定义值,则必须使用 ( )。以下是 Raku 中编写的 Direction 枚举

enum Direction (
    UP    => '',
    DOWN  => '',
    LEFT  => '',
    RIGHT => ''
);

有关更多信息,请参阅有关enum 的文档。

使用类型§

在 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 中,变量可以通过将类型放在声明符 (myour 等) 和变量名称之间来进行类型化。分配与变量类型不匹配的值将抛出编译时或运行时错误,具体取决于值的评估方式。

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 类型§

以下是 JavaScript 类型及其在 Raku 中的等效类型的表格

JavaScriptRaku
ObjectMu、Any、Hash
ArrayList、Array、Seq
StringStr
NumberInt、Num、Rat
BooleanBool
MapMap、Hash
SetSet、SetHash

Object 既是 JavaScript 中所有类型的超类,也是创建哈希的一种方式。在 Raku 中,Mu 是所有类型的超类,尽管通常您希望使用 Any,它是 Mu 的子类,但也是几乎所有类型的超类,Junction 是一个例外。当使用 Object 作为哈希时,Hash 是您想要使用的。ObjectHash 之间的一个关键区别是 Object 保留其键的顺序;Hash 默认情况下不保留。

有三种类型等效于 ArrayArray 最类似于 Array,因为它充当可变数组。 List 类似于 Array,但它是不可变的。 Seq 用于创建延迟数组。

StringStr 在大多数情况下使用方式相同。

Raku 中有几种不同的类型等效于 Number,但您最常看到的三个是 IntNumRatInt 表示整数。 Num 表示浮点数,使其最类似于 NumberRat 表示两个数字的分数,当 Num 无法提供足够精确的值时使用。

BooleanBool 在大多数情况下使用方式相同。

Map 在 Raku 中既有可变的也有不可变的等效类型。 Map 是不可变的,而 Hash 是可变的。不要混淆它们!与 JavaScript 中的 Map 一样,MapHash 可以具有任何类型的键或值,而不仅仅是键的字符串。

Map 一样,Set 在 Raku 中也有可变的和不可变的等效类型。 Set 是不可变的,而 SetHash 是可变的。

函数§

# 待定

面向对象编程§

# 待定

异步编程§

# 待定

缓冲区§

NodeJS 使用类 BufferBlob 处理原始二进制数据,而 Raku 使用角色 BufBlob,它们分别是可变和不可变的缓冲区。在 Raku 中,`Buf` 组成一个 `Blob`,因此所有 `Blob` 方法都可用于 `Buf` 对象。

下表总结了 NodeJS 和 Raku 中缓冲区构造之间的异同

类/角色NodeJSRaku
`Buffer`/`Buf`固定长度的字节序列(没有 `push`、`pop` 等方法)可以动态增长或缩小的字节序列。您可以使用 `push`、`pop` 等方法。
使用 `for..of` 语法可迭代可以使用循环结构进行迭代。
可以使用数组索引更新每个字节,例如 `buf[i]++`。与 NodeJS 相同。
`Blob`固定长度的字节序列(没有 `push`、`pop` 等方法)与 NodeJS 相同。
不可迭代。可以使用循环结构进行迭代。
每个字节都是不可变的。与 NodeJS 相同。

创建缓冲区§

在 NodeJS 中,有几种方法可以创建新的缓冲区。您可以使用静态方法 Buffer.allocn 个字节的缓冲区分配内存,除非提供了 `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(897);
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(897);
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(11497107117);
say $blob# OUTPUT: «Blob:0x<72 61 6B 75>␤» 
 
my $buf = Buf.new(11497107117);
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(11497107117);
say $blob.decode('utf-8'); # OUTPUT: «raku␤» 

写入缓冲区§

在 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(05'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(57' 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>␤» 

从缓冲区读取§

有多种方法可以访问缓冲区中的数据,从访问单个字节到提取整个内容,再到解码其内容。

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 提供偏移量和长度,以仅从缓冲区读取相关字节。

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

在 Raku 中,您可以使用 decode 方法执行相同的操作。

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

但是,您不能同时对缓冲区进行切片和解码。相反,您可以使用 subbuf 从调用者缓冲区中提取相关部分,然后对返回的缓冲区进行解码。

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

更多有用的方法§

Buffer.isBuffer§

在 NodeJS 中,您可以使用 isBuffer 方法检查对象是否为缓冲区。

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

在 Raku 中,您可以与 BlobBuf 进行智能匹配(请记住,Buf 包含 Blob)。

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(020).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(1620);
$animal.splice(0$animal.bytes'🐪'.encode('utf-8'));
say $animal.decode# OUTPUT: «🐪␤» 
 
say $target.subbuf(020).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(164= '🐪'.encode('utf-8');
 
say $target.subbuf(020).decode('utf-8'); # OUTPUT: «Happy birthday! 🐪␤» 

网络 API§

Net§

在 Raku 中,有两个 API 用于处理网络:IO::Socket::INET(用于同步网络)和 IO::Socket::Async(用于异步网络)。

IO::Socket::INET 目前仅支持 TCP 连接。它的 API 类似于 C 的套接字 API。如果您熟悉该 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。但是,CPAN 上的包,例如 Cro,可以帮助弥补这一差距。

DNS§

Raku 目前不支持 Node.js 的 DNS 模块实现的大部分功能。 IO::Socket::INETIO::Socket::Async 可以解析主机名,但尚未实现解析 DNS 记录和反向 IP 查找等功能。有一些模块正在开发中,例如 Net::DNS::BIND::Manage,旨在改进 DNS 支持。

Punycode§

Punycode 支持可以通过 CPAN 上的 Net::LibIDNNet::LibIDN2IDNA::Punycode 模块获得。

文件系统 API§

# 待定

模块和包§

# 待定