本文档旨在向您简要介绍 Raku 编程语言。
对于 Raku 新手来说,它应该可以帮助您入门。
本文档的某些部分引用了Raku 文档的其他(更完整、更准确)部分。如果您需要有关特定主题的更多信息,则应阅读它们。
在本文档中,您将找到大多数讨论主题的示例。为了更好地理解它们,请花时间重现所有示例。
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。要查看此许可证的副本,请访问
如果您想为本文档做出贡献,请访问
欢迎所有反馈:[email protected]
如果您喜欢这项工作,请在Github上为该存储库点“星标”。
1. 简介
1.1. 什么是 Raku
Raku 是一种高级的、通用的、逐渐类型化的语言。Raku 是多范式的。它支持过程式、面向对象和函数式编程。
-
TMTOWTDI(发音为 Tim Toady):条条大路通罗马。
1.2. 术语
-
Raku:是一种具有测试套件的语言规范。通过规范测试套件的实现被认为是 Raku。
-
Rakudo:是 Raku 的编译器。
-
Zef:是 Raku 模块安装器。
-
Rakudo Star:是一个捆绑包,其中包括 Rakudo、Zef、Raku 模块集合和文档。
1.3. 安装 Raku
https://rakubrew.org 是一个平台无关的环境管理器(类似于 Raku 的 pyenv)
要安装 Rakudo Star,请在终端中运行以下命令
mkdir ~/rakudo && cd $_
curl -LJO https://rakudo.perl5.cn/latest/star/src
tar -xzf rakudo-star-*.tar.gz
mv rakudo-star-*/* .
rm -fr rakudo-star-*
./bin/rstar install
echo "export PATH=$(pwd)/bin/:$(pwd)/share/perl6/site/bin:$(pwd)/share/perl6/vendor/bin:$(pwd)/share/perl6/core/bin:\$PATH" >> ~/.bashrc
source ~/.bashrc
有关其他 Unix 选项,请访问 https://rakudo.perl5.cn/star/source
有四种选择
-
按照安装 Linux 时列出的相同步骤操作
-
使用 Homebrew 安装:
brew install rakudo-star
-
使用 MacPorts 安装:
sudo port install rakudo
-
从 https://rakudo.perl5.cn/latest/star/macos 获取最新的安装程序(扩展名为 .dmg 的文件)
-
对于 64 位架构:从 https://rakudo.perl5.cn/latest/star/win 获取最新的安装程序(扩展名为 .msi 的文件)
-
安装后,请确保
C:\rakudo\bin
在 PATH 中
-
获取官方 Docker 镜像
docker pull rakudo-star
-
然后使用该镜像运行容器
docker run -it rakudo-star
1.4. 运行 Raku 代码
可以使用 REPL(读取-求值-打印循环)运行 Raku 代码。为此,请打开终端,在终端窗口中键入 raku
,然后按 [Enter] 键。这将导致出现 >
提示符。接下来,键入一行代码并按 [Enter] 键。REPL 将打印出该行的值。然后,您可以键入另一行,或者键入 exit
并按 [Enter] 键退出 REPL。
或者,将代码写入文件,保存并运行它。建议 Raku 脚本的文件扩展名为 .raku
。通过在终端窗口中键入 raku filename.raku
并按 [Enter] 键来运行文件。与 REPL 不同,这不会自动打印每行的结果:代码必须包含类似 say
的语句才能打印输出。
REPL 主要用于尝试特定的代码段,通常是一行代码。对于包含多行代码的程序,建议将其存储在文件中,然后运行它们。
也可以通过在命令行上键入 raku -e 'your code here'
并按 [Enter] 键来非交互式地尝试单行代码。
Rakudo Star 捆绑了一个行编辑器,可以帮助您充分利用 REPL。 如果您安装的是纯 Rakudo 而不是 Rakudo Star,那么您可能没有启用行编辑功能(使用向上和向下箭头查看历史记录,使用向左和向右箭头编辑输入,使用 Tab 键自动完成)。请考虑运行以下命令,您将一切就绪
|
1.5. 编辑器
由于大多数情况下我们将编写 Raku 程序并将其存储在文件中,因此我们应该有一个可以识别 Raku 语法的合适的文本编辑器。
社区经常使用以下编辑器
-
Visual Studio Code:使用 raku-navigator 启用语法高亮显示和错误检查
-
Nano:使用 raku.nanorc 启用语法高亮显示
-
Notepad++:开箱即用地提供语法高亮显示
-
Kate:开箱即用地提供语法高亮显示
1.6. 你好,世界!
我们将从 hello world
仪式开始。
say 'hello world';
也可以写成
'hello world'.say;
1.7. 语法概述
Raku 是自由格式的:大多数情况下,您可以随意使用任意数量的空格,尽管在某些情况下空格具有含义。
语句通常是用分号分隔的单行代码
say "Hello" if True;
say "World" if False;
文件或代码块中的最后一条语句后不需要分号,但最好还是加上它们。
块可以包含一组语句。用花括号括起语句以创建块
{
say "First statement in the block.";
say "Second statement in the block.";
}
表达式是一种特殊的语句,它返回一个值:1+2
将返回 3
表达式由项和运算符组成。
项是
-
变量:可以操作和更改的值。
-
字面量:常量值,如数字或字符串。
运算符按类型分类
类型 |
说明 |
示例 |
前缀 |
在项之前。 |
|
中缀 |
在项之间 |
|
后缀 |
在项之后 |
|
环绕 |
围绕项 |
|
后环绕 |
在一个项之后,围绕另一个项 |
|
1.7.1. 标识符
标识符是在定义项时赋予它们的名称。
-
它们必须以字母字符或下划线开头。
-
它们可以包含数字(第一个字符除外)。
-
它们可以包含破折号或撇号(第一个和最后一个字符除外),前提是每个破折号或撇号的右侧都有一个字母字符。
有效 |
无效 |
|
|
|
|
|
|
|
|
|
|
-
驼峰式大小写:
variableNo1
-
烤肉串式大小写:
variable-no1
-
蛇形式大小写:
variable_no1
您可以随意命名标识符,但最好始终采用一种命名约定。
使用有意义的名称将使您(和其他人)的编程生活更轻松。
-
var1 = var2 * var3
在语法上是正确的,但其目的并不明显。 -
monthly-salary = daily-rate * working-days
将是命名变量的更好方法。
1.7.2. 注释
注释是编译器忽略并用作注释的文本。
注释分为 3 种类型
-
单行
# This is a single line comment
-
嵌入式
say #`(This is an embedded comment) "Hello World."
-
多行
=begin comment This is a multi line comment. Comment 1 Comment 2 =end comment
1.7.3. 引号
字符串需要用双引号或单引号分隔。
始终使用双引号
-
如果您的字符串包含撇号。
-
如果您的字符串包含需要插值的变量。
say 'Hello World'; # Hello World
say "Hello World"; # Hello World
say "Don't"; # Don't
my $name = 'John Doe';
say 'Hello $name'; # Hello $name
say "Hello $name"; # Hello John Doe
2. 运算符
2.1. 常用运算符
下表列出了最常用的运算符。
运算符 | 类型 | 描述 | 示例 | 结果 |
---|---|---|---|---|
|
|
加法 |
|
|
|
|
减法 |
|
|
|
|
乘法 |
|
|
|
|
幂 |
|
|
|
|
除法 |
|
|
|
|
整数除法(向下取整) |
|
|
|
|
模数 |
|
|
|
|
可整除性 |
|
|
|
|
|||
|
|
最大公约数 |
|
|
|
|
最小公倍数 |
|
|
|
|
数值相等 |
|
|
|
|
数值不相等 |
|
|
|
|
数值小于 |
|
|
|
|
数值大于 |
|
|
|
|
数值小于或等于 |
|
|
|
|
数值大于或等于 |
|
|
|
|
数值三向比较器 |
|
|
|
|
|||
|
|
|||
|
|
字符串相等 |
|
|
|
|
字符串不相等 |
|
|
|
|
字符串小于 |
|
|
|
|
字符串大于 |
|
|
|
|
字符串小于或等于 |
|
|
|
|
字符串大于或等于 |
|
|
|
|
字符串三向比较器 |
|
|
|
|
|||
|
|
|||
|
|
智能三向比较器 |
|
|
|
|
|||
|
|
赋值 |
|
|
|
|
字符串连接 |
|
|
|
|
|||
|
|
字符串复制 |
|
|
|
|
|||
|
|
智能匹配 |
|
|
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
递增 |
|
|
|
递增 |
|
|
|
|
|
递减 |
|
|
|
递减 |
|
|
|
|
|
将操作数强制转换为数值 |
|
|
|
|
|||
|
|
|||
|
|
将操作数强制转换为数值并返回其负值 |
|
|
|
|
|||
|
|
|||
|
|
将操作数强制转换为布尔值 |
|
|
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
将操作数强制转换为布尔值并返回其否定值 |
|
|
|
|
范围构造函数 |
|
|
|
|
范围构造函数 |
|
|
|
|
范围构造函数 |
|
|
|
|
范围构造函数 |
|
|
|
|
范围构造函数 |
|
|
|
|
惰性列表构造函数 |
|
|
|
|
扁平化 |
|
|
|
|
2.2. 反向运算符
在任何运算符之前添加 R
将会反转其操作数。
正常操作 | 结果 | 反转运算符 | 结果 |
---|---|---|---|
|
|
|
|
|
|
|
|
2.3. 归约运算符
归约运算符作用于值列表。它们是通过用方括号 []
将运算符括起来形成的
正常操作 | 结果 | 归约运算符 | 结果 |
---|---|---|---|
|
|
|
|
|
|
|
|
有关运算符的完整列表,包括其优先级,请访问 https://raku-docs.perl5.cn/language/operators |
3. 变量
Raku 变量分为 3 类:标量、数组和哈希。
符号(拉丁语中的 Sign)是用于对变量进行分类的前缀字符。
-
$
用于标量 -
@
用于数组 -
%
用于哈希
本指南提供了一个简化的变量模型,适合学习 Raku 的基础知识。要更深入地了解变量,请参阅 https://raku-docs.perl5.cn/language/containers |
3.1. 标量
标量保存一个值或引用。
# String
my $name = 'John Doe';
say $name;
# Integer
my $age = 99;
say $age;
可以对标量执行一组特定的操作,具体取决于它保存的值。
my $name = 'John Doe';
say $name.uc;
say $name.chars;
say $name.flip;
JOHN DOE
8
eoD nhoJ
有关适用于字符串的完整方法列表,请参阅 https://raku-docs.perl5.cn/type/Str |
my $age = 17;
say $age.is-prime;
True
有关适用于整数的完整方法列表,请参阅 https://raku-docs.perl5.cn/type/Int |
my $age = 2.3;
say $age.numerator;
say $age.denominator;
say $age.nude;
23
10
(23 10)
有关适用于有理数的完整方法列表,请参阅 https://raku-docs.perl5.cn/type/Rat |
3.2. 数组
数组是包含多个值的列表。
my @animals = 'camel','llama','owl';
say @animals;
如下例所示,可以对数组执行许多操作
波浪号 ~ 用于字符串连接。
|
脚本
my @animals = 'camel','vicuña','llama';
say "The zoo contains " ~ @animals.elems ~ " animals";
say "The animals are: " ~ @animals;
say "I will adopt an owl for the zoo";
@animals.push("owl");
say "Now my zoo has: " ~ @animals;
say "The first animal we adopted was the " ~ @animals[0];
@animals.pop;
say "Unfortunately the owl got away and we're left with: " ~ @animals;
say "We're closing the zoo and keeping one animal only";
say "We're going to let go: " ~ @animals.splice(1,2) ~ " and keep the " ~ @animals;
输出
The zoo contains 3 animals
The animals are: camel vicuña llama
I will adopt an owl for the zoo
Now my zoo has: camel vicuña llama owl
The first animal we adopted was the camel
Unfortunately the owl got away and we're left with: camel vicuña llama
We're closing the zoo and keeping one animal only
We're going to let go: vicuña llama and keep the camel
.elems
返回数组中元素的数量。
.push()
向数组添加一个或多个元素。
我们可以通过指定数组中元素的位置 @animals[0]
来访问该元素。
.pop
从数组中移除最后一个元素并返回它。
.splice(a,b)
将从位置 a
开始移除 b
个元素。
3.2.1. 固定大小数组
基本数组的声明如下
my @array;
基本数组可以具有无限长度,因此称为自动扩展数组。
该数组将接受任意数量的值,没有任何限制。
相反,我们也可以创建固定大小的数组。
无法访问超出其定义大小的数组元素。
要声明固定大小的数组,请在其名称后面的方括号中指定其最大元素数
my @array[3];
此数组最多可以容纳 3 个值,索引从 0 到 2。
my @array[3];
@array[0] = "first value";
@array[1] = "second value";
@array[2] = "third value";
您将无法向此数组添加第四个值
my @array[3];
@array[0] = "first value";
@array[1] = "second value";
@array[2] = "third value";
@array[3] = "fourth value";
Index 3 for dimension 1 out of range (must be 0..2)
3.2.2. 多维数组
到目前为止,我们看到的数组都是一维的。
幸运的是,我们可以在 Raku 中定义多维数组。
my @tbl[3;2];
此数组是二维的。第一个维度最多可以有 3 个值,第二个维度最多可以有 2 个值。
可以将其视为 3x2 的网格。
my @tbl[3;2];
@tbl[0;0] = 1;
@tbl[0;1] = "x";
@tbl[1;0] = 2;
@tbl[1;1] = "y";
@tbl[2;0] = 3;
@tbl[2;1] = "z";
say @tbl
[[1 x] [2 y] [3 z]]
[1 x]
[2 y]
[3 z]
有关完整的数组参考,请参阅 https://raku-docs.perl5.cn/type/Array |
3.3. 哈希
my %capitals = 'UK','London','Germany','Berlin';
say %capitals;
my %capitals = UK => 'London', Germany => 'Berlin';
say %capitals;
可以对哈希调用的一些方法是
脚本
my %capitals = UK => 'London', Germany => 'Berlin';
%capitals.push: (France => 'Paris');
say %capitals.kv;
say %capitals.keys;
say %capitals.values;
say "The capital of France is: " ~ %capitals<France>;
输出
(France Paris Germany Berlin UK London)
(France Germany UK)
(Paris Berlin London)
The capital of France is: Paris
.push: (key => 'Value')
添加一个新的键/值对。
.kv
返回一个包含所有键和值的列表。
.keys
返回一个包含所有键的列表。
.values
返回一个包含所有值的列表。
我们可以通过指定其键 %hash<key>
来访问哈希中的特定值
有关完整的哈希参考,请参阅 https://raku-docs.perl5.cn/type/Hash |
3.4. 类型
在前面的示例中,我们没有指定变量应该保存什么类型的值。
.WHAT 将返回变量中保存的值的类型。
|
my $var = 'Text';
say $var;
say $var.WHAT;
$var = 123;
say $var;
say $var.WHAT;
如您在上面的示例中所见,$var
中值的类型曾经是 (Str),然后是 (Int)。
这种编程风格称为动态类型。动态是指变量可以包含任何类型的值。
现在尝试运行以下示例
请注意变量名称前的 Int
。
my Int $var = 'Text';
say $var;
say $var.WHAT;
它将失败并返回此错误消息:在赋值给 $var 时类型检查失败;预期为 Int,但得到 Str
发生的情况是,我们事先指定了变量的类型应该是 (Int)。当我们尝试为其分配 (Str) 时,它失败了。
这种编程风格称为静态类型。静态是指变量类型在赋值之前定义,并且不能更改。
Raku 被归类为渐进类型;它允许静态和动态类型。
my Int @array = 1,2,3;
say @array;
say @array.WHAT;
my Str @multilingual = "Hello","Salut","Hallo","您好","안녕하세요","こんにちは";
say @multilingual;
say @multilingual.WHAT;
my Str %capitals = UK => 'London', Germany => 'Berlin';
say %capitals;
say %capitals.WHAT;
my Int %country-codes = UK => 44, Germany => 49;
say %country-codes;
say %country-codes.WHAT;
您很可能永远不会使用前两种类型,但为了提供信息,这里还是列出了它们。
|
|
|
|
|
|
||
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.5. 自省
自省是获取有关对象属性(如其类型)的信息的过程。
在前面的示例中,我们使用 .WHAT
返回变量的类型。
my Int $var;
say $var.WHAT; # (Int)
my $var2;
say $var2.WHAT; # (Any)
$var2 = 1;
say $var2.WHAT; # (Int)
$var2 = "Hello";
say $var2.WHAT; # (Str)
$var2 = True;
say $var2.WHAT; # (Bool)
$var2 = Nil;
say $var2.WHAT; # (Any)
保存值的变量的类型与其值相关。
强声明的空变量的类型是声明它时使用的类型。
未进行强声明的空变量的类型为 (Any)
要清除变量的值,请为其分配 Nil
。
3.6. 作用域
在首次使用变量之前,需要声明它。
Raku 中使用了几种声明符。到目前为止,我们一直在使用 my
。
my $var=1;
my
声明符为变量提供了词法作用域。换句话说,该变量只能在其声明的同一个块中访问。
Raku 中的代码块由 { }
界定。如果没有找到代码块,则变量将在整个 Raku 脚本中可用。
{
my Str $var = 'Text';
say $var; # is accessible
}
say $var; # is not accessible, returns an error
由于变量只能在其定义的代码块中访问,因此相同的变量名可以在另一个代码块中使用。
{
my Str $var = 'Text';
say $var;
}
my Int $var = 123;
say $var;
3.7. 赋值与绑定
我们在前面的例子中已经看到了如何给变量赋值。
赋值使用 =
运算符完成。
my Int $var = 123;
say $var;
我们可以更改分配给变量的值
my Int $var = 123;
say $var;
$var = 999;
say $var;
输出
123
999
另一方面,我们不能更改绑定到变量的值。
绑定使用 :=
运算符完成。
my Int $var := 123;
say $var;
$var = 999;
say $var;
输出
123
Cannot assign to an immutable value
my $a;
my $b;
$b := $a;
$a = 7;
say $b;
$b = 8;
say $a;
输出
7
8
绑定变量是双向的。
$a := $b
和 $b := $a
具有相同的效果。
有关变量的更多信息,请参阅 https://raku-docs.perl5.cn/language/variables |
4. 函数和修改器
区分函数和修改器非常重要。
函数不会更改调用它们的对象的状态。
修改器会修改对象的状态。
脚本
1
2
3
4
5
6
7
8
9
10
my @numbers = [7,2,4,9,11,3];
@numbers.push(99);
say @numbers; #1
say @numbers.sort; #2
say @numbers; #3
@numbers.=sort;
say @numbers; #4
输出
[7 2 4 9 11 3 99] #1
(2 3 4 7 9 11 99) #2
[7 2 4 9 11 3 99] #3
[2 3 4 7 9 11 99] #4
.push
是一个修改器;它会更改数组的状态(#1)
.sort
是一个函数;它返回一个排序后的数组,但不会修改初始数组的状态
-
(#2)表明它返回了一个排序后的数组。
-
(#3)表明初始数组仍然未被修改。
为了强制函数充当修改器,我们使用 .=
而不是 .
(#4)(脚本的第 9 行)
5. 循环和条件
Raku 有许多条件和循环结构。
5.1. if
代码仅在满足条件时运行;即表达式计算结果为 True
。
my $age = 19;
if $age > 18 {
say 'Welcome'
}
在 Raku 中,我们可以反转代码和条件。
即使代码和条件已反转,也始终先评估条件。
my $age = 19;
say 'Welcome' if $age > 18;
如果不满足条件,我们可以使用以下方法指定要执行的备用块
-
else
-
elsif
# run the same code for different values of the variable
my $number-of-seats = 9;
if $number-of-seats <= 5 {
say 'I am a sedan'
} elsif $number-of-seats <= 7 {
say 'I am 7 seater'
} else {
say 'I am a van'
}
5.2. unless
if 语句的否定版本可以使用 unless
编写。
以下代码
my $clean-shoes = False;
if not $clean-shoes {
say 'Clean your shoes'
}
可以写成
my $clean-shoes = False;
unless $clean-shoes {
say 'Clean your shoes'
}
Raku 中的否定使用 !
或 not
完成。
使用 unless (condition)
而不是 if not (condition)
。
unless
不能有 else
子句。
5.3. with
with
的行为类似于 if
语句,但会检查变量是否已定义。
my Int $var=1;
with $var {
say 'Hello'
}
如果您在没有为变量赋值的情况下运行代码,则不会发生任何事情。
my Int $var;
with $var {
say 'Hello'
}
without
是 with
的否定版本。您应该能够将其与 unless
联系起来。
如果第一个 with
条件不满足,则可以使用 orwith
指定备用路径。
with
和 orwith
可以与 if
和 elsif
进行比较。
5.4. for
for
循环迭代多个值。
my @array = 1,2,3;
for @array -> $array-item {
say $array-item * 100
}
请注意,我们创建了一个迭代变量 $array-item
,然后对每个数组项执行了 *100
操作。
5.5. given
given
是 Raku 中其他语言中 switch 语句的等效项,但功能更强大。
my $var = 42;
given $var {
when 0..50 { say 'Less than or equal to 50'}
when Int { say "is an Int" }
when 42 { say 42 }
default { say "huh?" }
}
成功匹配后,匹配过程将停止。
或者,proceed
将指示 Raku 即使在成功匹配后也继续匹配。
my $var = 42;
given $var {
when 0..50 { say 'Less than or equal to 50';proceed}
when Int { say "is an Int";proceed}
when 42 { say 42 }
default { say "huh?" }
}
5.6. loop
loop
是编写 for
循环的另一种方式。
实际上,loop
是在 C 系列编程语言中编写 for
循环的方式。
Raku 属于 C 系列语言。
loop (my $i = 0; $i < 5; $i++) {
say "The current number is $i"
}
有关循环和条件的更多信息,请参阅 https://raku-docs.perl5.cn/language/control |
6. 输入/输出
在 Raku 中,两个最常见的*输入/输出*接口是*终端*和*文件*。
6.1. 使用终端进行基本输入/输出
6.1.1. say
say
写入标准输出。它在末尾追加一个换行符。换句话说,以下代码
say 'Hello Mam.';
say 'Hello Sir.';
将写入 2 个不同的行。
6.1.2. print
另一方面,print
的行为类似于 say
,但不会添加新行。
尝试用 print
替换 say
并比较结果。
6.1.3. get
get
用于从终端捕获输入。
my $name;
say "Hi, what's your name?";
$name = get;
say "Dear $name welcome to Raku";
当上述代码运行时,终端将等待您输入您的姓名。输入它,然后按 [Enter]。随后,它会向您问好。
6.1.4. prompt
prompt
是 print
和 get
的组合。
上面的例子可以这样写
my $name = prompt "Hi, what's your name? ";
say "Dear $name welcome to Raku";
6.2. 运行 Shell 命令
可以使用两个子例程来运行 shell 命令
-
run
运行外部命令而不涉及 shell -
shell
通过系统 shell 运行命令。它取决于平台和 shell。所有 shell 元字符都由 shell 解释,包括管道、重定向、环境变量替换等。
my $name = 'Neo';
run 'echo', "hello $name";
shell "ls";
shell "dir";
echo
和 ls
是 Linux 上常见的 shell 关键字
echo
将文本打印到终端(相当于 Raku 中的 say
)
ls
列出当前目录中的所有文件和文件夹
dir
等效于 Windows 上的 ls
。
6.3. 文件输入/输出
6.3.1. slurp
slurp
用于从文件中读取数据。
创建一个包含以下内容的文本文件
John 9
Johnnie 7
Jane 8
Joanna 7
my $data = slurp "datafile.txt";
say $data;
6.3.2. spurt
spurt
用于将数据写入文件。
my $newdata = "New scores:
Paul 10
Paulie 9
Paulo 11";
spurt "newdatafile.txt", $newdata;
运行上述代码后,将创建一个名为 newdatafile.txt 的新文件。它将包含新的分数。
6.4. 处理文件和目录
Raku 可以列出目录的内容,而无需借助 shell 命令(例如,使用 ls
)。
say dir; # List files and folders in the current directory
say dir "/Documents"; # List files and folders in the specified directory
此外,您还可以创建和删除目录。
mkdir "newfolder";
rmdir "newfolder";
mkdir
创建一个新目录。
rmdir
删除一个空目录,如果目录不为空,则返回错误。
您还可以检查路径是否存在;它是文件还是目录
在您将运行以下脚本的目录中,创建一个空文件夹 folder123
和一个空的 raku 文件 script123.raku
say "script123.raku".IO.e;
say "folder123".IO.e;
say "script123.raku".IO.d;
say "folder123".IO.d;
say "script123.raku".IO.f;
say "folder123".IO.f;
IO.e
检查目录/文件是否存在。
IO.f
检查路径是否为文件。
IO.d
检查路径是否为目录。
Windows 用户可以使用 / 或 \\ 来定义目录C:\\rakudo\\bin C:/rakudo/bin |
有关 I/O 的更多信息,请参阅 https://raku-docs.perl5.cn/type/IO |
7. 子例程
7.1. 定义
子例程(也称为 subs 或 函数)是一种打包和重用功能的方法。
子例程定义以关键字 sub
开头。在定义之后,可以通过它们的句柄调用它们。
查看以下示例
sub alien-greeting {
say "Hello earthlings";
}
alien-greeting;
前面的示例展示了一个不需要任何输入的子例程。
7.2. 签名
子例程可能需要输入。该输入由参数提供。子例程可以定义零个或多个参数。子例程定义的参数的数量和类型称为其签名。
以下子例程接受一个字符串参数。
sub say-hello (Str $name) {
say "Hello " ~ $name ~ "!!!!"
}
say-hello "Paul";
say-hello "Paula";
7.3. 多重分派
可以定义多个具有相同名称但签名不同的子例程。调用子例程时,运行时环境将根据提供的参数的数量和类型决定使用哪个版本。这种类型的子例程的定义方式与普通子例程相同,只是我们使用 multi
关键字而不是 sub
。
multi greet($name) {
say "Good morning $name";
}
multi greet($name, $title) {
say "Good morning $title $name";
}
greet "Johnnie";
greet "Laura","Mrs.";
7.4. 默认参数和可选参数
如果定义了一个子例程来接受一个参数,而我们在调用它时没有提供所需的 参数,它将失败。
Raku 为我们提供了定义具有以下内容的子例程的能力
-
可选参数
-
默认参数
可选参数是通过在参数名称后附加 ?
来定义的。
sub say-hello($name?) {
with $name { say "Hello " ~ $name }
else { say "Hello Human" }
}
say-hello;
say-hello("Laura");
如果用户不需要提供参数,则可以定义默认值。
这是通过在子例程定义中为参数赋值来完成的。
sub say-hello($name="Matt") {
say "Hello " ~ $name;
}
say-hello;
say-hello("Laura");
7.5. 返回值
到目前为止,我们看到的所有子例程都做了一些事情 ——它们在终端上显示一些文本。
但是,有时我们会为了它的返回值而执行一个子例程,以便我们可以在程序流程的稍后阶段使用它。
如果允许函数运行到其块的末尾,则最后一条语句或表达式将决定返回值。
sub squared ($x) {
$x ** 2;
}
say "7 squared is equal to " ~ squared(7);
为了清楚起见,最好明确地指定我们想要返回的内容。这可以使用 return
关键字来完成。
sub squared ($x) {
return $x ** 2;
}
say "7 squared is equal to " ~ squared(7);
7.5.1. 限制返回值
在前面的示例之一中,我们看到了如何限制接受的参数为特定类型。返回值也可以这样做。
要将返回值限制为特定类型,我们在签名中使用箭头符号 -->
。
sub squared ($x --> Int) {
return $x ** 2;
}
say "1.2 squared is equal to " ~ squared(1.2);
如果我们未能提供与类型约束匹配的返回值,则会引发错误。
Type check failed for return value; expected Int but got Rat (1.44)
类型约束不仅可以控制返回值的类型;它们还可以控制其定义性。 在前面的示例中,我们指定返回值应为 我们还可以使用以下签名指定返回的 话虽如此,最好使用这些类型约束。
|
有关子例程和函数的更多信息,请参阅 https://raku-docs.perl5.cn/language/functions |
8. 函数式编程
在本章中,我们将介绍一些有助于函数式编程的功能。
8.1. 函数是一等公民
函数/子例程是一等公民
-
它们可以作为参数传递
-
它们可以从其他函数返回
-
它们可以赋值给变量
一个很好的例子是 map
函数。
map
是一个高阶函数,它可以接受另一个函数作为参数。
my @array = <1 2 3 4 5>;
sub squared($x) {
$x ** 2
}
say map(&squared,@array);
(1 4 9 16 25)
我们定义了一个名为 squared
的子例程,它接受一个参数并将该参数与其自身相乘。
接下来,我们使用 map
(一个高阶函数),并为其提供了两个参数,即 squared
子例程和一个数组。
结果是数组中元素的平方列表。
请注意,在将子例程作为参数传递时,我们需要在其名称前添加 &
。
8.2. 匿名函数
匿名函数也称为lambda。
匿名函数不绑定到标识符(它没有名称)。
让我们重写 map
示例,并使用匿名函数
my @array = <1 2 3 4 5>;
say map(-> $x {$x ** 2},@array);
请注意,我们没有声明 squared 子例程并将其作为参数传递给 map
,而是在匿名子例程中将其定义为 -> $x {$x ** 2}
。
在 Raku 语境中,我们称这种表示法为尖括号块
my $squared = -> $x {
$x ** 2
}
say $squared(9);
8.3. 链式调用
在 Raku 中,方法可以链接,因此您不需要将一个方法的结果作为参数传递给另一个方法。
举例说明:给定一个数组,您可能需要返回该数组的唯一值,并按从大到小的顺序排序。
这是一个非链式解决方案
my @array = <7 8 9 0 1 2 4 3 5 6 7 8 9>;
my @final-array = reverse(sort(unique(@array)));
say @final-array;
在这里,我们对 @array
调用 unique
,将结果作为参数传递给 sort
,然后将该结果传递给 reverse
。
相比之下,使用链式方法,上面的示例可以改写为
my @array = <7 8 9 0 1 2 4 3 5 6 7 8 9>;
my @final-array = @array.unique.sort.reverse;
say @final-array;
您已经可以看到,链式方法更易于阅读。
8.4. Feed 运算符
feed 运算符,在某些函数式编程语言中称为管道,进一步说明了方法链接。
my @array = <7 8 9 0 1 2 4 3 5 6 7 8 9>;
@array ==> unique()
==> sort()
==> reverse()
==> my @final-array;
say @final-array;
Start with `@array` then return a list of unique elements
then sort it
then reverse it
then store the result in @final-array
请注意,方法调用的流程是自上而下的,从第一步到最后一步。
my @array = <7 8 9 0 1 2 4 3 5 6 7 8 9>;
my @final-array-v2 <== reverse()
<== sort()
<== unique()
<== @array;
say @final-array-v2;
后向 feed 类似于前向 feed,但方向相反。
方法调用的流程是自下而上的,从最后一步到第一步。
8.5. Hyper 运算符
超级运算符 >>.
将对列表的所有元素调用一个方法,并返回结果列表。
my @array = <0 1 2 3 4 5 6 7 8 9 10>;
sub is-even($var) { $var %% 2 };
say @array>>.is-prime;
say @array>>.&is-even;
使用超级运算符,我们可以调用 Raku 中已经定义的方法,例如 is-prime
,它告诉我们一个数字是否是素数。
此外,我们可以定义新的子例程,并使用超级运算符调用它们。在这种情况下,我们必须在方法名称前加上 &
;例如,&is-even
。
这非常实用,因为它使我们不必编写 for
循环来迭代每个值。
Raku 保证结果的顺序与原始列表的顺序相同。但是,不能保证 Raku 实际上会按列表顺序或在同一线程中调用方法。因此,请谨慎使用具有副作用的方法,例如 say 或 print 。
|
8.6. 连接
连接是值的逻辑叠加。
在下面的示例中,1|2|3
是一个连接。
my $var = 2;
if $var == 1|2|3 {
say "The variable is 1 or 2 or 3"
}
连接的使用通常会触发自动线程化;对每个连接元素执行操作,并将所有结果组合成一个新的连接并返回。
8.7. 惰性列表
惰性列表是一个惰性求值的列表。
惰性求值会延迟表达式的求值,直到需要时才进行,并通过将结果存储在查找表中来避免重复求值。
优点包括
-
通过避免不必要的计算来提高性能
-
能够构建潜在的无限数据结构
-
能够定义控制流程
要构建惰性列表,我们使用中缀运算符 …
惰性列表具有初始元素、生成器和终点。
my $lazylist = (1 ... 10);
say $lazylist;
初始元素是 1,终点是 10。没有定义生成器,因此默认生成器是后继者 (+1)
换句话说,这个惰性列表可以返回(如果请求)以下元素 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
my $lazylist = (1 ... Inf);
say $lazylist;
此列表可以返回(如果请求)1 到无穷大之间的任何整数,换句话说,任何整数。
my $lazylist = (0,2 ... 10);
say $lazylist;
初始元素是 0 和 2,终点是 10。没有定义生成器,但使用初始元素,Raku 将推断生成器是 (+2)
此惰性列表可以返回(如果请求)以下元素 (0, 2, 4, 6, 8, 10)
my $lazylist = (0, { $_ + 3 } ... 12);
say $lazylist;
在此示例中,我们显式定义了一个包含在 { }
中的生成器
此惰性列表可以返回(如果请求)以下元素 (0, 3, 6, 9, 12)
使用显式生成器时,终点必须是生成器可以返回的值之一。 或者,您可以将 这不会停止生成器
这将停止生成器
|
8.8. 闭包
Raku 中的所有代码对象都是闭包,这意味着它们可以引用外部作用域中的词法变量。
sub generate-greeting {
my $name = "John Doe";
sub greeting {
say "Good Morning $name";
};
return &greeting;
}
my $generated = generate-greeting;
$generated();
如果运行上面的代码,它将在终端上显示 早上好,张三
。
虽然结果相当简单,但这个例子有趣的是,内部子例程 greeting
在执行之前从外部子例程返回。
$generated
已成为一个闭包。
闭包是一种特殊类型的对象,它结合了两个东西
-
一个子例程
-
创建该子例程的环境。
环境包含闭包创建时作用域内的所有局部变量。在本例中,$generated
是一个闭包,它包含了 greeting
子例程和创建闭包时存在的字符串 John Doe
。
让我们来看一个更有趣的例子。
sub greeting-generator($period) {
return sub ($name) {
return "Good $period $name"
}
}
my $morning = greeting-generator("Morning");
my $evening = greeting-generator("Evening");
say $morning("John");
say $evening("Jane");
在本例中,我们定义了一个子例程 greeting-generator($period)
,它接受一个参数 $period
并返回一个新的子例程。它返回的子例程接受一个参数 $name
并返回构造的问候语。
基本上,greeting-generator
是一个子例程工厂。在本例中,我们使用 greeting-generator
创建了两个新的子例程,一个说 早上好
,另一个说 晚上好
。
$morning
和 $evening
都是闭包。它们共享相同的子例程体定义,但存储不同的环境。
在 $morning
的环境中,$period
是 Morning
。在 $evening
的环境中,$period
是 Evening
。
9. 类和对象
在上一章中,我们学习了 Raku 如何促进函数式编程。
在本章中,我们将了解 Raku 中的面向对象编程。
9.1. 简介
面向对象编程是当今广泛使用的范例之一。
对象是变量和子例程的集合。
变量称为属性,子例程称为方法。
属性定义状态,方法定义对象的行为。
类是创建对象的模板。
为了理解这种关系,请考虑以下示例
一个房间里有 4 个人 |
对象 ⇒ 4 个人 |
这 4 个人都是人类 |
类 ⇒ 人类 |
他们有不同的姓名、年龄、性别和国籍 |
属性 ⇒ 姓名、年龄、性别、国籍 |
在面向对象的说法中,我们说对象是类的实例。
考虑以下脚本
class Human {
has $.name;
has $.age;
has $.sex;
has $.nationality;
}
my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
say $john;
class
关键字用于定义类。
has
关键字用于定义类的属性。
.new()
方法称为构造函数。它创建作为调用它的类的实例的对象。
在上面的脚本中,新变量 $john
持有一个对 Human.new()
定义的“Human”新实例的引用。
传递给 .new()
方法的参数用于设置底层对象的属性。
可以使用 my
为类提供词法作用域
my class Human {
}
9.2. 封装
封装是一种面向对象的思想,它将一组数据和方法捆绑在一起。
对象中的数据(属性)应该是私有的,换句话说,只能从对象内部访问。
为了从对象外部访问属性,我们使用称为访问器的方法。
以下两个脚本具有相同的结果。
my $var = 7;
say $var;
my $var = 7;
sub sayvar {
$var;
}
say sayvar;
方法 sayvar
是一个访问器。它允许我们访问变量的值,而无需直接访问它。
Raku 中通过使用twigils 来促进封装。
Twigils 是次要的符号。它们位于符号和属性名称之间。
类中使用两种 twigils
-
!
用于显式声明属性是私有的。 -
.
用于自动生成属性的访问器。
默认情况下,所有属性都是私有的,但始终使用 !
twigil 是一个好习惯。
因此,我们应该将上面的类重写为
class Human {
has $!name;
has $!age;
has $!sex;
has $!nationality;
}
my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
say $john;
将以下语句附加到脚本:say $john.age;
它将返回此错误:Method 'age' not found for invocant of class 'Human'
,因为 $!age
是私有的,只能在对象内部使用。尝试在对象外部访问它将返回错误。
现在将 has $!age
替换为 has $.age
并观察 say $john.age;
的结果
9.3. 命名参数与位置参数
在 Raku 中,所有类都继承默认的 .new()
构造函数。
它可以通过向其提供参数来创建对象。
默认构造函数只能提供命名参数。
在我们上面的示例中,请注意提供给 .new()
的参数是按名称定义的
-
name => 'John'
-
age => 23
如果我不想每次创建对象时都提供每个属性的名称怎么办?
然后我需要创建另一个接受位置参数的构造函数。
class Human {
has $.name;
has $.age;
has $.sex;
has $.nationality;
# new constructor that overrides the default one.
method new ($name,$age,$sex,$nationality) {
self.bless(:$name,:$age,:$sex,:$nationality);
}
}
my $john = Human.new('John',23,'M','American');
say $john;
9.4. 方法
9.4.1. 简介
方法是对象的子例程。
与子例程一样,它们是打包一组功能的一种方式,它们接受参数,具有签名,并且可以定义为多。
方法使用 method
关键字定义。
通常情况下,需要使用方法对对象的属性执行某种操作。这强化了封装的概念。对象属性只能在对象内部使用方法进行操作。外部世界只能与对象方法交互,而不能直接访问其属性。
class Human {
has $.name;
has $.age;
has $.sex;
has $.nationality;
has $.eligible;
method assess-eligibility {
if self.age < 21 {
$!eligible = 'No'
} else {
$!eligible = 'Yes'
}
}
}
my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
$john.assess-eligibility;
say $john.eligible;
在类中定义方法后,可以使用“点符号”在对象上调用它们。
对象 . 方法 或如上例所示: $john.assess-eligibility
在方法的定义中,如果我们需要引用对象本身来调用另一个方法,我们使用 self
关键字。
在方法的定义中,如果我们需要引用一个属性,即使它是用 .
定义的,我们也使用 !
。
其理由是 .
twigil 所做的是使用 !
声明一个属性,并自动创建一个访问器。
在上面的例子中,if self.age < 21
和 if $!age < 21
具有相同的效果,尽管它们在技术上是不同的。
-
self.age
调用.age
方法(访问器)。
可以写成$.age
。 -
$!age
是对变量的直接调用。
9.4.2. 私有方法
普通方法可以在类外部的对象上调用。
**私有方法**是只能从类内部调用的方法。
一个可能的用例是一个方法调用另一个方法来执行特定操作。与外部世界交互的方法是公共的,而被引用的方法应该是私有的。我们不希望用户直接调用它,因此我们将其声明为私有的。
声明私有方法需要在其名称前使用 !
twigil。
私有方法使用 !
而不是 .
调用。
method !iamprivate {
# code goes in here
}
method iampublic {
self!iamprivate;
# do additional things
}
9.5. 类属性
**类属性**是属于类本身而不是其对象的属性。
它们可以在定义期间初始化。
类属性使用 my
而不是 has
声明。
它们是在类本身上而不是在其对象上调用的。
class Human {
has $.name;
my $.counter = 0;
method new($name) {
Human.counter++;
self.bless(:$name);
}
}
my $a = Human.new('a');
my $b = Human.new('b');
say Human.counter;
9.6. 访问类型
到目前为止,我们看到的所有例子都使用访问器从对象的属性中**获取**信息。
如果我们需要修改属性的值怎么办?
我们需要使用关键字 is rw
将其标记为“读/写”。
class Human {
has $.name;
has $.age is rw;
}
my $john = Human.new(name => 'John', age => 21);
say $john.age;
$john.age = 23;
say $john.age;
默认情况下,所有属性都声明为“只读”,但您可以使用 is readonly
显式地执行此操作。
9.7. 继承
9.7.1. 简介
**继承**是面向对象编程的另一个概念。
在定义类时,我们很快就会意识到一些属性/方法是许多类共有的。
我们应该复制代码吗?
不!我们应该使用**继承**。
假设我们要定义两个类,一个是人类的类,一个是雇员的类。
人类有两个属性:姓名和年龄。
雇员有四个属性:姓名、年龄、公司和工资。
人们可能会尝试将类定义为
class Human {
has $.name;
has $.age;
}
class Employee {
has $.name;
has $.age;
has $.company;
has $.salary;
}
虽然技术上正确,但上面的代码在概念上被认为是糟糕的。
更好的写法是
class Human {
has $.name;
has $.age;
}
class Employee is Human {
has $.company;
has $.salary;
}
is
关键字定义继承。
在面向对象的术语中,我们说 Employee 是 Human 的**子类**,而 Human 是 Employee 的**父类**。
所有子类都继承父类的属性和方法,因此无需重新定义它们。
9.7.2. 覆盖
类继承其父类的所有属性和方法。
在某些情况下,我们需要子类中的方法的行为与其继承的方法不同。
为此,我们在子类中重新定义该方法。
这个概念叫做**覆盖**。
在下面的例子中,方法 introduce-yourself
由 Employee 类继承。
class Human {
has $.name;
has $.age;
method introduce-yourself {
say 'Hi I am a human being, my name is ' ~ self.name;
}
}
class Employee is Human {
has $.company;
has $.salary;
}
my $john = Human.new(name =>'John', age => 23,);
my $jane = Employee.new(name =>'Jane', age => 25, company => 'Acme', salary => 4000);
$john.introduce-yourself;
$jane.introduce-yourself;
覆盖的工作原理如下:
class Human {
has $.name;
has $.age;
method introduce-yourself {
say 'Hi I am a human being, my name is ' ~ self.name;
}
}
class Employee is Human {
has $.company;
has $.salary;
method introduce-yourself {
say 'Hi I am a employee, my name is ' ~ self.name ~ ' and I work at: ' ~ self.company;
}
}
my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);
$john.introduce-yourself;
$jane.introduce-yourself;
根据对象的所属类,将调用正确的方法。
9.7.3. 子方法
**子方法**是一种不被子类继承的方法。
它们只能从声明它们的类中访问。
它们使用 submethod
关键字定义。
9.8. 多重继承
Raku 允许多重继承。一个类可以继承自多个其他类。
class bar-chart {
has Int @.bar-values;
method plot {
say @.bar-values;
}
}
class line-chart {
has Int @.line-values;
method plot {
say @.line-values;
}
}
class combo-chart is bar-chart is line-chart {
}
my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);
my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
输出
Actual sales:
[10 9 11 8 7 10]
Forecast sales:
[9 8 10 7 6 9]
Actual vs Forecast:
[10 9 11 8 7 10]
combo-chart
类应该能够容纳两个序列,一个用于绘制在条形图上的实际值,另一个用于绘制在线条图上的预测值。
这就是为什么我们将它定义为 line-chart
和 bar-chart
的子类。
您应该已经注意到,在 combo-chart
上调用 plot
方法并没有产生预期的结果。只绘制了一个序列。
为什么会发生这种情况?
combo-chart
继承自 line-chart
和 bar-chart
,并且它们都有一个名为 plot
的方法。当我们在 combo-chart
上调用该方法时,Raku 内部机制将尝试通过调用其中一个继承的方法来解决冲突。
为了使其正常运行,我们应该在 combo-chart
中重写 plot
方法。
class bar-chart {
has Int @.bar-values;
method plot {
say @.bar-values;
}
}
class line-chart {
has Int @.line-values;
method plot {
say @.line-values;
}
}
class combo-chart is bar-chart is line-chart {
method plot {
say @.bar-values;
say @.line-values;
}
}
my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);
my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
输出
Actual sales:
[10 9 11 8 7 10]
Forecast sales:
[9 8 10 7 6 9]
Actual vs Forecast:
[10 9 11 8 7 10]
[9 8 10 7 6 9]
9.9. 角色
**角色**类似于类,因为它们是属性和方法的集合。
角色使用关键字 role
声明。希望实现角色的类使用 does
关键字来实现。
role bar-chart {
has Int @.bar-values;
method plot {
say @.bar-values;
}
}
role line-chart {
has Int @.line-values;
method plot {
say @.line-values;
}
}
class combo-chart does bar-chart does line-chart {
method plot {
say @.bar-values;
say @.line-values;
}
}
my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);
my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
运行上面的脚本,您将看到结果相同。
到目前为止,您可能会问自己:如果角色的行为像类,那么它们有什么用?
为了回答您的问题,请修改第一个用于展示多重继承的脚本,即我们*忘记*重写 plot
方法的脚本。
role bar-chart {
has Int @.bar-values;
method plot {
say @.bar-values;
}
}
role line-chart {
has Int @.line-values;
method plot {
say @.line-values;
}
}
class combo-chart does bar-chart does line-chart {
}
my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);
my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
输出
===SORRY!===
Method 'plot' must be resolved by class combo-chart because it exists in multiple roles (line-chart, bar-chart)
如果将多个角色应用于同一个类并且存在冲突,则会引发编译时错误。
这是一种比多重继承更安全的方法,在多重继承中,冲突不被视为错误,而是在运行时简单地解决。
角色会警告您存在冲突。
9.10. 自省
**自省**是获取有关对象的信息的过程,例如其类型、属性或方法。
class Human {
has Str $.name;
has Int $.age;
method introduce-yourself {
say 'Hi I am a human being, my name is ' ~ self.name;
}
}
class Employee is Human {
has Str $.company;
has Int $.salary;
method introduce-yourself {
say 'Hi I am a employee, my name is ' ~ self.name ~ ' and I work at: ' ~ self.company;
}
}
my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);
say $john.WHAT;
say $jane.WHAT;
say $john.^attributes;
say $jane.^attributes;
say $john.^methods;
say $jane.^methods;
say $jane.^parents;
if $jane ~~ Human {say 'Jane is a Human'};
自省由以下内容提供便利:
-
.WHAT
- 返回创建对象的类 -
.^attributes
- 返回对象的所有属性 -
.^methods
- 返回可以在对象上调用的所有方法 -
.^parents
- 返回对象的父类 -
~~
被称为智能匹配运算符。如果对象是从与其进行比较的类或其任何继承类创建的,则其计算结果为 *True*。
有关 Raku 中面向对象编程的更多信息,请参阅 |
10. 异常处理
10.1. 捕获异常
**异常**是在运行时出现问题时发生的一种特殊行为。
我们说异常被*抛出*。
请考虑以下正确运行的脚本
my Str $name;
$name = "Joanna";
say "Hello " ~ $name;
say "How are you doing today?"
输出
Hello Joanna
How are you doing today?
现在考虑这个抛出异常的脚本
my Str $name;
$name = 123;
say "Hello " ~ $name;
say "How are you doing today?"
输出
Type check failed in assignment to $name; expected Str but got Int
in block <unit> at exceptions.raku:2
请注意,每当发生错误时(在本例中,为字符串变量分配一个数字),程序都会停止,并且不会评估其他代码行。
**异常处理**是*捕获*已*抛出*的异常的过程,以便脚本继续工作。
my Str $name;
try {
$name = 123;
say "Hello " ~ $name;
CATCH {
default {
say "Can you tell us your name again, we couldn't find it in the register.";
}
}
}
say "How are you doing today?";
输出
Can you tell us your name again, we couldn't find it in the register.
How are you doing today?
异常处理是通过使用 try-catch
块完成的。
try {
# code goes in here
# if anything goes wrong, the script will enter the below CATCH block
# if nothing goes wrong, the CATCH block will be ignored
CATCH {
default {
# the code in here will be evaluated only if an exception has been thrown
}
}
}
CATCH
块的定义方式与 given
块的定义方式相同。这意味着我们可以*捕获*并以不同的方式处理多种类型的异常。
try {
# code goes in here
# if anything goes wrong, the script will enter the below CATCH block
# if nothing goes wrong, the CATCH block will be ignored
CATCH {
when X::AdHoc { # do something if exception of type X::AdHoc is thrown }
when X::IO { # do something if exception of type X::IO is thrown }
when X::OS { # do something if exception of type X::OS is thrown }
default { # do something if exception is thrown and doesn't belong to the above types }
}
}
10.2. 抛出异常
Raku 还允许您显式抛出异常。
可以抛出两种类型的异常
-
临时异常
-
类型化异常
my Int $age = 21;
die "Error !";
my Int $age = 21;
X::AdHoc.new(payload => 'Error !').throw;
临时异常是使用 die
子例程抛出的,后跟异常消息。
类型化异常是对象,因此在上面的示例中使用了 .new()
构造函数。
所有类型化异常都派生自类 X
,以下是一些示例
X::AdHoc
是最简单的异常类型
X::IO
与 IO 错误相关
X::OS
与操作系统错误相关
X::Str::Numeric
与尝试将字符串强制转换为数字相关
有关异常类型及其关联方法的完整列表,请访问 https://raku-docs.perl5.cn/type-exceptions.html |
11. 正则表达式
正则表达式(或简称 *regex*)是用于模式匹配的字符序列。
将其视为一种模式。
if 'enlightenment' ~~ m/ light / {
say "enlightenment contains the word light";
}
在本例中,智能匹配运算符 ~~
用于检查字符串(enlightenment)是否包含单词(light)。
将“Enlightenment”与正则表达式 m/ light /
进行匹配
11.1. 正则表达式定义
可以像这样定义正则表达式
-
/light/
-
m/light/
-
rx/light/
除非明确指定,否则将忽略空格;m/light/
和 m/ light /
相同。
11.2. 匹配字符
字母数字字符和下划线 _
按原样写入。
所有其他字符必须使用反斜杠转义或用引号括起来。
if 'Temperature: 13' ~~ m/ \: / {
say "The string provided contains a colon :";
}
if 'Age = 13' ~~ m/ '=' / {
say "The string provided contains an equal character = ";
}
if '[email protected]' ~~ m/ "@" / {
say "This is a valid email address because it contains an @ character";
}
11.3. 匹配字符类别
字符可以分为几类,我们可以根据它们进行匹配。
我们也可以匹配该类别的反面(除它之外的所有内容)。
类别 |
正则表达式 |
反面 |
正则表达式 |
单词字符(字母、数字或下划线) |
\w |
除单词字符以外的任何字符 |
\W |
数字 |
\d |
除数字以外的任何字符 |
\D |
空格 |
\s |
除空格以外的任何字符 |
\S |
水平空格 |
\h |
除水平空格以外的任何字符 |
\H |
垂直空格 |
\v |
除垂直空格以外的任何字符 |
\V |
制表符 |
\t |
除制表符以外的任何字符 |
\T |
换行符 |
\n |
除换行符以外的任何字符 |
\N |
if "John123" ~~ / \d / {
say "This is not a valid name, numbers are not allowed";
} else {
say "This is a valid name"
}
if "John-Doe" ~~ / \s / {
say "This string contains whitespace";
} else {
say "This string doesn't contain whitespace"
}
11.4. Unicode 属性
如前一节所示,根据字符类别进行匹配很方便。
也就是说,更系统的方法是使用 Unicode 属性。
这允许您匹配 ASCII 标准内部和外部的字符类别。
ASCII 标准。
Unicode 属性用 <: >
括起来
if "Devanagari Numbers १२३" ~~ / <:N> / {
say "Contains a number";
} else {
say "Doesn't contain a number"
}
if "Привет, Иван." ~~ / <:Lu> / {
say "Contains an uppercase letter";
} else {
say "Doesn't contain an upper case letter"
}
if "John-Doe" ~~ / <:Pd> / {
say "Contains a dash";
} else {
say "Doesn't contain a dash"
}
11.5. 通配符
通配符也可以在正则表达式中使用。
点 .
表示任何单个字符。
if 'abc' ~~ m/ a.c / {
say "Match";
}
if 'a2c' ~~ m/ a.c / {
say "Match";
}
if 'ac' ~~ m/ a.c / {
say "Match";
} else {
say "No Match";
}
11.6. 量词
量词位于字符之后,用于指定我们期望它出现多少次。
问号 ?
表示零次或一次。
if 'ac' ~~ m/ a?c / {
say "Match";
} else {
say "No Match";
}
if 'c' ~~ m/ a?c / {
say "Match";
} else {
say "No Match";
}
星号 *
表示零次或多次。
if 'az' ~~ m/ a*z / {
say "Match";
} else {
say "No Match";
}
if 'aaz' ~~ m/ a*z / {
say "Match";
} else {
say "No Match";
}
if 'aaaaaaaaaaz' ~~ m/ a*z / {
say "Match";
} else {
say "No Match";
}
if 'z' ~~ m/ a*z / {
say "Match";
} else {
say "No Match";
}
+
表示至少一次。
if 'az' ~~ m/ a+z / {
say "Match";
} else {
say "No Match";
}
if 'aaz' ~~ m/ a+z / {
say "Match";
} else {
say "No Match";
}
if 'aaaaaaaaaaz' ~~ m/ a+z / {
say "Match";
} else {
say "No Match";
}
if 'z' ~~ m/ a+z / {
say "Match";
} else {
say "No Match";
}
11.7. 匹配结果
每当将字符串与正则表达式匹配的过程成功时,匹配结果都会存储在特殊变量 $/
中。
if 'Rakudo is a Perl 6 compiler' ~~ m/:s Perl 6/ {
say "The match is: " ~ $/;
say "The string before the match is: " ~ $/.prematch;
say "The string after the match is: " ~ $/.postmatch;
say "The matching string starts at position: " ~ $/.from;
say "The matching string ends at position: " ~ $/.to;
}
The match is: Perl 6
The string before the match is: Rakudo is a
The string after the match is: compiler
The matching string starts at position: 12
The matching string ends at position: 18
$/
返回一个*匹配对象*(与正则表达式匹配的字符串)。
可以在*匹配对象*上调用以下方法
.prematch
返回匹配之前的字符串。
.postmatch
返回匹配之后的字符串。
.from
返回匹配的起始位置。
.to
返回匹配的结束位置。
默认情况下,正则表达式定义中的空格将被忽略。 如果我们想匹配包含空格的正则表达式,我们必须明确地这样做。 正则表达式 m/:s Perl 6/ 中的 :s 强制考虑空格。或者,我们可以将正则表达式写成 m/ Perl\s6 / 并使用 \s 来表示空格。如果正则表达式包含多个空格,则使用 :s 比为每个空格使用 \s 更好。
|
11.8. 示例
让我们检查一个电子邮件地址是否有效。
在本例中,我们假设一个有效的电子邮件地址具有以下格式
名字 [点] 姓氏 [at] 公司 [点] (com/org/net)
本例中用于电子邮件验证的正则表达式不是很准确。 其唯一目的是演示 Raku 中的正则表达式功能。 不要在生产环境中按原样使用它。 |
my $email = '[email protected]';
my $regex = / <:L>+\.<:L>+\@<:L+:N>+\.<:L>+ /;
if $email ~~ $regex {
say $/ ~ " is a valid email";
} else {
say "This is not a valid email";
}
[email protected] 是一个有效的电子邮件地址
<:L>
匹配单个字母
<:L>` 匹配一个或多个字母 + `\.` 匹配单个 [点] 字符 + `\@` 匹配单个 [at] 字符 + `<:L:N>
匹配一个字母或一个数字
<:L+:N>+
匹配一个或多个字母或数字
正则表达式可以分解如下
-
名字
<:L>+
-
[点]
\.
-
姓氏
<:L>+
-
[at]
\@
-
公司名称
<:L+:N>+
-
[点]
\.
-
com/org/net
<:L>+
my $email = '[email protected]';
my regex many-letters { <:L>+ };
my regex dot { \. };
my regex at { \@ };
my regex many-letters-numbers { <:L+:N>+ };
if $email ~~ / <many-letters> <dot> <many-letters> <at> <many-letters-numbers> <dot> <many-letters> / {
say $/ ~ " is a valid email";
} else {
say "This is not a valid email";
}
命名的正则表达式使用以下语法定义:my regex 正则表达式名称 { 正则表达式定义 }
可以使用以下语法调用命名的正则表达式:<正则表达式名称>
有关正则表达式的更多信息,请参阅 https://raku-docs.perl5.cn/language/regexes |
12. Raku 模块
Raku 是一种通用编程语言。它可以用来处理多种任务,包括:文本操作、图形、Web、数据库、网络协议等。
可重用性是一个非常重要的概念,程序员不必每次想做新任务时都重新发明轮子。
Raku 允许创建和重新分发**模块**。每个模块都是一组打包的功能,安装后即可重用。
**Zef** 是 Rakudo Star 附带的模块管理工具。
要安装特定模块,请在终端中键入以下命令
zef install "模块名称"
可以在以下位置找到 Raku 模块目录:https://raku.land/ |
12.1. 使用模块
MD5 是一种加密哈希函数,可生成 128 位的哈希值。
MD5 有多种应用,包括对数据库中存储的密码进行加密。当新用户注册时,他们的凭据不会以纯文本形式存储,而是经过“哈希”处理。其背后的原理是,如果数据库遭到入侵,攻击者将无法知道密码是什么。
幸运的是,您无需自己实现 MD5 算法;已经有现成的 Raku 模块可供使用。
让我们来安装它
zef install Digest::MD5
现在,运行以下脚本
use Digest::MD5;
my $password = "password123";
my $hashed-password = md5( $password );
say $hashed-password;
为了运行创建哈希的 md5()
函数,我们需要加载所需的模块。
use
关键字加载要在脚本中使用的模块,该模块提供了一个 md5
子例程。
在实践中,仅使用 MD5 哈希是不够的,因为它容易受到字典攻击。 它应该与盐值结合使用 https://en.wikipedia.org/wiki/Salt_(cryptography)。 |
13. Unicode
Unicode 是一种字符编码标准,用于表示世界上大多数书写系统的文本。
UTF-8 是一种字符编码,能够对 Unicode 中所有可能的字符(或代码点)进行编码。
字符由以下内容定义
字形:视觉表示。
代码点:分配给字符的数字。
代码点名称:分配给字符的名称。
13.1. 使用 Unicode
say "a";
say "\x0061";
say "\c[LATIN SMALL LETTER A]";
以上三行展示了构建字符的不同方法
-
直接写入字符(字形)
-
使用
\x
和代码点 -
使用
\c
和代码点名称
say "☺";
say "\x263a";
say "\c[WHITE SMILING FACE]";
say "á";
say "\x00e1";
say "\x0061\x0301";
say "\c[LATIN SMALL LETTER A WITH ACUTE]";
字母 á
可以写成
-
使用其唯一的代码点
\x00e1
-
或作为代码点
a
和尖音符\x0061\x0301
的组合
say "á".NFC;
say "á".NFD;
say "á".uniname;
输出
NFC:0x<00e1>
NFD:0x<0061 0301>
LATIN SMALL LETTER A WITH ACUTE
NFC
返回唯一的代码点。
NFD
分解字符并返回每个部分的代码点。
uniname
返回代码点名称。
my $Δ = 1;
$Δ++;
say $Δ;
my $var = 2 + ⅒;
say $var;
13.2. Unicode 感知操作
13.2.1. 数字
阿拉伯数字是十个数字:0、1、2、3、4、5、6、7、8、9。这套数字是全世界使用最广泛的。
尽管如此,在世界不同地区,不同类型的数字也有一定程度的使用。
使用阿拉伯数字以外的数字集时,无需特别注意。所有方法/运算符都按预期工作。
say (٤,٥,٦,1,2,3).sort; # (1 2 3 4 5 6)
say 1 + ٩; # 10
13.2.2. 字符串
如果我们使用通用的字符串操作,我们可能不会总是得到我们想要的结果,尤其是在比较或排序时。
比较
say 'a' cmp 'B'; # More
上面的例子表明 a
比 B
大。原因是小写字母 a
的代码点大于大写字母 B
的代码点。
虽然从技术上讲这是正确的,但这可能不是我们想要的。
幸运的是,Raku 有一些方法/运算符可以实现 Unicode 排序算法。
其中之一是 unicmp
,它的行为类似于上面展示的 cmp
,但它支持 Unicode。
say 'a' unicmp 'B'; # Less
如您所见,使用 unicmp
运算符现在可以得到预期的结果,即 a
小于 B
。
排序
作为使用代码点进行排序的 sort
方法的替代方法,Raku 提供了一个 collate
方法,该方法实现了 Unicode 排序算法。
say ('a','b','c','D','E','F').sort; # (D E F a b c)
say ('a','b','c','D','E','F').collate; # (a b c D E F)
14. 并行、并发和异步
14.1. 并行
在正常情况下,程序中的所有任务都是按顺序运行的。
这可能不是问题,除非您尝试做的事情需要很长时间。
幸运的是,Raku 具有使您能够并行运行的功能。
在这个阶段,重要的是要注意并行性可以指两种情况之一
-
任务并行性:两个(或多个)独立表达式并行运行。
-
数据并行性:单个表达式并行迭代元素列表。
让我们从后者开始。
14.1.1. 数据并行性
my @array = 0..50000; # Array population
my @result = @array.map({ is-prime $_ }); # call is-prime for each array element
say now - INIT now; # Output the time it took for the script to complete
我们只执行了一个操作 @array.map({ is-prime $_ })
is-prime
子例程正在被顺序调用,用于处理数组中的每个元素
即,先执行 is-prime @array[0]
,然后是 is-prime @array[1]
,接着是 is-prime @array[2]
,以此类推。
is-prime
my @array = 0..50000; # Array population
my @result = @array.race.map({ is-prime $_ }); # call is-prime for each array element
say now - INIT now; # Output the time it took to complete
请注意表达式中 race
的使用。 此方法将启用数组元素的并行迭代。
运行这两个示例(使用和不使用 race
)后,比较两个脚本完成所需的时间。
race
hyper
如果你运行这两个示例,你应该会注意到一个已排序,而另一个未排序。 |
14.1.2. 任务并行
my @array1 = 0..49999;
my @array2 = 2..50001;
my @result1 = @array1.map( {is-prime($_ + 1)} );
my @result2 = @array2.map( {is-prime($_ - 1)} );
say @result1 eqv @result2;
say now - INIT now;
-
我们定义了 2 个数组
-
对每个数组应用不同的操作并存储结果
-
并检查两个结果是否相同
脚本等待 @array1.map( {is-prime($_ + 1)} )
完成
然后评估 @array2.map( {is-prime($_ - 1)} )
应用于每个数组的两个操作彼此不依赖。
my @array1 = 0..49999;
my @array2 = 2..50001;
my $promise1 = start @array1.map( {is-prime($_ + 1)} ).eager;
my $promise2 = start @array2.map( {is-prime($_ - 1)} ).eager;
my @result1 = await $promise1;
my @result2 = await $promise2;
say @result1 eqv @result2;
say now - INIT now;
start
子例程评估代码并返回一个 promise 类型的对象,或简称为promise。
如果代码评估正确,则promise 将被保留。
如果代码抛出异常,则promise 将被破坏。
await
子例程等待promise。
如果它被保留,它将获得返回值。
如果它被破坏,它将获得抛出的异常。
检查每个脚本完成所需的时间。
并行性始终会增加线程开销。 如果计算速度的提高没有抵消这种开销,则脚本看起来会更慢。 这就是为什么,对相当简单的脚本使用 race 、hyper 、start 和 await 实际上会降低它们的速度。
|
14.2. 并发和异步
有关并发和异步编程的更多信息,请参阅 https://raku-docs.perl5.cn/language/concurrency |
15. 本地调用接口
Raku 使我们能够使用 C 库,使用本机调用接口。
NativeCall
是 Raku 附带的标准模块,它提供了一组功能来简化 Raku 和 C 交互的工作。
15.1. 调用函数
考虑以下定义了一个名为 hellofromc
的函数的 C 代码。 此函数在终端上打印 Hello from C
。 它不接受任何参数也不返回任何值。
#include <stdio.h>
void hellofromc () {
printf("Hello from C\n");
}
根据你的操作系统,运行以下命令将上述 C 代码编译成库。
gcc -c -fpic ncitest.c
gcc -shared -o libncitest.so ncitest.o
gcc -c ncitest.c
gcc -shared -o ncitest.dll ncitest.o
gcc -dynamiclib -o libncitest.dylib ncitest.c
在编译 C 库的同一目录中,创建一个包含以下代码的新 Raku 文件并运行它。
use NativeCall;
constant LIBPATH = "$*CWD/ncitest";
sub hellofromc() is native(LIBPATH) { * }
hellofromc();
首先,我们声明我们将使用 NativeCall
模块。
然后我们创建了一个常量 LIBPATH
,它保存 C 库的路径。
请注意,$*CWD
返回当前工作目录。
然后我们创建了一个名为 hellofromc()
的新 Raku 子例程,它应该充当其对应 C 函数的包装器,该函数具有相同的名称并驻留在 LIBPATH
中的 C 库中。
所有这些都是通过使用 is native
特性完成的。
最后我们调用了我们的 Raku 子例程。
本质上,这一切都归结为使用特性 is native
和 C 库的名称声明一个子例程。
15.2. 重命名函数
在上一部分中,我们看到了如何通过使用 is native
特性将其包装在具有相同名称的 Raku 子例程中来调用一个非常简单的 C 函数。
在某些情况下,我们想更改 Raku 子例程的名称。
为此,我们使用 is symbol
特性。
让我们修改上面的 Raku 脚本并将 Raku 子例程重命名为 hello
而不是 hellofromc
use NativeCall;
constant LIBPATH = "$*CWD/ncitest";
sub hello() is native(LIBPATH) is symbol('hellofromc') { * }
hello();
如果 Raku 子例程的名称与其 C 对应名称不同,我们应该使用带有原始 C 函数名称的 is symbol
特性。
15.3. 传递参数
编译以下修改后的 C 库并再次运行以下 Raku 脚本。
请注意我们如何修改 C 和 Raku 代码以接受字符串(C 中的 char*
和 Raku 中的 Str
)
#include <stdio.h>
void hellofromc (char* name) {
printf("Hello, %s! This is C!\n", name);
}
use NativeCall;
constant LIBPATH = "$*CWD/ncitest";
sub hello(Str) is native(LIBPATH) is symbol('hellofromc') { * }
hello('Jane');
15.4. 返回值
让我们再重复一次该过程,并创建一个简单的计算器,该计算器接受 2 个整数并将它们相加。
编译 C 库并运行 Raku 脚本。
int add (int a, int b) {
return (a + b);
}
use NativeCall;
constant LIBPATH = "$*CWD/ncitest";
sub add(int32,int32 --> int32) is native(LIBPATH) { * }
say add(2,3);
请注意 C 和 Raku 函数如何接受两个整数并返回一个(C 中的 int
和 Raku 中的 int32
)
15.5. 类型
你可能问过自己,为什么我们在最新的 Raku 脚本中使用 int32
而不是 Int
。
某些 Raku 类型(如 Int
、Rat
等)不能按原样用于传递和接收来自 C 函数的值。
必须在 Raku 中使用与 C 中相同的类型。
幸运的是,Raku 提供了许多映射到其各自 C 对应类型的类型。
C 类型 | Raku 类型 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
数组:例如 |
|
有关原生调用接口的更多信息,请参阅 https://raku-docs.perl5.cn/language/nativecall |
16. 社区
-
#raku IRC 频道。很多讨论都在 IRC 上进行。
如果您有任何需要立即得到解答的问题,这里应该是您的首选之地:https://raku.perl5.cn/community/irc -
StackOverflow Raku 问题 是一个可以更深入地回答有关 Raku 问题的地方。
-
Rakudo 周报 每周概述 Rakudo 及其周围的变化。
-
Raku 星球 博客聚合器。通过阅读关注 Raku 的博客文章来保持关注。
-
/r/rakulang 订阅 Raku subreddit。
-
@raku_news 在 X 上关注社区。
-
@rakulang 在 Mastodon 上关注社区。