Raku 编程/单页面
Raku 是 Perl 编程语言的继任者,代表了该语言的一次重大向后不兼容的重写。它是一种多范式编程语言,用途广泛且功能强大。本书将向读者介绍 Raku 语言及其众多功能。
Raku 编程语言是 Perl 的第六个主要版本。
它旨在解决 Perl 在其悠久历史中积累的缺陷。这些缺陷主要是由于 Perl 连续版本对向后兼容性的要求造成的。这就是为什么 Raku 是第一个不向后兼容的 Perl 版本的原因。
Raku 并没有取代 Perl。与其说它是一种姊妹语言,不如说它是 Perl 的研发分支。在某种程度上,Raku 之于 Perl,就像 C++ 之于 C。虽然 C++ 是一种非常成功的编程语言,但它并没有取代 C。
Perl 编程语言由 Larry Wall 于 1987 年创建,他是一名语言学家,也是 Unisys 的计算机系统管理员。Perl 是一种动态编程语言,在其大部分历史中被认为是“脚本语言”或命令行管理工具。但是,从版本 5 开始,Perl 成为了一种功能强大且实用的通用编程语言,在 Web 开发人员、系统管理员和业余程序员中一直很受欢迎。
Perl 编程语言作为开源免费软件项目开发,并逐渐扩展到版本 5 发布,这是 Perl 的最新技术水平。在所有这些发展过程中,Perl 保持与以前版本的向后兼容性。Perl 5 解释器可以读取、理解和执行(大部分)用 Perl 1、Perl 2、Perl 3 和 Perl 4 编写的程序。不幸的是,这使得 Perl 5 解释器内部变得杂乱无章,使许多编程任务变得比实际需要的更难。
另一个绊脚石是 Perl 5 语言规范;它根本不是规范。Perl 解释器本身就是标准:解释器的行为就是 Perl 的“标准”行为。唯一能够复制 Perl 语言所有奇怪和特殊行为的方法是仅使用该标准软件。
到 2000 年,很明显 Perl 需要注入生命
"[P5P/Perl 大会] 会议最初是 Chip Salzenberg、Jarkko Hietaniemi、Elaine Ashton、Tim Bunce、Sarathy、Nick Ing-Simmons、Larry Wall、Nat Torkington、brian d foy 和 Adam Turoff 的聚会,他们聚在一起起草了一份类似宪法的文件,因为社区似乎正在分裂。Jon 迟到了会议,发现我们正在讨论社区,他开始扔东西来表达他对 perl 本身停滞不前,甚至可能消亡的不满,并认为我们应该讨论如何复兴 Perl。据我后来了解,杯子事件是事先策划好的。所以,它已经是一个既成事实,但狂怒是它的曝光。" [1]
Perl 6 的时机已经成熟:从头开始重写 Perl 语言。为了解决语言的根本问题,并添加必要的新的功能,放弃了与 Perl 5 的兼容性。因此,它与 Perl 5 完全不同,但同时又明显属于同一个“语言家族”。与随着时间的推移而有机发展的 Perl 5 不同,Perl 6 从一组规范开始,并在多个独立且平等的实现中实例化。
Perl 6 开始了一段漫长的社区参与和 RFC 流程。社区成员被要求为新语言贡献想法和建议。Larry 审核了这些建议,保留了好的建议,删除了不好的建议,并试图以统一的方式将所有建议整合在一起。Perl 5 曾因“hacky”和不一致而受到批评,因此 Perl 6 应该从一开始就避免这种情况。一旦所有建议都被统计和讨论,Larry 发布了一系列设计文档,称为Apocalypse。每个 Apocalypse 的编号大致对应于“Programming Perl”一书中的一章,旨在揭示在 Perl 6 设计中考虑的概念和权衡。从这些文档(内容缺乏具体细节)中,Damian Conway 制作了一系列相应的解释性文档,称为Exegeses。Apocalypse 揭示了一些设计,而 Exegeses 则用日常程序员编写的代码来解释这些设计对日常程序员意味着什么。后来,随着设计日趋成熟,创建了称为Synopses 的设计规范,以综合和记录 Perl 6 的设计。Synopses 目前是 Perl 6 语言的官方设计文档。
在 2019 年 10 月,Perl 6 社区投票决定将名称更改为 Raku。
Perl 一直以来都是一种灵活且功能强大的编程语言。Perl 团队最重要的口号之一是多种方法,任君选择(TIMTOWTDI,发音为“Tim Toady”)。Raku 是一种非常灵活的语言,它结合了多种不同的编程范式,以支持各种程序员和不同的编程任务。由于这种 TIMTOWTDI 哲学,Raku 是一种非常庞大的编程语言,拥有许多不同的功能和能力。
换句话说,Perl 为你提供了充足的绳索,但你必须小心不要被它绊倒。Raku 中存在着许多伟大的想法,但并非所有想法对所有编程任务都有用。此外,Raku 中存在许多可能在编程界不被视为“最佳实践”的事情。重要的是要学习如何在 Raku 中编写某些内容,以及何时以某些方式编写内容。如果没有这些知识,程序很容易沦为毫无意义的、不可读的乱码。
在本书中,我们将尝试向您展示一些最佳实践,并尝试讨论每个功能在哪些地方有用,以及在哪些地方没有用。
- Pugs
- 是第一个或多或少可用的 Raku 实现。它是由 Audrey Tang 用 Haskell 编写的。现在它主要与历史兴趣相关。
- Niecza
- 使用 .net framework 实现的 Raku。
- Rakudo
- Raku 的领先的高级实现。它是自托管的,这意味着它主要用 Raku 和 Raku 的子语言:nqp 编写。它针对多个进程虚拟机:Parrot、JVM、MoarVM,以及未来可能的其他虚拟机(JavaScript、Lua 等)。
截至 2014 年 4 月,MoarVM 上的 Rakudo 是最有希望的实现。它完全是免费的开源软件,并使用专门为 Raku 设计的虚拟机。
经过长时间的语言设计,是时候开始创建新语言的实现。为了避免 Perl 的问题,最初的组织者决定在后端执行引擎和前端语言解析器之间创建更好的分离。经过多次讨论,Parrot 虚拟机 项目启动,旨在为 Raku 等动态语言创建虚拟机。Parrot 迅速发展,成为独立于 Raku 的项目,转而成为所有动态语言的虚拟机。由于 Raku 规模庞大,野心勃勃,任何能够支持它的虚拟机也能很好地支持许多其他动态语言。
Perl 黑客 Audrey Tang 使用 Haskell 编程语言构建了 Raku 的参考实现。此实现被称为 Pugs,并用作语言设计师正在开发的许多想法的测试平台。来自 Pugs 团队的反馈帮助塑造了语言设计,而语言设计中的更改又导致了 Pugs 的修改。这是一种有用且有益的关系,尤其是在当时没有其他实现处于如此高的开发状态的情况下。
Raku 的“官方”语法将用 Raku 本身编写。这是因为 Raku 被设计为在当时拥有任何现有语言中最先进的语法引擎之一。对于如此先进的语言,没有比该语言本身更好的语法实现选择。STD.pm 被创建为标准的 Raku 语法,并且在各种实现之间发生冲突时仍然被参考。
STD_red 是使用 Ruby 编程语言实现的 Raku 语法。STD_blue 是 STD.pm 的一个更新的编译器,用 Perl 编写。
ELF 是 Raku 的一个自举实现,它使用 STD_blue 将 Raku 代码编译成 Perl 代码以执行。
然而,当 Audrey Tang 离开 Pugs 项目时,该项目的开发降到了最低。它仍然对测试和参考有用,但 Pugs 不再是曾经的活跃开发平台。但是,Parrot 自那以后取得了飞跃式的进步,并且最终准备开始支持高级语言的编译器。一个名为“Rakudo”的 Raku 项目启动,并开始迅速发展。Rakudo 项目的一部分是 Patrick Michaud 创建的高级解析器工具,称为 PCT(“Parrot Compiler Tools”)。PCT 是一种类似于低级 Flex 和 Bison 工具的解析器生成工具。但是,PCT 使用 Raku 语言的一个子集来编写解析器,而不是使用 C 或 C++。这意味着 Rakudo 正在成为自托管:Rakudo 编译器本身部分用 Raku 编写。
有关 Rakudo 的更多信息,请访问 http://www.rakudo.org 网站。
Raku 属于一类称为 动态语言 的编程语言。动态语言使用其数据类型可以在运行时更改的变量,并且不需要预先声明。动态语言的替代方案是静态语言,例如 C 或 Java,在这些语言中,变量通常必须在使用之前声明为特定的数据类型。
在像 C 这样的语言或其派生语言(例如 C++、C# 或 Java)中,变量需要在使用之前预先声明为一个类型
unsigned short int x;
x = 10;
上面的代码并不完全准确,因为在 C 中,您可以在声明变量时对其进行初始化
unsigned short int x = 10;
但是,变量 x
必须在声明之前才能使用。一旦将其声明为 unsigned short int
类型,您就不能使用 x
来存储其他类型的数据,例如浮点数或数据指针,至少在没有显式强制转换的情况下不能这样做
unsigned short int x;
x = 1.02; /* Wrong! */
unsigned short int y;
x = &y; /* Wrong! */
在像 Raku 这样的动态编程语言中,变量可以在首次使用时自动分配,而无需显式声明。此外,Raku 中的变量是多态的:它们可以是整数、字符串、浮点数或复杂的数据结构,例如数组或哈希,而无需任何强制转换。以下是一些示例
my $x;
$x = 5; # Integer
$x = "hello"; # String
$x = 3.1415; # Floating Point Number
上面的示例演示了我们在本书的其余部分将要讨论的许多不同的想法。一个重要的想法是 注释。注释是源代码中的注释,旨在供程序员阅读,并且被 Raku 解释器忽略。在 Raku 中,大多数注释用 #
符号标记,并一直持续到行尾。Raku 还具有嵌入式注释和多行文档,我们将在稍后讨论。
我们并没有完全诚实地说明 Raku 数据。Raku 确实允许数据被赋予显式类型,如果您想要的话。默认情况下使用的是像上面我们看到的这样的多态数据,但您也可以声明一个标量可能只保存一个整数,或一个字符串,或一个数字,或一个完全不同的数据项。我们将在稍后详细讨论 Raku 的显式类型系统。现在,更容易认为 Raku 中的数据没有显式类型(或者更确切地说,它不需要它们)。
我们上面看到的示例还显示了一个重要的关键字:my
。my
用于声明一个新的变量以供使用。我们将在稍后详细讨论 my
及其用途。
正如我们在上面的简短示例中看到的,Raku 变量在其前面有符号,称为 符号。符号有很多重要的用途,但其中一个最重要的用途是建立 上下文。Raku 有四种类型的符号,用于建立不同类型的数据。我们上面看到的 $ 符号用于 标量:单个数据值,例如数字、字符串或对象引用。其他要使用的符号是 @,它表示数据的数组, % 表示数据的哈希,以及 & 表示子例程或可执行代码块。
- 标量
- 正如我们已经看到的那样,标量包含单个数据项,例如数字或字符串。
- 数组
- 数组是有序数据的列表,它们使用数字索引。
- 哈希
- 哈希是潜在不同类型数据对象的集合,使用字符串索引。
- 代码引用
- 代码引用是指向可执行代码结构的指针,这些结构可以像数据一样传递,并在代码的不同位置调用。
我们在上面已经看到了一些标量的用法,这里我们将展示一个稍微更全面的列表。
my $x;
$x = 42; # Decimal Integer
$x = 0xF6; # Hexadecimal Integer
$x = 0b1010010001; # Binary Integer
$x = 3.1415; # Floating Point Number
$x = 2.34E-5; # Scientific Notation
$x = "Hello "; # Double-Quoted String
$x = 'World!'; # Single-Quoted String
$x = q:to/EOS/; # Heredoc string
This is a heredoc string. It starts at the "q:to"
term and continues until we reach the terminator
specified in quotes above. This is useful for
large multi-line string literals. We will talk about
heredocs in more detail later
EOS
$x = MyObject.new(); # Object Reference
标量是 Raku 中最基本和最基本的数据类型,并且可能是在您的程序中最常使用的类型。这是因为它们非常通用。
正如我们在上面提到的,数组是有序数据对象的列表,这些对象被认为是同一类型。由于数组是标量的列表,因此数组中的某些元素可能是数字,而某些元素可能是字符串,还有一些元素可能是完全不同的数据。但是,这通常不被认为是数组的最佳用途。
数组以 @ 符号为前缀,可以使用 [ 方括号 ] 中的整数索引。以下是一些使用数组的示例
my @a;
@a = 1, 2, 3;
@a = "first", "second", "third";
@a = 1.2, 3.14, 2.717;
一旦我们有了数组,就可以使用索引符号从其中提取标量数据项
my @a, $x;
@a = "first", "second", "third";
$x = @a[0]; # first
$x = @a[1]; # second
$x = @a[2]; # third
数组也可以是多维的
my @a;
@a[0, 0] = 1;
@a[0, 1] = 2;
@a[1, 0] = 3;
@a[1, 1] = 4;
# @a is now:
# |1, 2|
# |3, 4|
数组也不必只存储标量,它们也可以存储任何其他数据项
my @a, @b, @c, @d, %e, %f, %g, &h, &i, &j;
@a = @b, @c, @d;
@a = %e, %f, %g;
@a = &h, &i, &j;
这可以成为一些复杂数据结构的基础,我们将在稍后详细讨论如何组合这些结构。
哈希在很多方面类似于数组:它们可以包含一组对象。但是,与数组不同,哈希使用名称来标识其项目,而不是使用数字。以下是一些示例
my %a = "first" => 1, "second" => 2, "third" => 3;
my $x = %a{"first"}; # 1
my $y = %a{"second"}; # 2
my $z = %a{"third"}; # 3
特殊的 =>
符号类似于逗号,但它创建的是一个 对。对是字符串名称和关联数据对象的组合。哈希有时可以被认为是成对的数组。还要注意,哈希使用花括号来索引其数据,而不是像数组那样使用方括号。
哈希还可以使用一种称为 自动引用 的特殊语法,以帮助简化哈希值的查找。您可以使用不带引号的尖括号 < >
来代替使用花括号和引号 {" "}
来完成相同的工作
my %a = "foo" => "first", "bar" => "second";
my $x = %a{"foo"}; # "first"
my $y = %a<bar> # "second"
哈希中的键值对可以使用另一种方式定义,而无需使用 =>
运算符。键值对也可以使用 **副词语法** 来定义。副词语法在 Raku 中被广泛用于提供命名数据值,因此它不仅仅适用于哈希。"name" => data
形式的键值对可以用副词语法写成 :name(data)
。
my %foo = "first" => 1, "second" => 2, "third" => 3;
my %bar = :first(1), :second(2), :third(3); # Same!
我们将在 Raku 中看到副词的许多用法,因此现在学习它们很重要。
Raku 使用特殊变量 $_
作为默认变量。当没有提供其他变量时,$_
会接收值,并且如果方法以点开头,它会被方法使用。$_
可以显式地通过名称使用,也可以隐式地使用。
$_ = "Hello ";
.print; # Call method 'print' on $_
$_.print; # Same
print $_; # Same, but written as a sub;
given "world!" { # Different way of saying $_ = "world"
.print; # Same as print $_;
}
默认变量在许多地方都很有用,例如循环,它们可以用来清理代码并使操作更明确。我们将在后面的章节中更详细地讨论默认变量。
我们已经讨论了各种基本数据类型:标量、数组和哈希。每个变量的符号将它置于特定的 **上下文** 中。不同类型的变量在不同上下文中使用时表现不同。至少有两种基本类型的上下文,我们现在将讨论其中的两种:标量上下文和列表上下文。标量是所有带 $ 符号的变量,而列表是像数组和哈希这样的东西。
我们将借此机会讨论 Raku 的内置函数之一:say
。say
在程序运行时将一行文本打印到控制台。这是我们将在后面更详细讨论的更大的输入/输出(I/O)系统的一部分。say
接收一个字符串或字符串列表,并将它们打印到控制台。
say "hello world!";
say "This is ", "Raku speaking!";
范围是具有某种数值关系的值列表。范围使用 ..
运算符创建。
my @digits = 0..9; # (0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
范围也可以使用变量作为分隔符。
my $max = 15;
my $min = 12;
my @list = $min .. $max; # (12, 13, 14, 15);
范围与数组是完全不同的类型对象,即使范围会创建类似数组的值列表。范围实现了一种称为 **延迟求值** 的行为:范围不会先计算所有值,而是以紧凑的方式存储为起始点和结束点。只有当真正读取范围中的值时,才会从范围中计算值。这意味着我们可以轻松地拥有无限范围,而不会占用我们计算机的所有内存。
my @range = 1..Inf; # Infinite range, finite memory use
my $x = @range[1000000]; # Calculated on demand
延迟求值不仅是范围的行为,它实际上是直接内置于 Raku 中,并在许多地方自动使用。这意味着大型数组不一定占用大量内存,而是可以仅在需要时计算值。
我们可以使用各种强制转换技术来指定数据项的上下文。$( )
强制括号之间的任何内容都被视为标量,即使它通常不是标量。@( )
和 %( )
对数组和哈希也做同样的事情。由于 ~
始终与字符串相关联,而 +
始终与数字相关联,因此我们可以使用它们将值分别强制转换为字符串和数字。
my Num $x = +"3"; # Becomes a number
my Str $y = ~3; # The string "3"
my $z = $(2..4); # Three elements, but still a single Range object
my @w = @("key" => "value"); # Convert hash to array
上面的例子并不是唯一可以将数据从一种类型转换为另一种类型的例子。强制转换允许我们将变量强制转换为特定类型。在某些情况下,复杂的变量类型或类可能会根据其强制转换方式表现出截然不同的行为。我们将在后面的章节中讨论这些情况。
这里是一个快速列出的上下文指定符。
+( )
- 转换为数字
~( )
- 转换为字符串
$( )
- 转换为标量
@( )
- 转换为数组
%( )
- 转换为哈希
我们之前讨论过 Raku 是一种动态语言,因此其中的变量和数据项没有预定义的类型。我们还提到了(几乎是在脚注中)Raku 也有一个类型系统,如果需要,可以可选地使用它。Raku 是一种非常灵活的语言,它被设计为让程序员能够以多种不同的方式进行编程。Raku 为编程提供的一种方法是结构化的、静态类型的编程。
如果您为变量指定了一个类型,Raku 会遵循该类型,并且只允许该类型的数据在该变量中使用。这在某些情况下非常有用,因为某些类别的操作会导致编译时错误而不是运行时错误。此外,如果编译器知道变量的类型永远不会改变,它可以自由地执行某些类型的优化。这些行为是依赖于实现的,当然 - 只有当你尝试利用它们时才会有效。一般规则是,你提供给编译器的信息越多,编译器就能为你进行越有用的分析。
我们之前还提到过,变量不需要在使用之前显式声明。这也稍微有点偏离了事实。变量不需要预先声明,但 Raku 让你可以选择在需要时进行声明。要预先声明变量,请使用 my
关键字。
my $x = 5;
my
定义变量为局部词法变量。另一种方法是将其定义为 our
,使其成为全局共享变量。Raku 中全局变量的另一个名称是“包变量”,因为它对整个软件包或文件可用。
our $x = 5;
全局变量很好,因为你可以在任何地方使用它们,而无需传递它们并跟踪它们。然而,与幼儿园不同,在大型程序中,共享并不总是最好的主意。
Raku 提供了一些 Raku 编译器事先知道的内置类型。你始终可以定义自己的类型,但 Raku 从一开始就提供了一些类型给你。这里部分列出了一些 Raku 的基本内置类型。这不是一个完整的列表,因为 Raku 中的一些类型在目前阶段无法理解。
- 布尔值
- 布尔值,真或假。布尔值是枚举类型,我们将在稍后更深入地讨论。布尔值只能是
True
或False
。 - 整数
- 基本整数值。
- 数组
- 由整型下标索引的值数组。
- 哈希
- 由字符串索引的值哈希。
- 数字
- 浮点数。
- 复数
- 类似浮点数,但也允许虚数和复数数据类型。
- 键值对
- 我们在讨论哈希时简要地提到了键值对。键值对是一个数据对象和一个字符串的组合。
- 字符串
- 字符串数据对象。
使用这些值,你可以像在普通静态类型语言中一样编写静态类型代码。
my Int $x;
$x = 5; # Good, it's an integer
$x = "five"; # Bad, just the name of an integer
我们还可以使用类型系统来捕获我们在变量之间移动数据时的错误。
my Int $foo = 5;
my Num $bar = 3.14;
$foo = $bar; # ERROR! $bar is not an Int!
通过这种方式,编译器可以告诉你,你是否试图以你没有预料到的方式使用变量。
在过去几章中,我们一直在研究 Raku 数据以及保存这些数据的变量。现在我们将探索获得这些数据后可以对它们做些什么。Perl 有一系列针对整数和浮点数的普通算术运算符,甚至还有一些针对其他数据类型的运算符。了解所有普通运算符后,我们就可以开始研究元运算符,它们将相同的概念应用于不同的上下文。
运算符在其 **操作数** 上工作,即进行运算的量。要理解任何运算符,你必须知道它的 **元数**(它需要多少个操作数)。如果它需要一个操作数,则称为 **一元** 运算符;如果需要两个操作数,则称为 **二元** 运算符;如果需要三个操作数,则称为 **三元** 运算符。
最简单的算术运算符是一元符号运算符 +
和 -
。它们应用于数值的前部,以影响该数字的符号。
my Int $x = 5;
$y = -$x # Now, $y is -5
还有一个前缀 +
运算符,它不反转符号。
与其他编程语言一样,Raku 有许多算术运算符。
my $x = 12;
my $y = 3;
my $z;
$z = $x + $y; # $z is 15
$z = $x - $y; # $z is 9
$z = $y - $x; # $z is -9
$z = $x * $y; # $z is 36
$z = $x / $y; # $z is 4
$z = $x % $y; # $z is 0 (remainder)
算术运算符期望数值参数,因此参数将自动转换为数字,就好像我们使用了上下文器 +( )
一样。
my Str $x = "123";
my Str $y = "456";
my Int $z = $x + $y; # 579
my Int $w = +($x) + +($y); # Same, but more verbose
在 Raku 中,符号 ~
始终与字符串相关联。当用作前缀时,它会将它所作用的任何变量转换为字符串,即使它之前不是字符串。当它用作两个字符串之间的运算符时,它将字符串首尾相连,这个过程称为 **连接**。
我们之前已经讨论过使用 ~( )
作为字符串上下文说明符。这被称为 **字符串化**。字符串化将变量从其他类型转换为字符串表示形式。
两个字符串可以连接在一起,生成一个新的字符串。
my Str $x = "hello " ~ "world!";
~
运算符会自动将任何不是字符串的参数字符串化。因此我们可以这样写
my Int $foo = 5;
my Str $bar = "I have " ~ $foo ~ " chapters to write";
print $bar;
这将打印出字符串:I have 5 chapters to write
。
在大多数情况下,使用插值可能更容易,我们将在后面讨论。
我们之前简要地介绍了三种基本类型的字符串:双引号字符串、单引号字符串和 heredocs。单引号和双引号字符串看起来可能很相似,但它们的行为彼此不同。区别在于 **插值**。
双引号字符串是插值的。出现在字符串内部的变量名将转换为它们的字符串值,并包含在字符串中。单引号字符串没有这种行为。
my Int $x = 5;
my Str $foo = "The value is $x"; # The value is 5
my Str $bar = 'The value is $x'; # The value is $x
变量增加或减少一个的操作非常常见,因此需要使用特定的运算符。++
和 --
运算符可以用作标量变量的前缀或后缀。这两个不同的位置有细微的差别。
my Int $x = 5;
$x++; # 6 (increment is done after)
++$x; # 7 (increment is done before)
$x--; # 6 (as above)
--$x; # 5 (as above)
两种形式,前缀形式和后缀形式,在上面的示例代码中看起来通常执行相同的操作。代码 ++$x
和 $x++
都执行相同的操作,但操作发生的时间不同。让我们用一些例子来说明这一点。
my Int $x = 5;
my Int $y;
$y = $x++; # $y is 5, $x is 6
$y = ++$x; # $y is 7, $x is 7
$y = $x--; # $y is 7, $x is 6
$y = --$x; # $y is 5, $x is 5
前缀版本在变量在语句中使用之前执行递增或递减。后缀版本在变量使用后执行递增或递减。
我们在前面的章节中看到了如何创建变量以及如何使用它们执行基本的算术运算和其他操作。现在我们将介绍流程控制的概念,使用用于 **分支** 和 **循环** 的特殊结构。
Perl 与其他语言(如 C、C++ 和 Java)共享许多通用的流程控制结构名称。Raku 选择重命名了一些这些通用结构。我们将在特别令人困惑的结构附近发布注释。 |
块是 { }
花括号内的代码块。这些块以多种方式与附近的其他代码区分开来。它们还为变量定义了新的 **范围**:使用 my
定义并在块内使用的变量在块外部不可见或不可用。这使得代码可以进行分隔,以确保仅用于临时用途的变量仅临时使用。
分支可以使用以下两个语句之一:**if** 和 **unless**。if
可以选择性地包含一个 **else** 子句。if
语句评估给定的条件,如果它是真语句,则执行 if
之后的块。当语句为假时,如果存在,则执行 else
块。unless
语句执行相反的操作。它评估条件,只有在条件为假时才执行其块。使用 unless
时,不能使用 else
子句。
有多种关系运算符可用于确定真值。以下是一些例子
$x == $y; # $x and $y are equal
$x > $y; # $x is greater than $y
$x >= $y; # $x is greater than or equal to $y
$x < $y; # $x is less than $y
$x <= $y; # $x is less than or equal to $y
$x != $y; # $x is not equal to $y
所有这些运算符都返回一个布尔值,可以将其分配给一个变量。
$x = (5 > 3); # $x is True
$y = (5 == 3); # $y is False
上面的括号仅用于清晰起见;它们实际上并非必需。
让我们从一个例子开始
my Int $x = 5;
if ($x > 3) {
say '$x is greater than 3'; # This prints
}
else {
say '$x is not greater than 3'; # This doesn't
}
请注意,在上面的示例中,if
和 ($x > 3)
之间有一个空格。这一点很重要,不是可选的。Raku 的解析规则在这点上很明确:任何词语后跟一个 (
开括号将被视为子例程调用。空格将此语句与子例程调用区分开来,并让解析器知道这是一个条件语句。
if($x > 5) { # Calls subroutine "if"
}
if ($x > 5) { # An if conditional
}
为了避免任何混淆,可以安全地省略括号
if $x > 5 { # Always a condition
}
unless
的行为与 if
相反。
my Int $x = 5;
unless $x > 3 {
say '$x is not greater than 3'; # This doesn't print
}
unless
之后不允许使用 else
子句。
if
和 unless
不仅可以用于标记要条件执行的块。它们还可以以自然的方式应用于语句的末尾,以仅影响该语句。
$x = 5 if $y == 3;
$z++ unless $x + $y > 8;
上面的这两行代码只有在满足其条件的情况下才会执行。第一行在 $y
等于 3 的情况下将 $x
设置为 5。第二行在 $x + $y
的总和大于 8 的情况下递增 $z
。
有时您需要检查两件事是否匹配。关系运算符==
检查两个值是否相等,但这非常有限。如果我们想要检查其他相等关系怎么办?我们想要的是一个无论这意味着什么,都能做我们想做的事的运算符。这个神奇的运算符就是智能匹配运算符~~
。
现在,当您看到~~
运算符时,您可能立即会想到字符串。智能匹配运算符在字符串中做了很多工作,但并不局限于字符串。
以下是一些智能匹配运算符起作用的示例
5 ~~ "5"; # true, same numerical value
["a", "b"] ~~ *, "a", *; # true, "a" contained in the array
("a" => 1, "b" => 2) ~~ *, "b", *; # true, hash contains a "b" key
"c" ~~ /c/; # true, "c" matches the regex /c/
3 ~~ Int # true, 3 is an Int
如您所见,智能匹配运算符可以用多种方式来测试两件事,以查看它们是否以某种方式匹配。在上面,我们看到了正则表达式的示例,我们将在后面的章节中更详细地讨论它。这也不是可以匹配事物的完整列表,我们将在整本书中看到更多内容。
Raku 有一个将数量与多个不同备选项进行匹配的功能。这种结构是given
和when
块。
given $x {
when Bool { say '$x is the boolean quantity ' ~ $x; }
when Int { when 5 { say '$x is the number 5'; } }
when "abc" { say '$x is the string "abc"'; }
}
每个when
都是一个智能匹配。上面的代码等效于以下代码
if $x ~~ 5 {
say '$x is the number 5';
}
elsif $x ~~ "abc" {
say '$x is the string "abc"';
}
elsif $x ~~ Bool {
say '$x is the boolean quantity ' ~$x;
}
given
/when
结构比if
/else
更简洁,并且在内部它可能以更优化的方式实现。
循环是多次重复某些语句组的方式。Raku 有许多可用的循环类型,每种类型都有不同的用途。
for 块接受数组或范围参数,并遍历每个元素。在最基本的情况下,for
将每个连续的值分配给默认变量$_
。或者,可以列出特定变量以接收值。以下是一些for
块的示例
# Prints the numbers "12345"
for 1..5 { # Assign each value to $_
.print; # print $_;
}
# Same thing, but using an array
my @nums = 1..5;
for @nums {
.print;
}
# Same, but uses an array that's not a range
my @nums = (1, 2, 3, 4, 5);
for @nums {
.print;
}
# Using a different variable than $_
for 1..5 -> $var {
print $var;
}
在上面所有示例中,for
的数组参数也可以可选地括在括号中。特殊的“尖角”语法->
将在后面详细解释,虽然值得注意的是,我们可以将其扩展为在每次循环迭代中从数组中读取多个值
my @nums = 0..5;
for @nums -> $even, $odd {
say "Even: $even Odd: $odd";
}
这将打印以下行
Even: 0 Odd: 1 Even: 2 Odd: 3 Even: 4 Odd: 5
for
也可以用作语句后缀,就像我们对if
和unless
所做的那样,尽管有一些警告
print $_ for (1..5); # Prints "12345"
print for (1..5); # Parse Error! Print requires an argument
.print for 1..5; # Prints "12345"
C 程序员会认识到loop
结构的行为,它与 C 中的for
循环具有相同的格式和行为。Raku 已经为我们上一节中看到的数组循环结构重新使用了名称for
,并使用名称loop
来描述 C 循环的增量行为。以下是 loop
结构
loop (my $i = 0; $i <= 5; $i++) {
print $i; # "12345"
}
通常,loop
包含以下三个组件
loop ( INITIALIZER ; CONDITION ; INCREMENTER )
loop
中的INITIALIZER
是一行在循环开始之前执行的代码,但具有与循环主体相同的词法范围。CONDITION
是在每次迭代之前检查的布尔测试。如果测试为假,循环退出,如果为真,循环重复。INCREMENTER
是在每次迭代开始之前,循环结束时发生的一条语句。所有这些部分都可以选择性地省略。以下五种方式可以写出同一个循环
loop (my $i = 0; $i <= 5; $i++) {
print $i; # "12345"
}
my $i = 0; # Small Difference: $i is scoped differently
loop ( ; $i <= 5; $i++) {
print $i;
}
loop (my $i = 0; $i <= 5; ) {
print $i; # "12345"
$i++;
}
loop (my $i = 0; ; $i++) {
last unless ($i <= 5);
print $i; # "12345"
}
my $i = 0;
loop ( ; ; ) {
last unless ($i <= 5);
print $i; # "12345"
$i++;
}
如果要使用无限循环,也可以省略括号而不是使用 (;;)
my $i = 0;
loop { # Possibly infinite loop
last unless ($i <= 5);
print $i; # "12345"
$i++;
}
重复块将至少执行一次其主体,因为条件位于块之后。在下面的示例中,您可以看到,即使$i
大于 2,块仍将运行。
my $i = 3;
repeat {
say $i;
} while $i < 2;
在代码重用方面,最基本的构建块是子程序。然而,它们并不是工具箱中唯一的构建块:Raku 还支持方法和子方法,我们将在讨论类和对象时讨论它们。
子程序是使用sub
关键字创建的,后面跟着名称、可选的参数列表,然后是一个代码块。
块是包含在{ }
大括号中的代码组。块有很多用途,包括将代码隔开、将多个语句组合在一起以及为变量创建作用域。
子程序是使用sub
关键字定义的。以下是一个示例
sub mySubroutine () {
}
括号用于定义子程序的形式参数列表。参数就像普通的my
局部变量,不同之处在于它们在子程序被调用时用值初始化。子程序可以使用return
关键字将结果传回其调用者
sub double ($x) {
my $y = $x * 2;
return $y;
}
可选参数在其后有?
。此外,可选参数可以使用=
给出默认值。必需参数在其后可能有一个!
,尽管这是位置参数的默认值。所有必需参数都必须列在所有可选参数之前。
sub foo (
$first, # First parameter, required
$second!, # Second parameter, required
$third?, # Third parameter, optional (defaults to undef)
$fourth = 4 # Fourth parameter, optional (defaults to 4)
)
正常参数按其位置传递:第一个传递的参数进入第一个位置参数,第二个进入第二个,依此类推。但是,也有一种方法可以按名称传递参数,并且可以按任何顺序传递。命名参数基本上是成对的,其中一个字符串名称与一个数据值相关联。命名数据值可以使用成对或副词语法传递。
sub mySub(:name($value), :othername($othervalue))
当然,子程序签名允许使用特殊简写,如果您的变量与成对的名称相同,您可以使用它
sub mySub(:name($name), :othername($othername))
sub mySub(:$name, :$othername) # Same!
在子程序声明中,命名参数必须放在所有必需和可选位置参数之后。命名参数默认情况下被视为可选,除非它们后面跟着!
。实际上,您也可以在必需的位置参数之后放一个!
,但这是默认值。
sub mySub(
:$name!, # Required
:$type, # Optional
:$method? # Still optional
)
Raku 还允许使用*@
语法进行所谓的“贪婪”参数。
sub mySub($scalar, @array, *@theRest) {
say "the first argument was: $scalar";
say "the second argument was: " ~ @array;
say "the rest were: " ~ @theRest;
}
*@
告诉 Raku 将剩余的参数展平到列表中并存储在数组@theRest
中。这是允许 perl 接受位置或命名数组而无需引用所必需的。
my $first = "scalar";
my @array = 1, 2, 3;
mySub($first, @array, "foo", "bar");
上面的代码将输出三行
- 第一个参数是:标量
- 第二个参数是:1, 2, 3
- 其余的是:"foo", "bar"
一旦定义了子程序,我们就可以稍后调用它来获取结果或操作。我们已经看到了内置的 say
函数,您可以向其传递字符串,并让这些字符串打印到控制台。我们可以使用上面的 double
函数来计算各种值。
my $x = double(2); # 4
my $y = double(3); # 6
my $z = double(3.5); # 7
我们可以使用 &
符号将子程序的引用存储到普通的标量变量中。
my $sub = &double;
my $x = $sub(7) # 14
在这个例子中,您看到我们正在向 double
子程序传递整数和浮点数。但是,我们可以使用类型说明符来限制值的类型。
sub double (Int $x) { # $x can only be an int!
return $x * 2;
}
my $foo = double(4); # 8
my $bar = double(1.5); # Error!
Raku 允许您编写多个具有相同名称的函数,只要它们具有不同的参数签名并且用关键字 multi
标记。这被称为 **多方法分派**,是 Raku 编程的重要方面。
multi sub double(Int $x) {
my $y = $x * 2;
say "Doubling an Integer $x: $y";
return $x * 2;
}
multi sub double(Num $x) {
my $y = $x * 2;
say "Doubling a Number $x: $y";
return $x * 2;
}
my $foo = double(5); # Doubling an Integer 5: 10
my $bar = double(3.5); # Doubling a Number 3.5: 7
我们可以定义一个 **匿名子程序** 并将对它的引用存储在变量中,而不是像往常一样命名子程序。
my $double = sub ($x) { return $x * 2; };
my $triple = sub ($x) { return $x * 3; };
my $foo = $double(5); # 10
my $bar = $triple(12); # 36
注意,我们也可以将这些代码引用存储在数组中。
my @times;
@times[2] = sub ($x) { return $x * 2; };
@times[3] = sub ($x) { return $x * 3; };
my $foo = @times[2](7); # 14
my $bar = @times[3](5); # 15
当我们讨论子程序时,我们发现子程序声明由三个部分组成:子程序名称、子程序参数列表以及子程序内部代码块。块在 Raku 中非常基础,我们现在将使用它们来做各种很酷的事情。
我们已经看到一些块在各种构造中使用。
# if/else statements
if $x == 1 {
}
else {
}
# subroutines
sub thisIsMySub () {
}
# loops
for @ary {
}
loop (my $i = 0; $i <= 5; $i++) {
}
repeat {
} while $x == 1;
所有这些块都用于将代码行组合在一起以实现特定目的。在 if
块中,当 if
条件为真时,块内的所有语句都会执行。如果条件为假,则整个块不会执行。在循环中,循环块中的所有语句都会一起重复执行。
除了将类似的代码组合在一起之外,块还引入了范围的概念。在块内定义的 my
变量在块外不可见。范围确保变量只在需要时使用,并且在不应该修改时不会被修改。块不需要与任何特定构造相关联,比如 if
或 loop
。块可以独立存在。
my $x = 5;
my $y = 5;
{
my $y = 3;
say $x; # 5
say $y; # 3
}
say $x; # 5
say $y; # 5
该示例很好地展示了范围的概念:块内的变量 $y
与块外的变量 $y
不相同。即使它们具有相同的名称,但它们具有不同的范围。以下是一个稍微不同的示例。
my $x = 5;
{
my $y = 7;
{
my $z = 9;
say $x; # 5
say $y; # 7
say $z; # 9
}
say $x; # 5
say $y; # 7
say $z; # ERROR: Undeclared variable!
}
say $x; # 5
say $y; # ERROR! Undeclared variable!
say $z; # ERROR! Undeclared variable!
变量 $x
从其定义点开始可见,并且在定义它的范围内的所有范围内部也可见。但是,$y
只能在定义它的块和该块内部的块内可见。$z
只能在最内层的块中可见。
在存在歧义的情况下,可以准确地指定范围。我们可以使用 OUTER
等关键字来指定来自当前范围直接上层范围的变量。
my $x = 5;
{
my $x = 6;
say $x; # 6
say $OUTER::x # 5
}
子程序可以使用 CALLER
范围访问它们被调用的范围,假设外层范围中的变量被声明为 is context
。
my $x is context = 5;
mySubroutine(7);
sub mySubroutine($x) {
say $x; # 7
say $CALLER::x; # 5
}
块可以作为代码引用存储在单个标量变量中。一旦存储在代码引用变量中,块就可以像常规子程序引用一样执行。
my $dostuff = {
print "Hello ";
say "world!";
}
$dostuff();
我们在上面的示例中看到,块可以存储在变量中。此操作创建一个 **闭包**。闭包是一个存储的代码块,它保存其当前状态和当前范围,这些状态和范围可以在以后访问。让我们看看闭包的实际应用。
my $block;
{
my $x = 2;
$block = { say $x; };
}
$block(); # Prints "2", even though $x is not in scope anymore
闭包在闭包创建时保存对 $x
变量的引用。即使该变量在代码块执行时不再处于作用域内。
当我们稍后更改 $x 时,闭包将看到更改后的值,因此如果您想创建多个包含不同封闭变量的闭包,则每次都必须创建一个新变量。
my @times = ();
for 1..10 {
my $t = $_; # each subroutine gets a different $t
@times[$_] = sub ($a) { return $a * $t; };
}
say @times[3](4); # 12
say @times[5](20); # 100
say @times[7](3); # 21
我们可以使用 sub
关键字来创建子程序或子程序引用。这不是执行此操作的唯一语法,实际上对于未命名(“匿名”)子程序或子程序引用的常见情况来说,它有点冗长。对于这些,我们可以使用一种称为 **尖角块** 的构造。尖角块(在其他语言中称为 *lambda* 块)非常有用。它们可以像匿名子程序一样创建代码引用,并且还可以创建带参数的代码块。尖角块很像未命名的子程序。更一般地说,它就像一个带参数的块。当我们讨论循环时,我们简要地看到了尖角块。我们在循环构造关联中使用尖角块来为循环变量命名,而不是依赖默认变量 $_
。这就是我们在这些情况下使用尖角块的原因:它们使我们能够指定变量名用作任意代码块的参数。
我们将展示一些示例。
my @myArray = (1, 2, 3, 4, 5, 6);
# In a loop:
for @myArray -> $item {
say $item;
# Output is:
# 1
# 2
# 3
# 4
# 5
# 6
}
# In a loop, multiples
for @myArray -> $a, $b, $c {
say "$a, $b, $c";
# Output is:
# 1, 2, 3
# 4, 5, 6
}
# As a condition:
my $x = 5;
if ($x) -> $a { say $a; } # 5
# As a coderef
my $x = -> $a, $b { say "First: $a. Second: $b"; }
$x(1, 2); # First: 1, Second: 2
$x("x", "y"); # First: x, Second: y
# As an inline coderef
-> $a, $b { say "First: $a, Second: $b"; }(1, 2)
#In a while loop
while ($x == 5) -> $a {
say "Boolean Value: $a";
}
在块中,如果我们不想费力地写出参数列表,我们可以使用占位符参数。占位符使用特殊的 ^
twigil。传递的值将按字母顺序分配给占位符变量。
for 1..3 {
say $^a; # 1
say $^c; # 3
say $^b; # 2
}
到目前为止我们看到的都是面向过程编程的基础:表达式列表、分支、循环以及告诉计算机做什么以及如何准确地执行的子程序。Raku 非常支持面向过程编程,但这不是 Raku 支持的唯一编程风格。Raku 很好地适应的另一个常见范式是面向对象方法。
对象是数据及其作用于该数据的操作的组合。对象的數據稱為其屬性,而對象的操作稱為其方法。从这个意义上说,属性定义了状态,方法定义了对象的行为。
类是创建对象的模板。当引用特定类的对象时,通常将该对象称为该类的实例。
类使用 class
关键字定义,并被赋予一个名称。
class MyClass {
}
在类声明中,您可以定义属性、方法或子方法。
属性使用 has
关键字定义,并使用特殊的语法指定。例如,让我们考虑以下类
class Point3D {
has $!x-axis;
has $!y-axis;
has $!z-axis;
}
类 Point2D 定义了一个三维坐标系中的点,它有三个名为 x 轴、y 轴和 z 轴的属性。
在 Raku 中,所有属性都是私有的,使用 ! 标识符可以明确地表达这一点。使用 ! 标识符声明的属性只能在类中使用 !attribute-name 直接访问。这种方式声明属性的另一个重要后果是,对象不能使用默认的 new 构造函数进行填充。
如果使用 . 标识符声明属性,则会自动生成一个只读访问器[检查拼写] 方法。您可以将 . 标识符理解为 "属性 + 访问器"。这个访问器是一个以其属性命名的方法,可以从类外部调用并返回其属性的值。为了允许通过提供的访问器对属性进行更改,必须在属性中添加 is rw
特性。
之前的 Point3D 类可以声明如下
class Point3D {
has $.x-axis;
has $.y-axis;
has $.z-axis is rw;
}
鉴于 . 标识符声明了一个 ! 标识符和一个访问器[检查拼写] 方法,即使使用 . 标识符声明属性,属性也可以始终使用 ! 标识符。
方法的定义与普通子程序类似,但有一些关键区别
- 方法使用
method
关键字代替sub
。 - 方法有特殊的变量
self
,它指的是正在调用方法的对象。这被称为**调用者**。 - 方法可以直接访问对象的内部特征。
在定义方法时,您可以为调用者指定不同的名称,而不是必须使用 self
。为此,您将它放在方法签名的开头,并用冒号与签名中的其他部分隔开
method myMethod($invocant: $x, $y)
在这种情况下,冒号被视为一种特殊的逗号,因此您可以用额外的空格来写它,如果这样更容易
method myMethod($invocant : $x, $y)
这是一个例子
class Point3D {
has $.x-axis;
has $.y-axis;
has $.z-axis;
method set-coord($new-x, $new-y, $new-z) {
$!x-axis = $new-x;
$!y-axis = $new-y;
$!z-axis = $new-z;
}
method print-point {
say "("~$!x-axis~","~$!y-axis~","~$!z-axis~")";
}
# method using the self invocant
method distance-to-center {
return sqrt(self.x-axis ** 2 + self.y-axis ** 2);
}
# method using a custom invocant named $rect
method polar-coordinates($rect:) {
my $r = $rect.distance-to-center;
my $theta = atan2($rect.y-axis, $rect.x-axis);
return "("~$r~","~$theta~","~$rect.z-axis~")";
}
}
对象是数据项,其类型为给定的类。对象包含类定义的任何属性,并且还可以访问类中的任何方法。对象使用 new
关键字创建。
使用 Point3D 类
my $point01 = Point3D.new();
**类构造函数** new()
可以使用命名方法来初始化类的任何属性
# Either syntax would work for object initialization
my $point01 = Point3D.new(:x-axis(3), :y-axis(4), :z-axis(6));
my $point02 = Point3D.new(x-axis => 3, y-axis => 4, z-axis => 6);
该类中的方法使用点表示法调用。这是对象、句点以及该方法之后的名称。
$point01.print-point();
say $point01.polar-coordinates();
当点表示法方法调用没有提供对象时,默认变量 $_
会被使用
$_ = Point3D.new(:x-axis(6), :y-axis(8), :z-axis(6));;
.print-point();
say .polar-coordinates();
基本类系统使数据和操作这些数据的代码例程能够以逻辑方式捆绑在一起。但是,大多数类系统的更高级功能也支持继承,这允许类相互构建。**继承**是类形成逻辑层次结构的能力。Raku 支持类和子类的正常继承,但也支持称为**mixin**和**role**的特殊高级功能。我们必须将其中一些更复杂的功能留到后面的章节中,但这里将介绍一些基础知识。
我们在前面的章节中讨论了 Perl 的一些基本类型。您可能会惊讶地发现,所有 Raku 数据类型都是类,并且所有这些值都具有内置的方法可以使用。在这里,我们将讨论一些可以调用我们在迄今为止看到的各种对象的方法。
我们已经看到了 print
和 say
内置函数。所有内置类都有同名方法,这些方法打印对象的字符串化形式。
我们将快速离题,讨论 eval
函数。eval
允许我们在运行时编译和执行 Raku 代码字符串。
eval("say 'hello world!';");
所有 Raku 对象都有一个名为 .perl
的方法,该方法返回一个 Raku 代码字符串,代表该对象。
my Int $x = 5;
$x.perl.say; # "5"
my @y = (1, 2, 3);
@y.perl.say; # "[1, 2, 3]"
my %z = :first(1), :second(2), :third(3);
%z.perl.say; # "{:first(1), :second(2), :third(3)}"
有一些方法可以被调用来显式地将给定的数据项更改为不同的形式。这就像一种显式的方式,强制给定的数据项在不同的上下文中被使用。以下是一个部分列表
.item
- 在标量上下文中返回项目。
.iterator
- 返回对象的迭代器。我们将在后面的章节中讨论迭代器。
.hash
- 在哈希上下文中返回对象
.list
- 在数组或 "列表" 上下文中返回对象
.Bool
- 返回对象的布尔值
.Array
- 返回包含对象数据的数组
.Hash
- 返回包含对象数据的哈希表
.Iterator
- 返回对象的迭代器。我们将在后面的章节中讨论迭代器。
.Scalar
- 返回对对象的标量引用
.Str
- 返回对象的字符串表示
.WHENCE
- 返回对象类型的自动生存闭包的代码引用。我们将在后面讨论自动生存和闭包。
.WHERE
- 返回数据对象的内存位置地址
.WHICH
- 返回对象的标识值,对于大多数对象来说,它只是它的内存位置(它的 .WHERE)
.HOW
- (HOW = Higher Order Workings) 返回处理此对象的元类
.WHAT
- 返回当前对象的类型对象
此页面或部分是一个未完成的草稿或提纲。 您可以帮助 开发作品,或者您可以在 项目室寻求帮助。 |
现在我们已经涵盖了 Raku 编程的大部分基础知识。当然,我们并没有涵盖整个语言。但是,我们已经看到了用于普通编程任务的基本工具类型。还有很多东西需要学习,许多高级工具和功能可以用来使常见任务更容易,而困难的任务则变得可能。我们将在稍后介绍一些更高级的功能,但在本章中,我们想通过谈论一些关于注释和文档的内容来结束“基础”部分。
我们之前提到过,注释是源代码中的注释,旨在供程序员阅读,并且被 Raku 解释器忽略。Raku 中最常见的注释形式是单行注释,它以单个井号 # 开头,一直延伸到行尾。
# Calculate factorial of a number using recursion
sub factorial (Int $n) {
return 1 if $n == 0; # This is the base case
return $n * factorial($n - 1); # This is the recursive call
}
当以上代码执行时,所有以单个井号 # 为前缀的文本将被 Raku 解释器忽略。
多行注释
[edit | edit source]虽然 Perl 不提供多行注释,但 Raku 提供。为了在 Raku 中创建多行注释,注释必须以单个井号、反引号、然后是一些开括号字符开头,并以匹配的闭括号字符结尾。
sub factorial(Int $n) {
#`( This function returns the factorial of a given parameter
which must be an integer. This is an example of a recursive
function where there is a base case to be reached through
recursive calls.
)
return 1 if $n == 0; # This is the base case
return $n * factorial($n - 1); # This is the recursive call
}
此外,注释的内容也可以嵌入到行内。
sub add(Int $a, Int $b) #`( two (integer) arguments must be passed! ) {
return $a + $b;
}
POD 文档
[edit | edit source]规则和语法
[edit | edit source]正则表达式
[edit | edit source]正则表达式
[edit | edit source]正则表达式是一种用于指定和搜索字符串中的模式的工具,除此之外还有其他用途。正则表达式是 Perl 中一个流行且强大的部分,尽管它们在该语言的后续版本中逐渐增长和扩展,以一种难以跟踪和实现的方式。
随着更多操作符和元字符被添加到引擎中,Perl 的正则表达式变得越来越难以使用和理解。因此,人们决定 Raku 将打破这种语法,从头开始重写正则表达式,使其更加灵活,并更好地集成到语言中。在 Raku 中,它们被称为regexes,并且变得更加强大。
Raku 以两种方式支持 regexes:它有一个支持 Perl 风格正则表达式的遗留模式,以及一个支持新风格正则表达式的正常模式。
基本量词
[edit | edit source]Regexes 描述了可以搜索和操作的字符串数据中的模式。要搜索的最基本模式之一是重复模式。为了描述重复,可以使用一些量词。
操作符 | 含义 | 示例 | 说明 |
---|---|---|---|
* |
"零个或多个" | B A* |
接受一个以 'B' 开头,后面跟随任意数量的 'A' 字符的字符串,即使是零个 'A' 字符。B 、BAAAAA 等。 |
+ |
"一个或多个" | B A+ |
接受一个以 'B' 开头,后面跟随至少一个 'A' 的字符串。例如:BAAA 或 BA ,但不包括 B 。 |
? |
"一个或零个" | B A? |
匹配一个 'B',可选地跟随一个 'A'。B 或 BA |
** |
"这么多" | B A**5 |
匹配一个以 'B' 开头,后面跟随恰好 5 个 'A' 字符的字符串。BAAAAA |
B A ** 2..5 |
匹配一个以 'B' 开头,后面跟随至少两个 'A',但不超过 5 个 'A' 的字符串。BAA 、BAAA 、BAAAA 、BAAAAA |
语法
[edit | edit source]语法
[edit | edit source]正则表达式本身很有用,但有限。重用正则表达式可能很困难,将它们分组为逻辑分组可能很困难,从一个分组继承到另一个分组可能非常困难。这就是语法的作用所在。语法对于正则表达式而言就像类对于数据和代码例程一样。语法允许正则表达式像编程语言中正常的首要组件一样工作,并利用类系统的强大功能。语法可以像类一样被继承和重载。实际上,Raku 语法本身可以被修改,以便在运行时为语言添加新功能。我们将在后面看到这些例子。
规则、标记和原型
[edit | edit source]语法被分解成称为规则、标记和原型的组件。标记就像我们已经见过的正则表达式。规则就像子程序,因为它们可以调用其他规则或标记。原型就像默认的多子程序,它们定义了一个可以被重写的规则原型。
标记
[edit | edit source]标记是不回溯的正则表达式,这意味着如果表达式的一部分已经被匹配,即使这会阻止表达式更大的一部分匹配,这一部分也不会被改变。虽然这牺牲了正则表达式的部分灵活性,但它允许更高效地创建更复杂的解析器。
token number {
\d+ ['.' \d+]?
}
规则
[edit | edit source]规则是将标记和其他规则组合在一起的方法。规则都带有名称,并且可以使用< >
尖括号引用同一个语法中的其他规则或标记。像标记一样,它们不回溯,但它们内部的空格被逐字解释,而不是被忽略。
rule URL {
<protocol>'://'<address>
}
此规则匹配一个 URL 字符串,其中协议名称(例如 "ftp" 或 "https")后面跟着字面符号 "://",然后是一个表示地址的字符串。此规则依赖于两个子规则,<protocol>
和 <address>
。这些可以定义为标记或规则,只要它们在同一个语法中即可。
grammar URL {
rule TOP {
<protocol>'://'<address>
}
token protocol {
'http'|'https'|'ftp'|'file'
}
rule address {
<subdomain>'.'<domain>'.'<tld>
}
...
}
原型
[edit | edit source]原型定义了规则或标记的类型。例如,我们可以定义一个原型标记<protocol>
,然后定义几个表示不同协议的标记。在一个标记内部,我们可以将其名称引用为<sym>
grammar URL {
rule TOP {
<protocol>'://'<address>
}
proto token protocol {*}
token protocol:sym<http> {
<sym>
}
token protocol:sym<https> {
<sym>
}
token protocol:sym<ftp> {
<sym>
}
token protocol:sym<ftps> {
<sym>
}
...
}
这相当于说
token protocol {
<http> | <https> | <ftp> | <ftps>
}
token http {
http
}
...
但它更具可扩展性,允许以后指定协议类型。例如,如果我们想要定义一个新的URL
类型,它也支持 "spdy" 协议,我们可以使用
grammar URL::WithSPDY is URL {
token protocol:sym<spdy> {
<sym>
}
}
匹配语法
[edit | edit source]一旦我们有了像上面定义的语法,我们就可以使用.parse
方法来匹配它。
my Str $mystring = "http://www.wikibooks.org";
if URL.parse($mystring) {
#if it matches a URL, do something
}
匹配对象
[edit | edit source]匹配对象是一种特殊的数据类型,它表示语法的解析状态。当前匹配对象存储在特殊变量$/
中。
解析器操作
[edit | edit source]通过将语法与解析器操作类组合,可以将语法变成交互式解析器。当语法匹配某些规则时,可以调用相应的操作方法,并传入当前匹配对象。
操作符重载
[edit | edit source]操作符只有5种类型:中缀、前缀、后缀、环绕和后环绕。
您可以像这样声明一个新的操作符
sub postfix:<!>(Int $n!) { [*] 1..$n }
say 5!; # prints 120
如您所见,以上代码声明了一个操作符 '!' 用于计算整数的阶乘。
语言扩展
[edit | edit source]联结最初是作为一个高级的 Perl 模块的一部分实现的,以简化一些常见的操作。假设我们有一个复杂的条件,我们需要将变量 $x
与多个离散值中的一个进行比较
if ($x == 2 || $x == 4 || $x == 5 || $x == "hello"
|| $x == 42 || $x == 3.14)
这非常混乱。我们想要做的是创建一个值的列表,并询问“如果 $x
是这些值中的一个”。联结允许这种行为,但也能做更多的事情。以下是使用联结编写的相同语句
if ($x == (2|4|5|"hello"|42|3.14))
联结有 4 种基本类型:any(所有组件的逻辑 OR)、all(所有组件的逻辑 AND)、one(所有组件的逻辑 XOR)和 none(所有组件的逻辑 NOR)。
列表运算符将联结构建为一个列表
my $options = any(1, 2, 3, 4); # Any of these is good
my $requirements = all(5, 6, 7, 8); # All or nothing
my $forbidden = none(9, 10, 11); # None of these
my $onlyone = one(12, 13, 4); # One and only one
另一种指定联结的方法是使用中缀运算符,就像我们已经看到的那样
my $options = 1 | 2 | 3 | 4; # Any of these is good
my $requirements = 5 & 6 & 7 & 8; # All or nothing
my $onlyone = 12 ^ 13 ^ 4; # One and only one
请注意,没有中缀运算符来创建 none()
联结。
联结与 Raku 中的任何其他数据类型一样,可以使用智能匹配运算符 ~~
进行匹配。该运算符将根据要匹配的联结类型自动执行正确的匹配算法。
my $junction = any(1, 2, 3, 4);
if $x ~~ $junction {
# execute block if $x is 1, 2, 3, or 4
}
if 1 ~~ all(1, "1", 1.0) # Success, all of them are equivalent
if 2 ~~ all(2.0, "2", "foo") # Failure, the last one doesn't match
只有当 all()
联结中的所有元素都与对象 $x
匹配时,它才会匹配。如果任何元素不匹配,整个匹配将失败。
只有当 one()
联结中恰好只有一个元素匹配时,它才会匹配。多一个或少一个,整个匹配都将失败。
if 1 ~~ one(1.0, 5.7, "garbanzo!") # Success, only one match
if 1 ~~ one(1.0, 5.7, Int) # Failure, two elements match
只要至少有一个元素匹配,any()
联结就会匹配。可以是一个或其他任何数量,但不能为零。any
联结失败的唯一方法是所有元素都不匹配。
if "foo" ~~ any(String, 5, 2.18) # Success, "foo" is a String
if "foo" ~~ any(2, Number, "bar") # Failure, none of these match
只有当联结中没有任何元素匹配时,none()
联结才会成功匹配。这样,它等同于 any()
联结的逆。如果 any()
成功,none()
失败。如果 any()
失败,none()
成功。
if $x ~~ none(1, "foo", 2.18)
if $x !~ any(1, "foo", 2.18) # Same thing!
在大多数传统的计算系统中,数据对象被分配到一个固定的大小,并将它们的值填充到内存中的空间中。例如,在 C 中,如果我们声明一个数组 int a[10]
,则数组 a
将是一个固定大小的数组,有足够的空间来存储恰好 10 个整数。如果我们要存储 100 个整数,我们需要分配一个可以存储 100 个整数的空间。如果我们要存储一百万个整数,我们需要分配一个大小为一百万的数组。
让我们考虑一个问题,我们要计算一个乘法表,一个二维数组,其中数组中给定单元格的值是它两个索引的乘积。这是一个简单的循环,可以使用它来生成因子不超过 N 的表格
int products[N][N];
int i, j;
for(i = 0; i < N; i++) {
for(j = 0; j < N; j++) {
products[i][j] = i * j;
}
}
创建此表格可能需要一段时间才能执行所有 N2 操作。当然,一旦我们初始化了表格,在其中查找值的速度非常快。这里要考虑的另一件事是,我们最终计算的值比我们实际使用的值还要多,因此这是浪费的努力。
现在,让我们看看执行相同操作的函数
int product(int i, int j) {
return i * j;
}
此函数不需要任何启动时间来初始化它的值,但是每次调用都需要额外的时间来计算结果。它比数组启动速度更快,但每次访问所花费的时间比数组多。
结合这两个想法,我们就得到了延迟列表。
延迟列表就像数组,但也有一些主要区别
- 它们并不一定用预定义的大小声明。它们可以是任何大小,甚至可以是无限长的。
- 它们在需要之前不会计算它们的值,并且只在需要时计算需要的值。
- 一旦它们的值被计算出来,就可以存储起来以进行快速查找。
延迟列表的相反是急切列表。急切列表立即计算并存储所有值,就像 C 中的数组一样。急切列表不能无限长,因为它们需要将它们的值存储在内存中,而计算机没有无限的内存。
Raku 同时拥有这两种类型的列表,它们在内部处理,无需程序员干预。可以延迟的列表将被延迟处理。不能延迟的列表将被急切地计算并存储。延迟列表在存储空间和计算开销方面提供了优势,因此 Raku 尝试默认使用它们。Raku 还提供了一些可以用来支持延迟并提高列表计算性能的构造。
我们已经看到了范围。范围默认情况下是延迟的,这意味着范围中的所有值并不一定在您将它们分配给数组时计算
my @lazylist = 1..20000; # Doesn't calculate all 20,000 values
由于它们的延迟性,范围甚至可以是无限的
my @lazylist = 1..Inf; # Infinite values!
迭代器是特殊的数据项,它们一次遍历复杂数据对象中的一个元素。想象一下文本编辑器程序中的光标;光标读取一次按键,将字符插入到它当前的位置,然后移动到下一个位置等待下一个按键。这样,一个长字符数组可以一次插入一个字符,而您(编辑器)无需手动移动光标。
同样,Raku 中的迭代器自动遍历数组和散列,自动跟踪您在数组中的当前位置,因此您无需手动跟踪。我们之前在循环讨论中已经看到了迭代器的使用,尽管我们没有将它们称为“迭代器”。以下是两个执行相同功能的循环
my @x = 1, 2, 3, 4, 5;
loop(my int $i = 0; $i < @x.elems; $i++) {
@x[$i].say;
}
for @x { # Same, but much shorter!
$_.say;
}
第一个循环使用 $i
变量手动遍历 @x
数组,以跟踪当前位置,并使用 $i < @x.length
测试来确保我们没有到达末尾。在第二个循环中,for
关键字为我们创建了一个迭代器。迭代器会自动跟踪我们当前在数组中的位置,自动检测我们何时到达数组末尾,并自动将每个后续值加载到 $_
默认变量中。值得一提的是,我们可以使用一些 Raku 习语使它更短
.say for @x;
迭代器是任何实现了 `Iterator` 角色的对象。我们将在稍后讨论 **角色**,但现在只需要知道角色是其他类可以参与的标准接口。由于它们可以是任何类,只要它具有标准接口,迭代器就可以执行我们定义的任何操作。迭代器可以轻松遍历数组和哈希表,但专门定义的类型也可以遍历树、图、堆、文件以及各种其他数据结构和概念。
如果数据项具有关联的迭代器类型,则可以通过 `Iterator()` 方法访问它。此方法在大多数情况下由 `for` 循环等结构在内部调用,但如果你确实需要,你可以访问它。
数据流
[edit | edit source]数据流提供了一种直观的图形化方式来展示数据在复杂赋值语句中的流动。数据流有两个端点,一个“钝端”和一个“尖端”。钝端连接到数据源,数据源是值列表。尖端连接到接收器,接收器可以一次接收至少一个元素。数据流可以用来将数据从右到左或从左到右发送,具体取决于数据流指向的方向。
my @x <== 1..5;
say @x # 1, 2, 3, 4, 5
@x ==> @y ==> print # 1, 2, 3, 4, 5
say @y # 1, 2, 3, 4, 5
分层数据流将数据从一个数据流移动到另一个数据流。但是,有两个端点的数据流会附加到数据流链中的最后一个项目。
my @x = 1..5;
@x ==> map {$_ * 2} ==> @y;
say @x; # 1, 2, 3, 4, 5
say @y; # 2, 4, 6, 8, 10
@x ==>>
@y ==> @z;
say @z # 1, 2, 3, 4, 5, 2, 4, 6, 8, 10
收集和获取
[edit | edit source]我们可以使用 `gather` 和 `take` 关键字编写我们自己的迭代器类型。这两个关键字的行为非常类似于我们之前见过的尖角块。但是,与尖角块不同,`gather` 和 `take` 可以返回值。与尖角块类似,`gather` 和 `take` 可以与循环结合使用,形成自定义迭代器。
`gather` 用于定义一个特殊的块。该块的代码可以执行任意计算,并使用 `take` 返回一个值。以下是一个示例
my $x = gather {
take 5;
}
say $x; # 5
这本身并不太有用。但是,我们现在可以将其与循环结合使用,以返回一个长值列表。
my @x = gather for 1..5 {
take $_ * 2;
}
say @x # 2, 4, 6, 8, 10
`take` 运算符执行两个操作:它捕获传递给它的值,并将该值作为 `gather` 块的结果之一返回;它还返回传递给它的值以供存储。我们可以很容易地将这种行为与 `state` 变量结合使用,以递归地使用值。
my @x = gather for 1..5 {
state $a = $_;
$a = take $_ + $a;
}
say @x; # 2, 4, 7, 11, 16
元运算符
[edit | edit source]元运算符
[edit | edit source]运算符对数据执行操作。元运算符对运算符执行操作。
列表运算符
[edit | edit source]归约运算符
[edit | edit source]归约运算符作用于列表,并返回一个标量值。它们通过在数组中的每对元素之间应用归约运算符来实现这一点。
my @nums = 1..5;
my $sum = [+] @nums # 1 + 2 + 3 + 4 + 5
`[]` 方括号将任何通常作用于标量的运算符转换为归约运算符,以对列表执行相同的操作。归约也可以与关系运算符一起使用。
my $x = [<] @y; # true if all elements of @y are in ascending order
my $z = [>] @y; # true if all elements of @y are in descending order
超运算符
[edit | edit source]归约运算符将运算符应用于数组中的所有元素,并将数组缩减为单个标量值。超运算符将操作分布到列表中的所有元素,并返回所有结果的列表。超运算符使用特殊的“法语引号”符号构造:« 和 ». 如果你键盘不支持这些符号,可以使用 ASCII 符号 `>>` 和 `<<` 代替。
my @a = 1..5;
my @b = 6..10;
my @c = @a »*« @b;
# @c = 1*6, 2*7, 3*8, 4*9, 5*10
你也可以将一元运算符与超运算符一起使用。
my @a = (2, 4, 6);
my @b = -« @a; # (-2, -4, -6)
一元超运算符始终返回一个与它接收的列表大小完全相同的数组。二元超运算符的行为不同,具体取决于其操作数的大小。
@a »+« @b; # @a and @b MUST be the same size
@a «+« @b; # @a can be smaller, will upgrade
@a »+» @b; # @b can be smaller, will upgrade
@a «+» @b; # Either can be smaller, Perl will Do What You Mean
将超运算符符号指向不同的方向会影响 Raku 对元素的处理。在尖端,它会扩展数组,使其与钝端上的数组一样长。如果两端都尖锐,它会扩展较小的一个。
超运算符也可以与赋值运算符一起使用。
@x »+=« @y # Same as @x = @x »+« @y
交叉运算符
[edit | edit source]交叉是“X”大写字母符号。作为运算符,交叉返回通过组合其操作数的元素而形成的所有可能列表的列表。
my @a = 1, 2;
my @b = 3, 4;
my @c = @a X @b; # (1,3), (1,4), (2,3), (2,4)
交叉也可以用作元运算符,对每个操作数的每个可能元素组合应用它修改的运算符。
my @a = 1, 2;
my @b = 3, 4;
my @c = @a X+ @b; # 1+3, 1+4, 2+3, 2+4
角色和继承
[edit | edit source]继承
[edit | edit source]基本类系统使数据和对该数据进行操作的代码例程能够以逻辑方式捆绑在一起。但是,大多数类系统还具有更高级的功能,这些功能也支持继承,这使类可以相互构建。 **继承** 是类形成逻辑层次结构的能力。Raku 支持类的正常继承和子类,但也支持称为 **mixin** 和 **角色** 的特殊高级功能。我们必须将这些功能中比较困难的部分保留到后面的章节中讨论,但我们会在本章中介绍它们。
类继承
[edit | edit source]角色和 `does`
[edit | edit source]mixin
[edit | edit source]参数化角色
[edit | edit source]块和子例程
[edit | edit source]高级子例程
[edit | edit source]高级子例程
[edit | edit source]我们之前讨论过子例程和代码引用,但这些问题还有很多内容需要学习,而我们在那章中没有足够的空间进行讨论。现在我们将介绍子例程和代码引用的一些更高级的功能。
`Code` 对象
[edit | edit source]从最基本的意义上来说,异常代表着由程序导致的错误。但是,与让程序崩溃不同,异常有机会被捕获并优雅地处理。异常被称为被**抛出**或**引发**,特殊的代码块称为**处理程序**可以**捕获**它们。
我们在上一章中看到了特殊的`CATCH`块,它用于处理从`CATCH`所在的块中抛出的异常。除了`CATCH`,还有许多其他特殊的**属性块**,可用于修改它们所在的块的行为。
属性块本质上是词法性的:它们修改了定义它们的块的行为,并且不影响外部作用域。
在 Raku 中,任何与文件的交互都是通过文件句柄进行的。[注释 1] 文件句柄是外部文件的内部名称。`open`函数在内部名称和外部名称之间建立关联,而`close`函数则断开这种关联。一些 IO 句柄可以在无需创建的情况下使用:`$*OUT`和`$*IN`分别连接到 STDOUT 和 STDIN,即标准输出和标准输入流。您需要自己打开所有其他文件句柄。
始终记住,程序中的任何文件路径都是相对于当前工作目录的。
要打开一个文件,我们需要创建一个文件句柄。这仅仅意味着我们创建一个(标量)变量,它将从现在开始引用该文件。双参数语法是调用`open`函数的最常见方式:`open PATHNAME, MODE`——其中`PATHNAME`是要打开文件的外部名称,而`MODE`是访问类型。如果成功,这将返回一个 IO 句柄对象,我们可以将其放入标量容器中
my $filename = "path/to/data.txt";
my $fh = open $filename, :r;
`:r`以只读模式打开文件。为了简短,您可以省略`:r`——因为它就是默认模式;当然,`PATHNAME`字符串可以直接传递,而不是通过`$filename`变量传递。
一旦我们有了文件句柄,就可以读取文件并对文件执行其他操作。
请使用 `slurp` 和 `spurt` 代替,因为
"file.txt".IO.spurt: "file contents here";
"file.txt".IO.slurp.say; # «file contents here»
读取文件最通用的方法是使用 `open` 函数建立与资源的连接,然后执行数据读取步骤,最后在打开过程中接收的文件句柄上调用 `close` 函数。
my $fileName;
my $fileHandle;
$fileName = "path/to/data.txt";
$fileHandle = open $fileName, :r;
# Read the file contents by the desiderated means.
$fileHandle.close;
要将文件数据立即完全传输到程序中,可以使用 `slurp` 函数。通常,这涉及将获得的字符串存储到变量中,以便进行进一步操作。
my $fileName;
my $fileHandle;
my $fileContents;
$fileName = "path/to/data.txt";
$fileHandle = open $fileName, :r;
# Read the complete file contents into a string.
$fileContents = $fileHandle.slurp;
$fileHandle.close;
如果由于行导向的编程任务或内存考虑,不希望完全读取数据,可以通过 `IO.lines` 函数逐行读取文件。
my $fileName;
my $fileHandle;
$fileName = "path/to/data.txt";
$fileHandle = open $fileName, :r;
# Iterate the file line by line, each line stored in "$currentLine".
for $fileHandle.IO.lines -> $currentLine {
# Utilize the "$currentLine" variable which holds a string.
}
$fileHandle.close;
使用文件句柄可以重复利用资源,但另一方面也要求程序员管理它。如果提供的优势不值得这些花费,上面提到的函数可以直接在以字符串形式表示的文件名上工作。
可以通过指定文件名作为字符串并调用 `IO.slurp` 来读取完整的文件内容。
my $fileName;
my $fileContents;
$fileName = "path/to/data.txt";
$fileContents = $fileName.IO.slurp;
如果这种面向对象的 approach 不适合你的风格,等效的程序化变体是
my $fileName;
my $fileContents;
$fileName = "path/to/data.txt";
$fileContents = slurp $fileName;
以相同的模式,逐行处理不依赖于文件句柄的方式是
my $fileName;
my $fileContents;
$fileName = "path/to/data.txt";
for $fileName.IO.lines -> $line {
# Utilize the "$currentLine" variable, which holds a string.
}
请记住,可以选择在不将文件名存储在变量中的情况下插入它,这将进一步缩短以上代码段。相应地,传输完整的文件内容可能会减少到
my $fileContents = "path/to/data.txt".IO.slurp;
或
my $fileContents = slurp "path/to/data.txt";
为了以更细粒度的级别访问文件,Raku 自然提供了通过 `readchars` 函数检索指定数量的字符的功能,该函数接受要读取的字符数量,并返回表示获得数据的字符串。
my $fileName;
my $fileHandle;
my $charactersFromFile;
$fileName = "path/to/data.txt";
$fileHandle = open $fileName, :r;
# Read eight characters from the file into a string variable.
$charactersFromFile = $fileHandle.readchars(8);
# Perform some action with the "$charactersFromFile" variable.
$fileHandle.close;
- ↑ 广义化到与其他 IO 对象(如流、套接字等)交互的IO 句柄。
从 Perl 5 迁移
[编辑 | 编辑源代码]Inline::Perl5 是一个模块,用于执行 Perl 5 代码并在 Perl 6 中访问 Perl 5 模块。