1. 关于 Raku
Raku 以前被称为 Perl 6,因此你会在转换完全完成之前的相当一段时间内看到该名称。
1.1. Rakudo
Rakudo 是用 NQP(«Not Quite Perl»)编写的 Raku 的生产就绪型实现,在专用的 MoarVM("运行时的元模型")虚拟机上运行。
MoarVM 可用于 Windows,MacOS 和 Linux(以及某些其他操作系统)。
Rakudo 每月发布一次。(或者至少打算拥有它们。)
在 jvm 和 javascript(node.js)上运行的 Rakudo 的实现还不完整。
1.1.1. Rakudo Star
Rakudo Star 每三个月发布一次。 Rakudo 捆绑了文档(p6doc
命令,请参见第 1.8 节, "文档")和一些有用的模块(尤其是模块安装程序 zef
; 请参见第 12.2 节"使用 zef 进行模块管理”)。
这是安装 Rakudo 的最简单方法。
如果你正在运行 Linux 或 MacOS,则可以选择使用 Docker 和 Rakudo 镜像。 在决定之前参考附录1.Docker。
1.1.2. 安装 Rakudo Star
Rakudo Star: 浏览 https://rakudo.org/files
-
Windows 和 Mac:使用安装二进制文件。 参见 https://rakudo.org/files/
-
Linux:使用常规软件包系统(Debian, Centos, Fedora, openSUSE,Ubuntu 和 Alpine)。参见 https://nxadm.github.io/rakudo-pkg/
1.2. 在浏览器中运行 Raku
在这些网站中也可以运行 Raku 程序:
请注意,它们不支持模块,并且更改服务器状态(作为文件操作)的任何操作都必然会失败。 REPL 模式(请参阅下一节)可能是一个更好的选择,但这确实需要安装 Raku。
1.3. REPL
运行不带任何参数的 raku
以交互或 REPL
(读取-计算-打印循环)模式启动它。
$ raku
To exit type 'exit' or '^D'
> say 12; my $a = False;
12
> my $a = False;
False
请注意,REPL
模式始终显示一个值。 如果你的代码输出一个值,那很好。 但是,如果没有,REPL
将输出最后一个表达式求值的内容。
为了节省空间,有时会在代码的同一行显示输出:
> my $a = False; # -> False
1.3.1. 命令行补全
你可以键入部分命令,然后使用 <TAB> 展开它。 如果可能的命令不止一个,请再次使用 <TAB> 在这些命令之间循环。
使用向上和向下箭头键循环显示命令历史记录。
如果箭头键无法正常工作,则必须安装 Linenoise (适用于所有平台,并且包括 Rakudo Star)。
|
Linenoise 不包含在本机软件包(Linux)中。 但是好消息是你不需要 «sudo» 来安装模块。
练习 1.1 如果尚未安装 Rakudo Star(或 Docker 和一个(或两个) Docker 映像),请先安装它。
启动 REPL,并使用 $*PERL.compiler
(或 shell 中的 raku -v
)检查版本。
Linenoise
Rakudo Star 随附了 raku 模块 Linenoise。 它提供了 REPL 命令行历史记录,并且就像普通的 shell 一样,可以编辑命令。
历史记录将保存到文件中,并在下次启动 REPL 时加载。
如果没有它(如在 REPL 中使用箭头键所示),请手动安装。
$ zef install Linenoise
请注意,由于该模块具有二进制组件,因此你需要一个可运行的 C 编译器。 |
在基于 Debian 的系统(例如 Ubuntu)上,构建 Linenoise 所需的一切都在 «build-essential» 软件包中。
如果你没有 «zef»,请先安装它。 有关说明,请参见 https://github.com/ugexe/zef。 或考虑安装 Rakudo Star, 或使用 Docker。
rlwrap
在类似 Unix 的系统上,也可以使用 «rlwrap» 库。 可以像这样在基于 Debian 的系统上安装它:
sudo apt install rlwrap
zef
不是必须的。
1.4. 单行程序
我们可以使用命令行为 Raku 提供单行代码,并使用 «-e» 选项执行:
$ raku -e 'say e';
2.718281828459045
$ raku -e 'say "hello"'
hello
我用 say 在屏幕上显示数据。 它在末尾添加一个换行符。
|
我本可以使用 print
,但随后必须手动指定换行符。
有关详细信息,请参见第6.3节"输出"。
在 Windows 上,你必须交换引号:
$ raku -e "say 'hello'";
hello
有时,你可以摆脱 Windows 在 Unix 之类的系统上的引用。
$ raku -e 'say "Hello, World!"'
Hello, World!
但是,shell 逃逸字符将由 shell 解析:
$ raku -e "say 'Hello, World!'"
bash: !': event not found
1.4.1. 命令行选项
Raku 解释器支持几个命令行选项。 上一节介绍了 -e
。
运行 raku --help
以获得完整列表。
1.5. 运行程序
在本书的所有示例中,我都输入以下代码来运行程序:
$ raku program
代码文件的第一行是:
#! /usr/bin/env raku
在类似 Unix 的系统上,我们可以在不指定 raku
的情况下执行文件,因为系统将读取第一行并自动为我们启动 Raku -只要文件中设置了可执行标志:
$ ./program
练习 1.2
使用文本编辑器(如 emacs
, vi
或 Notepad
)制作一个名为 «hello-world»(在 Windows 上为 «helloworld.p6» )的文本文件,其内容如下:
.文件: hello-world
say "Hello, World!"
并执行它。
请注意,如果通过双击该程序(在 Windows 中)运行该程序,它将为输出打开一个终端窗口,打印到该窗口,然后在程序终止时再次关闭它。 在所有人都可以阅读文本之前。 因此,打开一个终端窗口,然后在其中键入命令。 |
1.6. 错误信息
Raku 尽力给出有用的错误消息。
-
使用礼貌的 «===SORRY!===» 给出编译时错误。
-
没有给出运行时错误。
对于一般用户而言,区别可能并不那么重要,但是在编译时捕获的错误是可取的,因为实际上尚未执行任何代码。
1.7. use v6
如果你尝试使用 Perl 5 运行 Raku 程序,则会收到一条错误消息,提示你语法错误。
$ perl hello-world
String found where operator expected at content/code/hello-world line 3, near "say "Hello, World!""
(Do you need to predeclare say?)
如果添加 use v6;
在文件中的一行中,Perl 5 将给出更好的错误消息。
使用此指令可以告诉 Perl 5 要求它没有的版本,因此它将失败-无需尝试解析并执行程序。
$ perl -e"use v6"
Perl v6.0.0 required--this is only v5.26.2, stopped at -e line 1. BEGIN failed--compilation aborted at -e line 1.
我在本课程介绍的程序中没有做到这一点。
也可以指定 Raku 的特定版本。 例如 use v6.c
,就像我在某些情况下在代码中所做的那样。 我这样做是为了向早期(预发行版; 6.a 和 6.b)的用户施加压力,以进行升级。 缺点是即使有较新的版本,该代码仍将使用 6.c。 (6.d 已于 2018 年 11 月发布。)
有关更多信息,请参见附录4.Raku 背景和历史记录。
1.8. 文档
Raku 的文档非常好,并且一直在扩展。 它可以在线(在 Web上)和离线(本地)上使用。
1.8.1. 在线文档
1.8.2. 本地文档
该文档可通过 p6doc
命令在本地获得。 «p6doc» 软件包与 Rakudo Star 捆绑在一起,但可以与 «zef» 一起手动安装。
p6doc
存在一些影响可用性的问题,因此你可能会发现联机文档更易于使用。
让我们尝试在不带参数的情况下运行它:
$ p6doc
You want to maintain the index?
To build an index for 'p6doc -f'
p6doc build
To list the index keys
p6doc list
To display module name(s) containing key
p6doc lookup
To show where the index file lives
p6doc path-to-index
What documentation do you want to read?
Examples: p6doc Str
p6doc Str.split p6doc faq
p6doc path/to/file
Set the POD_TO_TEXT_ANSI if you want to use ANSI escape sequences to enhance text
You can list some top level documents:
p6doc -l
You can also look up specific method/routine/sub definitions:
p6doc -f hyper
p6doc -f Array.push
You can bypass the pager and print straight to stdout:
p6doc -n Str
请注意,本地文档是安装时的快照,自那时以来,联机文档可能已发生了很大变化。
可以运行文档网站的本地副本,最简单的方法是使用 Docker。 |
像这样运行它,并转到 http://localhost:31415 来使用它:
docker run --rm -it -p 31415:3000 jjmerelo/perl6-doc
«--rm» 部分告诉 Docker 在停止容器后自动删除它。 你仍将拥有该映像,但是再次运行它可能需要一些时间,因为它将需要重新设置它。
有关更多信息,请参见 https://github.com/Raku/doc。
练习 1.3
运行命令 p6doc list
并注意条目(或关键字)的绝对大小。
首先是很多行,从 method
开始,然后是 routine
,最后是 sub
。 它们的含义如下:
method |
方法。在对象上调用的东西。 |
routine |
可以用作方法或子(例程)。 |
sub |
子例程, 函数或过程。 |
稍后,我们将在有一些基础知识的基础上返回文档。
1.9. 更多信息
-
优秀的《每周 Rakudo 新闻》博客提供了(几乎)与 Raku 有关的所有内容的每周摘要:https://rakudoweekly.blog/。 自 2003 年以来一直在进行。
-
在 irc.freenode.net 的
#raku
IRC 频道上询问有关 Raku 问题的问题。 有关更多信息,请参见 https://raku.org/community/irc。 -
有关 Raku(和 Perl 6)的书籍:https://perl6book.com/ 请注意,本网站上未提及有关 Perl 6 的较旧书籍。 避免在 2016 年之前出版的书籍,因为它们已经过时了。
1.10. 速度
Rakudo 通常比 Perl 5 慢,但比一年前快得多。
开发人员关注的重点是:"做对的事情, 然后变的更快"。
Raku 完全兼容 Unicode,因此比原先的速度慢。 == 变量、运算符、值和过程
1.11. 使用 say 和 print 输出
在执行其他操作之前,我们将讨论如何在屏幕上显示文本:
正常方式(使用其他语言)是 print
命令。 如果要换行,必须在末尾添加换行(\n
):
> print "a"; print "b";
ab>
>
是 REPL 提示符,在同一行上。
在 Raku 中,say
会自动在末尾附加一个换行符:
> say "a"; say "b";
a
b
根据要实现的目的同时使用两者:
> print "a"; say "b";
ab
我们将在第 6.1 节"换行"中介绍有关换行的详细信息。
1.12. 变量
变量是可以保存值的命名容器(也称为存储桶),并且可以随时更改该值。
1.12.1. 符号
变量类型由名称前的第一个字符 sigil 决定。 四种类型是:
符号 |
类型 |
描述 |
$ |
任何类型 |
任何东西 |
@ |
Array |
几个值 |
% |
Hash |
几个键值对儿 |
& |
Code |
可调用代码 |
标记是变量名称的一部分,因此 «$a» 可以与 «@a», «%a» 和 «&a» 共存,它们是独立的变量。
Anything($)
可以像我们一开始那样保留一个值。 但是它也可以用来存放任何东西。
在 Perl 5 中,此类型称为 Scalar,你可能还会看到用于 Raku 的名称。
Arrays(@)
数组是具有一个或多个值的排序列表。
有关详细信息,请参见第8章,数组和列表。
Hashes (%)
散列是键和值对的未排序集合。 使用键查找值。
有关详细信息,请参见第9章,配对和哈希;有关更多详细信息,请参见"高级 Raku"课程。
Code (&)
&
表示我们获得了对代码的引用(通常是过程名称),而不是立即执行。
如果你曾经有过 Perl 5 的经验,那么应该熟悉前三个信号,并且这是该语言的内置类型的限制。 |
Raku 还有其他几种类型,我们稍后再介绍。 如果我们将这些类型之一的内容分配给哈希或数组,则会更改类型。 将其分配给标量以保留类型,因为标量可以包含任何内容。
我们将遇到的第一个示例(请参见第8章,数组和列表)是数组和列表之间的区别。
实际上,我们可以删除其他信号,并对所有内容使用标量 $
。 但这对可读性并没有帮助,因此不建议使用。
1.12.2. Twigils
符号后面可以加上一个 twigil("微调符号"的东西)。 最常用的是:
Twigil |
描述 |
|
属性(类成员) |
|
动态变量 |
|
方法(不是真正的变量) |
|
自我声明的正式命名参数 |
|
编译时变量 |
|
自我声明的正式位置参数,也称为占位符变量。 有关详细信息, 请参见 8.13.1,"占位符变量"。 |
有关完整列表,请参见 https://docs.raku.org/language/variables#Twigils。
我们将在适当时候进行调查。
1.12.3. my
必须先声明变量(使用 my
),然后才能使用它们。 否则我们会得到一个编译时错误:
===SORRY!=== Error while compiling:
Variable '$s' is not declared
------> <BOL>⏏$
弹出符号(⏏)显示编译器认为问题出在哪里。 实际输出取决于终端的功能。
> my $r
(Any)
(Any)
表示该变量没有值。 (或者说,它可以保留任何值。)我们将在第3章,类型系统中对此进行说明。
my
定义了一个词法范围的变量,这意味着该变量仅在当前块中可用,从其定义位置到第一个右花括号(}
):
{
my $a = 12;
say $a; # -> 12
}
say $a; # -> Variable '$a' is not declared
我们可以同时声明几个变量:
> my ($a, @b, %c);
my 和开口圆括号之间的空间至关重要。 如果你忘记了它,它将被视为对带有三个参数的过程"my"(希望不存在)的调用。
|
1.13. 注释
以 #
号开始注释。 该行的其余部分将被编译器忽略。
> say "12"; # This is a comment
12
1.13.1. 多行注释
多行和嵌入式注释以井号(#
),反引号(`)和方括号开头的字符(例如 (
, {
和 [
) 或字符组(例如 ((
或 {[
)。 直到匹配的一个包围的方括号字符为止。
say "Hello";
#`[ This is a comment.
The compiler will ignore it.
But you, the reader, cannot ignore it ]
say "Good bye";
可以使用递归方括号(注释内的注释),但可能不太有用: |
> say 14 #`{ a { b } c }, 12;
1412
注释一直持续到字符串的最后。
1.13.2. 嵌入式注释
我们也可以将多行语法用于内联注释(单行):
> say #`( Yeah, right. Why bother commenting the code? ) "Whatever...";
Whatever...
> say 14; #`({ hidden-comment })
14
1.14. 非破坏性运算符
几乎每个运算符和函数都将返回新的修改后的值,而原始值则保持不变/不变。
my $a = 10; my $b = 20;
my $a = $a + $b;
my $string = "abcabc" ~ "ABC";
我们可以同时分配新值:
$a += $b;
$string ~= "ABC";
为什么这不是默认设置? 好。 表达式 2+2
是有效的,但是你不能将总和(4
)分配回第一个值(2
)。
这也适用于方法,请参见第3.3节"一切都是对象"。 |
1.15. 数值运算符
操作符使用一个或多个值或变量执行某项操作。
当然,我们有通常的数学方法:
Numerical |
整数 |
描述 |
+ |
加法 |
|
- |
减法 |
|
++ |
递增 1 |
|
— |
递减 1 |
|
* |
乘法 |
|
/ |
div |
除法 |
1.15.1. +(加法)
使用 +
(加法运算符)将两个数字(和/或变量)相加:
> 2 + 2; # -> 4
1.15.2. -(减法)
使用 -
(减法运算符)从另一个减去一个数字(和/或变量):
> my $a = 3; my $b = 2;
> $a - $b; # -> 1
1.15.3. ++(自增)
使用 ++
将变量加1。如果该值尚未为数字,则将其强制为数字。
可以用作前缀和后缀运算符:
my $i = 10; say "{ $i++ } $i"; # -> 10 11
$i = 10; say "{ ++$i } $i"; # -> 11 11
请注意,组合前缀和后缀版本是非法的。 因此 --$a++
(和变体)将失败。
1.15.4. --(自减)
使用 --
将变量减1。如果该值尚未为数字,则将其强制为数字。
它可以用作前缀和后缀运算符。
my $i = 10; say "{ $i-- } $i"; # -> 10 9
$i = 10; say "{ --$i } $i"; # -> 9 9
请注意,组合前缀和后缀版本是非法的。 因此,++$a--
(和变体)将失败。
1.15.5. *(乘法)
使用 *
(乘法运算符)将两个数字(和/或变量)相乘:
> 2 * 7; # -> 14
1.15.6. /(除法)
使用 /
(除法运算符)将一个数字(和 /
或变量)除以另一个:
> 8 / 4; # -> 2
1.15.7. div
整数除法运算符 div
是 /
的变体,如果两个值都是整数,则可以使用 /
:
> 8 div 4; # -> 2
如果一个或两个值都是其他值,它将失败。
1.16. 运算符优先级
从最高(也称为最严格)优先级到最低优先级:
运算符 |
描述 |
|
圆括号 |
|
幂 |
|
乘法和除法 |
|
加法和减法 |
当在代码中遇到具有相同优先级的运算符时,它们将从左到右执行。
有关运算符优先级规则的完整列表,请参见 https://docs.raku.org/language/operators。
如有疑问,请使用括号。 (是的,我说真的。) |
练习 2.1
这个表达式的结果是?
> say 12 + 10 * 4;
1.16.1. =
(赋值)
使用 =
(赋值运算符)为变量赋值。
> my $s = 5; # -> 5
> $s = 10; # -> 10
这实际上是 Raku 中的运算符,尽管不是数学意义上的。
1.16.2. :=(绑定)
使用 :=
运算符将变量设置为指向右侧的东西。 正常赋值:
my $a = 42; # (1)
my $b = $a; # (1)
$a += 10; # (2)
say "$a $b";
这会打印出期望的 52 42
。
如果右边的东西是一个变量,那么我们在同一个容器中有一个别名:
my $a = 42; # (1)
my $b := $a; # (1)
$a += 10; # (2)
say "$a $b";
这会打印出 52 52
。
如果右边的东西是表达式(而不是变量),则将对其求值并使用该值:
my $a = 42; # (1)
my $b := $a + 0; # (1)
$a += 10; # (2)
say "$a $b";
这会打印出 42 52
。
表达式 my $b := $a + 0; 将变量绑定到值 42 (无容器)。 任何此类(无容器)值都是常量,并且无法更改:
|
> $b++; # -> Error
> 12++ # -> Just as this is an error
更改值的唯一方法是将其绑定到另一个:
$b := 4;
普通赋值将尝试更改该值,在这种情况下为 4,该值将不起作用:
> $b = 14;
Cannot assign to an immutable value
绑定也适用于数组和哈希。
1.17. 值
值可以是字符串,数字,也可以是更复杂的对象,我们将在第17章,类中看到。
1.17.1. 字符串
字符串用引号指定; 单引号 ,双引号或其他 Unicode 必须提供的内容:
> my $name = "Arne"; # -> Arne
> my $hello = "Hello, $name"; # -> Hello, Arne
> my $hello = 'Hello, $name'; # -> Hello, $name
除非使用单引号,否则将对变量进行插值。
所谓的«法语引号»(« 和 »)插值变量,但是字符串被分解为一个以空格作为分隔符的数组。 |
> «Hello, $name».raku; # -> ("Hello", "Arne")
这也适用于 ASCII 等效的 <<
和 >>
。
> <<Hello, $name>>.raku; # -> ("Hello,", "Arne")
单括号版本(带有 <
和 >
)用作单引号,因为不对变量进行插值。
> <Hello, $name>.raku; # -> ("Hello,", "\$name")
raku
方法显示 Raku 如何在内部存储值,并有助于显示正在发生的情况。 有关更多信息,请参见第 6.2.3 节"raku(perl)"。
请注意,如果不使用索引,则不会内插字符串中的数组和哈希。 我们可以添加一个空索引来使其工作:
> my @a = 1,2,3; say "@a"; # -> @a
> my @a = 1,2,3; say "@a[]"; # -> 1 2 3
> my %a; say "%a{}"; # -> %a
> my %a; %a<a> = 12; say "%a{}"; # -> a 12
或者我们可以将变量放置在花括号中以确保插值:
> my $hello = "Hello, { $name }";
这对于表达式很有用:
> say "I am almost { $age + 1 } years old.";
有关更多信息,请参见第7章,字符串;有关其他引用结构,请参见第7.12节"引用"。
~(字符串连接)
使用 ~
将两个字符串粘合在一起:
> my $t = "abc" ~ "def";
abcdef
1.17.2. 数字
如果它没有引号,并且看起来像一个数字,则可以是一个数字:
12 # Integer
12.8 # A number
1.12e+20 # Floating point
2+4i # A Complex number
或一个错误:
12A
1.12e
2+4j
复数将在《高级 Raku》课程中介绍。 |
1.18. 变量名
变量名(以及任何其他名称)中的第一个字符(在标记和可选的 twigil 之后);例如 过程,类)必须是字母(如 Unicode 所决定的是字母)或“开始”下划线(_
)。
其余的可以是字母,下划线(_
),减号(-
),单引号(')和数字。
减号(-
)或单引号(')后必须是字母或下划线(_
),最后一个字符必须是字母,下划线或数字。
一些例子:
my $r1234; # OK
my $r1234-56; # ERROR - parsed as "$r1234 - 56"
my $r1234_56; # OK
my $r1234-5A; # ERROR - as "5A" is not a number
my $r1234'5A; # ERROR - as "5A" is not a number
my $Große; # OK
my $ßßßßßß; # OK
my $______; # OK
my $; # OK (doesn't work in print; two Chinese letters)
my $_; # ERROR, as we cannot redeclare this one.
我们甚至可以使用难以打印的字符。
my $㑄㒔; # OK - two chinese letters
建议常识,尤其是在进入 Unicode 丛林之前。
练习 2.2
以下哪个变量名称是合法的?
my $don't-do-it;
my $dog;
my $dog2;
my $dog-3;
使用 REPL
来检测你是不是对的。
1.19. 常量
不要将变量用于应保持恒定的值:
> constant $pi = 3.14;
你不能更改常量值:
> constant $pi = 3.14;
> $pi = 3;
Cannot assign to an immutable value in block <unit> at <unknown file> line 1
现在,仅将“不变”视为“只读”的花哨词。 我们将在“高级 Raku”课程中再次介绍它。 |
1.20. 无符号变量
你可以删除符号,如果这样会使你感觉更好:
> constant pi = 3.14;
> say pi * 5;
我们可以使用绑定,但是与赋值相比,它不添加任何内容(也许除外):
> constant pi := 3.14; # Use assignment instead
1.20.1. pi
Raku 有一个内置的 pi
值:
> say pi; # -> 3.141592653589793
你可以重新定义它-无需任何警告。 但是请不要。
1.20.2. 仍旧是常量?
如果你认为 constant
输入太多,请在声明常量时使用反斜杠:
> my \z = 2; # -> 2
> say z + 100; # -> 102
> z= 2
Cannot modify an immutable Int (1) in block <unit> at <unknown file> line 1
变量 z
实际上是常量,因此,无符号变量根本不是变量。
1.21. True 和 False
Raku 内置了布尔值 True
和 False
:
> my $a = False; # -> False
> my $b = True; # -> True
在数字上下文中,False
的值为 0,True
的值为 1。这意味着我们可以执行以下操作,尽管这是不明智的:
> True + False; # -> 1
> True + True; # -> 2
> True * 12; # -> 12
1.21.1. so / ? / Bool
如果我们在布尔上下文中评估非布尔值,则如果未定义,则将为 False
,否则将为空字符串或数字 0。其他所有结果均将为 True
。
so
关键字(或 ?
前缀)强制在布尔上下文中对表达式求值:
> "False".Bool; # -> True
> so "False"; # -> True
> ? "False" # -> True ## The space is optional
> "False".so # -> True
1.21.2. 布尔运算符
布尔运算符成对出现,一个优先级高,另一个优先级低。
高优先级 |
低优先级 |
描述 |
|
|
否定 |
|
|
都 |
` |
` |
|
|
之一或都 |
|
!/not
!
和 not
运算符可用于否定布尔值:
> ! True; # -> False
> ! False; # -> True
当对非布尔值使用时,该值在取反之前将转换为布尔值:
> ! 10; # -> False
> ! 0; # -> True
> ! "ABC"; # -> False
> ! ""; # -> True
代替 |
> if ! $value == 15
使用取反的运算符 !=
(我们将在第3.7节"比较运算符"中进行解释):
> if $value != 15
或否定测试:
> unless $value == 15
小心优先级:
> not 1 - 1; # -> not (1 - 1) -> not 0
True
> ! 1 - 1; # -> (!1) - 1 -> False - 1 -> 0 - 1
-1
&& / and
如果所有参数的计算结果均为 True
,则返回 True
-ish值,否则返回 False
-ish 值。
> 1 and 6; # -> 6
> True and 0; # -> 0
> True and False; # -> False
返回值是第一个参数的结果为 False
或最后一个参数(结果为 True
)。
此操作会短路,因此编译器在遇到第一个 False 值后将跳过给出的所有表达式:
|
> my $a = 1; $a++ and $a++ and $a++ and $a++; # -> 4
> my $b = 0; $b++ and $b++ and $b++ and $b++; # -> 0
|| / or
如果至少一个参数的计算结果为 True
,则返回 True
:
> True || False; # -> True
> True || True; # -> True
> False || False; # -> False
xor / ^^
如果正好其中一个参数的计算结果为 True
,则返回 True
:
> True xor False; # -> True
> False xor False; # -> False
如果其中多个计算为 True
,则返回 Nil
:
> True xor True; # -> Nil
> True xor True xor False; # -> Nil
> False xor False xor False xor True; # -> True
> False ^^ False ^^ False ^^ True; # -> True
练习 2.3
解释为什么我们在使用 !
和 not
时会得到不同的结果:
> my $value = 1;
> ! $value == 15; # -> False
> not $value == 15; # -> True
1.22. //
or
和 ||
的问题是运算符是他们不区分零值和未定义的值:
> my $age = 0; # Age in years
> say $age || "unknown"; # -> unknown
我们可以使用 «Defined-or» 运算符 //
来解决此问题。 它返回定义的第一个操作数,如果未定义则返回最后一个:
> my $age = 0; # Age in years
> say $age // "unknown"; # -> 0
> my $price-pound;
> my $price-dollar = 5;
> my $price-yen = 2;
> say "The price is: { $price-pound // $price-dollar // $price-yen // "unknown" }."; The price is: 5.
(我们应该告诉我们以哪种货币给出了价格,但没关系。)
1.22.1. ()(分组运算符)
使用括号将表达式分组在一起。 它们具有比其他任何事物更高的优先级:
> 1 + 2 * 3 + 4; # -> 1 + ( 2 * 3 ) + 4; # -> 11
> (1 + 2) * (3 + 4) # -> 3 * 7; # -> 21
请注意,分组运算符不会列出。 我们可以使用逗号(称为列表运算符;请参见第8.1节"列表运算符")来创建列表。 == 类型系统
Raku 具有复杂的(如复杂的)类型系统,我们可以选择积极使用它(称为"强类型")- 或忽略它。
即使我们忽略它们,这些类型仍然存在,并且可能引起意外。 通常建议使用类型系统。
不使用类型:
> my $a = 12;
> $a = "hello, world!";
1.23. 强类型
使用强类型:
> my Int $a = 12; # -> 12
> $a = "hello, world!";
Type check failed in assignment to $a; expected Int but got Str ("Hello, world!")
in block <unit> at <unknown file> line 1
强类型可以防止编程错误。
当变量不包含值时,REPL 将报告类型。 Any 是最通用的类型,因为它可以表示任何东西。
|
我们可以对数组和哈希进行相同的操作:
> my Int @a;
> my Str %h;
你可以根据需要使用类型系统:
> my Int $a = 12; # -> 12
> my $b = $a + 1; # -> 13
> $b = "Hi!"; # -> Hi!
1.23.1. of(作为关键字)
我们也可以使用 of
将类型约束添加到变量:
> my $i of Int = 42;
> my Int $i = 42; # The same
> my Int @a;
> my @a of Int;
> my Int %h;
> my %h of Int;
1.23.2. ^name
^name
方法可用于告诉我们值或变量的类型:
> 12.^name; # -> Int
> "12".^name; # -> Str
> my $i = 12; say $i.^name; # -> Int
> $i = "AB"; say $i.^name; # -> Str
> True.WHAT; # -> Bool
还有一个 WHAT 方法(和过程)给出类似的结果:
|
> 12.^name; # -> Int # A scalar value
> 12.WHAT; # -> (Int) # A one-element list
但是建议使用 ^name
。
^name
(和 WHAT
)方法显示实现细节,你不应根据他们编写代码。
一个明显的原因是类和继承(将在第17章,类中讨论),可以更改类名。
另一个(不是很明显)的原因是优化程序,它可以选择更改类型。
1.23.3. of(作为方法)
我们可以使用 of
作为一种方法来显示数组和散列的值或变量的类型:
> my Int %hash; say %hash.of; # -> (Int) > my Str %hash; say %hash.of; # -> (Str) > my @a; say @a.of; # -> (Mu) > my Int @a; say @a.of; # -> (Int)
of 方法不适用于标量,在这里我们必须使用 .VAR 来获取标量对象:
|
> my Numeric $a; say $a.^name; # -> Numeric
> $a = 3; say $a.^name; # -> Int
> $a.VAR.of; # -> (Numeric)
> say $a.VAR.of.^name; # -> Numeric
这表明我们可以区分对象的类型(在这种情况下为 Numeric
)和实际值的类型(在这种情况下为整型)。 不过,它可能没那么有用。
1.24. ^mro
(方法解析顺序)
^mro
(《方法解析顺序》)方法以继承(和优先级)顺序列出对象或值所属的类型(或类)的列表:
> say 12.^mro; # -> ((Int) (Cool) (Any) (Mu))
这告诉我们数字 12 是 Int
类型,Int
从 Cool
继承,Cool
从 Any
继承,最后 Any
从 Mu
继承。
这意味着如果我们在 Int
上应用 say
,则调度程序(或方法解析器)以 Int
开头并检查那里是否存在 say
方法。 如果没有,它将沿着继承图继续,直到找到它或放弃为止。
从实现的角度来看,这很有用。 我们从基类(或父类)继承方法,并仅在需要时提供自定义版本。
^mro 中的初始插入符号告诉你此方法提供了特定于实现的详细信息。 来自此类调用的信息可能会在 Raku 的更高版本中更改,而不会事先发出警告。
|
有关类型系统的更多信息,请参见 https://docs.raku.org/type.html。
1.24.1. Int 继承树
^mro
也可以用于类型对象。 这里我们有 Int
:
> say Int.^mro; # -> ((Int) (Cool) (Any) (Mu))
类型 |
描述 |
|
类型系统的根 |
|
Thing/object |
Cool |
既可以当作字符串也可以当作数字的对象(«便捷OO循环»的缩写) |
Int |
整型 |
![img]()
使用的颜色为:黑色(类型),绿色(枚举;我们将在《高级 Raku》课程中再讲到它),蓝色(角色,请参见17.14,"角色")。 类型从它具有指针的东西继承。
有关 Str
继承树,请参见第6.6.2.1节"Str 继承树"。
1.24.2. 其它类型
注意我们不能在角色上使用 ^mro
:
> Real.^mro
No such method 'mro' for invocant of type 'Perl6::Metamodel::ParametricRoleGroupHOW'
in block <unit> at <unknown file> line 1
> Numeric.^mro
No such method 'mro' for ...
1.25. 一切皆对象
如果你想要的话。
大多数内置函数都有相应的方法:
> say $a;
> $a.say;
> say "Hello";
> "Hello".say;
请记住第一章中的下表:
method |
方法。在对象上调用的东西。 |
routine |
可以用作方法或子(例程)。 |
sub |
子例程, 函数或过程。 |
如果你想知道某个关键字是否可以用作方法,子例程或同时用作两者,请使用 p6doc list
进行查找。 例如 say
:
$ p6doc list | grep say method say sub say
这表明文档不 100% 一致。 我们应该在这里的"子例程 say"上受到打击。
大多数方法(以及运算符,请参见第2.4节"非破坏性运算符")保留原样调用的值,并返回修改后的版本。 |
我们可以使用以下简短形式进行赋值:
$val = $val.something; # This method doesn't exist.
$val .= something;
1.26. 特殊值
在本节中,我们将查看特殊值 Nil
,Any
,Inf
和 NaN
。
1.26.1. Nil & Any
Nil
是空值(不存在值)。
将其赋值给变量以将其重置为其默认(未定义)值:
> my $b = "b"; $b = Nil; # -> (Any)
> my Int $i = 4; $i = Nil; # -> (Int)
可以使用类型代替 Nil
:
> my Int $i = 4; $i = Int; # -> (Int)
如果你使用类型(强输入),则 Any
无效:
> my $a = Any; # -> (Any)
> my Int $i = Any;
Type check failed in assignment to $i; expected Int but got Any (Any)
> my Int $i = Nil; # -> (Int)
> my Int $i = Int; # -> (Int)
尝试输出未定义的值时要小心:
> my $a = Any; say $a; # -> (Any)
> my $a = Any; say ": $a";
Use of uninitialized value of type Any in string context.
详细信息, 请参见 "6.3.1节的 say"。
1.26.2. Infinity
Inf
是无穷大。 我们也可以使用 Unicode 无限符号 ∞
。
无穷大是一个我们无法表达的值,将永远遥不可及:
> say 100000000000000000000000000000000000000000000000000000 > Inf
False
你可以否定它:
> say -100000000000000000000000000000000000000000000000000000 < -Inf
False
不要将 Inf 视为数字。 它对于比较很有用,但是对其进行算术运算几乎没有用:
|
> Inf + 1; # -> Inf
> -Inf - 1; # -> -Inf
> -Inf + Inf; # -> NaN
> Inf * 0; # -> NaN
第一个证明 1 == 0
。期望它不是,因为 Inf
不是数字,因此不能在表达式中使用。
1.26.3. NaN(Not a Number)
查看 5.10 章节, "NaN(Not a Number)"
练习 3.1
我们可以在一个 Int
中存储的最大数量是多少?
使用 REPL
。
1.27. :D(定义副词)
很显然,类型化变量接受指定类型的值。 但它也将接受默认值 Nil
。 对于过程参数,通常不是一个好主意(有关详细信息,请参见第10章,过程),我们尚未对其进行讨论。
我们可以使用以下类型的 :D
(表示«Defined»)副词来解决这个问题:
> my Int:D $a;
===SORRY!=== Error while compiling:
Variable definition of type Int:D requires an initializer at line 2
> my Int:D $i = Nil
Type check failed in assignment to $i; expected type Int:D cannot be itself...
> my Int:D $i = Int
Type check failed in assignment to $i; expected Int:D but got Int (Int) ...
> my Int:D $i = Any
Type check failed in assignment to $i; expected Int:D but got Any (Any) ...
没有 :D
副词,上面的代码将起作用。
1.27.1. :U(Undefined Adverb)
也可以在类型上带有 :U
(即 «Undefined»)副词来禁止变量中的值:
my Int:U $i;
> $a = Nil
(Int:U)
> $a = Int
(Int)
$a = 1
Type check failed in assignment to $a; expected Int:U but got Int (1)
in block <unit> at <unknown file> line 1
这是没有意义的,但是可以在过程的参数上使用一连串的内容,在这里我们使用类型而不是实际值。 但是我真的不推荐。
1.27.2. defined
使用 defined
检查是否定义了一个值(有一个值):
> say Int.defined; # -> False
> say 12.defined; # -> True
> my $a; say $a.defined; # -> False
> my $a = 1; say $a.defined; # -> True
请注意,还有一个 DEFINITE
方法可以提供几乎相同的结果。 我们将在“高级 Raku”课程中再次介绍它。
1.28. 类型转换
Raku 具有自动和手动类型强制(也称为转换)。
1.28.1. 自动类型转换
如果可能,Raku 将自动将值转换为所需的类型。 但前提是我们没有使用强类型:
> my $string1 = "12"; my $string2 = "13";
> my $sum1 = $string1 + $string2; # Addition
25
> $sum1.^name;
Int
> my $sum2 = $string1 ~ $string2; # String concatenation
1213
> $sum2.^name;
Str
> my Int $a = 12; my Int $b = 13;
> my $c = $a ~ $b;
1213
> my Int $d = $a ~ $b;
Type check failed in assignment to $d; expected Int but got Str ("1213")
in block <unit> at <unknown file> line 1
1.28.2. 手动类型转换
当我们想要(并拥有)对类型的完全控制权时,手动类型强制与强类型结合很有用:
转换为 |
方法 |
前缀 |
关键字 |
函数 |
Numeric |
.Numeric |
+ |
Numeric() |
|
String |
.Str |
~ |
Str() |
|
Boolean |
.so 或 .Bool |
? |
so |
Bool() |
还有一个 ?^
前缀运算符,可强制转换为布尔值并取反,而 ?^
(相同),?|
和 ?&
中缀运算符,它们执行逻辑 XOR,OR 和 AND 运算。
这些值必须可转换才能正常工作。
Numeric / +
Numeric
或 +
前缀将转换为给定值的最佳数字类型:
> "12".Numeric.^name # -> Int # Integer
> "12.1".Numeric.^name # -> Rat # Rational number
> "5e+10".Numeric.^name # -> Num # Floating point
> +("12").^name # -> Int
> +("12.1").^name # -> Rat
> "abc".Numeric
Cannot convert string to number: ...
如果可以确定我们想要的是什么,我们可以指定实际的类型:
> "12".Int.^name # -> Int # Integer
> "12".Rat.^name # -> Rat # Rational number
> "12".Num.^name # -> Num # Floating point
> 12.1.Str.^name # -> Str # String; "12.1"
> ~(12.1).^name # -> Str # String; "12.1" # Prefix ~
但是你将完全得到你想要的:
> "12.1".Int; # -> 12
Str / ~
把数字转换为字符串:
> 12.Str;
> ~12;
有关布尔型示例,请参见第2.11节"True 和 False"。
还有其他将值分类的方法。 有关详细信息,请参见6.3, "输出"。 |
1.28.3. 使用 try
阻止运行时错误
Raku 无法执行某些操作时,将发生运行时错误。 例如:
> "AS".Int
Cannot convert string to number: ...
我们可以通过在表达式前面加上 try
来防止程序终止:
> try "AS".Int
Nil
现在,程序有责任处理错误情况。 一些可能性:
> my $text = "AS";
> try $possibly-an-int.Int;
Nil
> try $possibly-an-int.Int // 0;
0
小心零值:
> my $zero = 0;
> try $zero.Int;
0
将 try
应用于非故障时,它将使该值保持不变。 因此返回零,如果在测试中使用它,我们将遇到问题。 例如。 这样,即使有整数,也不会执行 if
块。
my $zero = 0;
if try $zero.Int {
...
}
我们可以使用 .defined
方法来避免此问题:
> my $zero = 0;
> say "OK" if try $zero.Int.defined; # -> OK
try
和 $!
将在"高级 Raku"课程中详细介绍。
1.29. 比较运算符
我们有常用的数字比较运算符及其字符串版本。 以及许多其他的:
Numeric |
Strings |
Smart |
Other |
What |
== |
eq |
Equal |
||
< |
lt |
before |
Less than |
|
⇐ |
le |
小于等于 |
||
> |
gt |
after |
大于 |
|
>= |
ge |
大于等于 |
||
!= |
ne |
不等于 |
||
<⇒ |
leg |
cmp |
三向比较 |
|
=:= |
容器相等; 查看 3.7.6 节, "=:=" |
|||
=== |
值相等; 查看 3.7.7 节, "===" |
|||
=~= |
近似相等; 查看 5.11 节, "=~=" |
请注意,字符串比较区分大小写。 它无法识别字母,但会比较 Unicode 值。 因此,大写字母位于小写字母版本之前。
它们大多数都是直截了当的,但是我们将在以下各节中讨论最后一行(“三向比较”运算符)和“智能”列。
1.29.1. cmp
cmp
(如《比较》中所述)是多功能的三向比较运算符。
它比较具有字符串语义的字符串,具有数字语义的数字,首先按键比较,然后按值比较等。
> say (a => 3) cmp (a => 4); # -> Less
> say 4 cmp 4.0; # -> Same
> say 'b' cmp 'a'; # -> More
在数字上下文中,返回值为 -1
(Less),0
(Same)和 1
(More)。
unicmp
有一个 cmp
的 unicmp
版本,它忽略了字符的大小写:
> "a" unicmp "B"; # -> Less
> "A" unicmp "b"; # -> Less
当我们比较同一字母的小写和大写版本时,它并没有达到预期的效果。 小写版本被认为小于大写版本:
> "A" unicmp "a"; # -> More
> "A" unicmp "A"; # -> Same
> "a" unicmp "A"; # -> Less
但这意味着我们可以实际使用它进行排序,并获得可预测的结果。
unicmp 的排序规则感知版本称为 coll 。 它将在《高级 Raku》课程中介绍。
|
1.29.2. leg
leg
(如 «Less, Equal or Greater» 中所述)是 cmp
的仅字符串版本(请参见第3.7.1节"cmp")。
比较之前,非字符串值将转换为字符串。
> say 'a' leg 'b'; # -> Less
> say 'a' leg 'a'; # -> Same
> say 'b' leg 'a'; # -> More
在数字上下文中,返回值为 -1
(Less), 0
(Same) 和 1
(More)。
比较 cmp 和 leg :
|
> 11 leg 2; # -> Less
> 11 cmp 2; # -> More
1.29.3. <⇒
<⇒
是 cmp
的数字版本(请参见第3.7.1节"cmp")。
> say 1 <=> 2; # -> Less
> say 2 <=> 2.0; # -> Same
> say 2 <=> 1; # -> More
非数字值会在比较之前转换为数字,如果无法进行转换,则会出现错误:
> 2 <=> "sj"
Cannot convert string to number: ...
1.29.4. before
before
运算符的行为类似于 cmp
(请参见第 3.7.1 节"cmp"),不同之处在于,如果第一个参数位于(小于)第二个参数之前,则返回 True
。
> 1 before 1; # -> False
> 1 before 2; # -> True
> 111 before 21; # -> False
> "111" before "21"; # -> True
1.29.5. after
after
运算符的行为与 cmp
相同(请参见第3.7.1节"cmp"),但如果第一个参数在第二个参数之后(大于),则返回 True
。
> "ab" after "aaaa"; # -> True
练习 3.2
解释为什么我们从第一个得到 False
,而从第二个得到 True
:
> 111 before 21; # -> False
> "111" before "21"; # -> True
1.29.6. =:=
使用容器标识运算符 =:=
来确定两个参数是否都绑定到同一容器。 如果返回 True
,则通常意味着修改一个也会修改另一个。
> my $a = 42;
> my $b = $a;
> $a =:= $b; # -> False
在这里,我们对 2.6.2 节"=:=(Binding)"中的前两个代码块应用 =:=
,以表明它可以识别绑定:
> my $a = 42;
> my $b := $a;
> $a =:= $b; # -> True
1.29.7. ===
使用值标识运算符 ===
来检查两个参数是否是相同的对象或值,而无需考虑任何容器化:
> my $a = 3; # -> 3
> my $b := 3; # -> 3
> $a === $b; # -> True
> 1 === 1.0; # -> False
在值上使用(如此处所做)时,===
的行为与 eqv
相同。
有关在对象上使用 ===
的描述,请参见第17.9.2节"==="。 我们可以使用类型对象来检查变量或值的类型:
> my $a = 12; $a.WHAT === Int; # -> True
这不适用于 ^name
:
> my $a = 12; $a.^name === Int; # -> False
1.29.8. isa
isa
方法更好,因为它避免了 WHAT
:
> my $a = 12; $a.isa(Int); # -> True
我们还可以使用智能匹配来检查类型。 请参见第9.16.2节"使用智能匹配"。 |
1.29.9. but(True 和 False, but …)
我们可以使用 but
关键字更改将非布尔值转换为布尔值的方式:
> my $a = "Hi" but False; # -> Hi
> say $a; # -> Hi
> say so $a; # -> False
你可以像 «yes, but…» 一样阅读它
but 子句是当前值(而不是变量)的一部分,如果我们更改值,它将丢失:
|
> my $c = 156 but False; # -> 156
> say so $c; # -> False
> $c++; # -> 157
> say so $c; # -> True
可能(但不明智)在 but
关键字之后将其与非布尔值一起使用。 这应该是所有可能的组合(基本类型为string/number/Boolean):
my $x = |
$x.Str |
$x.Int |
$x.Bool |
+$x |
$x + 0 |
10 but 0 |
10 |
0 |
True |
10 |
10 |
10 but 'ten' |
ten |
ten |
True |
ten |
10 |
10 but False |
10 |
10 |
False |
10 |
10 |
10 but True |
10 |
10 |
True |
10 |
10 |
0 but 10 |
0 |
10 |
False |
0 |
0 |
0 but 'ten' |
ten |
ten |
False |
ten |
0 |
0 but False |
0 |
0 |
False |
0 |
0 |
0 but True |
0 |
0 |
True |
0 |
0 |
'ten' but 0 |
ten |
0 |
True |
Error |
Error |
'ten' but 10 |
ten |
10 |
True |
Error |
Error |
'ten' but False |
ten |
Error |
False |
Error |
Error |
'ten' but True |
ten |
Error |
True |
Error |
Error |
True but 0 |
True |
0 |
True |
1 |
1 |
True but 10 |
True |
10 |
True |
1 |
1 |
True but 'ten' |
ten |
1 |
True |
1 |
1 |
True but False |
False |
0 |
False |
0 |
1 |
False but 0 |
False |
0 |
False |
0 |
0 |
False but 10 |
False |
10 |
False |
0 |
0 |
False but 'ten' |
ten |
0 |
False |
0 |
0 |
False but True |
True |
1 |
True |
1 |
0 |
Error 表示运行时错误(和程序终止)。
表格中的某些值没有太大意义。 但是问题是输入。 只要我们按预期使用它,然后在 but 后面的布尔值后面加上一个布尔值,那么它就会按预期工作。
|
该程序用于制作此表,并尝试防止由于错误而终止程序:
say "\$x = |\$x.Str |\$x.Int |\$x.Bool|+\$x |\$x+0";
say "----------------+-------+-------+-------+-------+----";
print-it("10 but 0", 10 but 0);
print-it("10 but 'ten'", 10 but 'ten');
print-it("10 but False", 10 but False);
print-it("10 but True", 10 but True);
print-it("0 but 10", 0 but 10);
print-it("0 but 'ten'", 0 but 'ten');
print-it("0 but False", 0 but False);
print-it("0 but True", 0 but True);
print-it("'ten' but 0", 'ten' but 0);
print-it("'ten' but 10", 'ten' but 10);
print-it("'ten' but False", 'ten' but False);
print-it("'ten' but True", 'ten' but True);
print-it("True but 0", True but 0);
print-it("True but 10", True but 10);
print-it("True but 'ten'", True but 'ten');
print-it("True but False", True but False);
print-it("False but 0", False but 0);
print-it("False but 10", False but 10);
print-it("False but 'ten'", False but 'ten');
print-it("False but True", False but True);
sub print-it ($label, $expression) {
print $label, "\t|";
print trap-it($expression.Str), "\t|";
print trap-it($expression.Int), "\t|";
print trap-it($expression.Bool), "\t|";
print trap-it(+$expression), "\t|";
print trap-zero($expression);
say "";
}
sub trap-it ($expression) {
my $result;
try { $result = $expression.gist; }
return $!
?? "ERR"
!! $result;
}
sub trap-zero ($expression) {
my $result;
try { $result = ($expression + 0).gist; }
return $!
?? "ERR"
!! $result;
}
错误变量 $! 包含错误对象(如果我们包装在 try 中的代码失败)。 在布尔上下文中,它告诉我们是否有错误。 在字符串上下文中,它给出错误消息。
|
1.29.10. does
does
关键字类似于 but
。 区别在于确实会将其添加到给定的变量中,而将其应用于它的副本。
因此正常赋值的工作原理是相同的:
> my $a = "Hi" but False; say $a.^name; # -> Str+{<anon|6>}
> my $b = "Hi" does False; say $b.^name; # -> Str+{<anon|7>}
> my $a = "Hi"; $a but False; say $a.^name; # -> Str
> my $b = "Hi"; $b does False; say $b.^name; # -> Str+{<anon|9>}
我们将 but
应用于 $a
的副本,由于未将其赋值给变量,因此该值被丢弃。
== 控制流
在本章中,我们将从常规的自上而下讨论用于更改执行流程的语句。
1.30. 块儿
块是被视为一个整体的代码的集合。 一对花括号内设置了块:
{
# This is a block
}
1.31. 范围(简介)
Raku 中的范围是连续递增的整数的集合。 范围 1 .. 10
包含从1到10的所有整数。
..
运算符给出范围(而不是列表):
> (1 .. 5).^name
Range
> say (1 .. 5) 1..5
范围是惰性的,因此只有在实际需要时才计算各个值。
从10(但不包括,所以从11)到100万:
> (10 ^.. 1_000_000)
我们也可以排除最后一个值:
> (10 ..^ 1_000_000)
起始值和结束值均不包括在内:
> (10 ^..^ 1_000_000)
(将 ^
字符读为 «up to/from,但不包括»。)
^ 是范围运算符的一部分,而不是值!
这就是为什么 ^ 和范围运算符之间不能有任何空格的原因。
如果需要,我们可以使用此简短格式。 例如取 10 个值:
|
> my @values = ^10;
[0 1 2 3 4 5 6 7 8 9]
它从零开始并递增。
1.32. loop
loop
语句是其他语言所熟知的经典 «for» 循环。 它需要三个由分号分隔的语句:
> loop (my $i = 0; # The initial value
> $i < 10; # The test to decide if the loop should be stopped
> $i++) # The incrementer
> { print $i; }
> print "\n";
0123456789
请注意,如此处所做的那样,可以将 my
用作引入新变量作为循环计数器,也可以使用在循环外部定义的变量。 但是副作用是我们改变了它的值。
可以跳过最后一部分(增量器):
> loop (my $i = 0; $i++ < 10;) { say $i; }
1
2
3
...
但这可能会引起细微的变化(不同的起始值),如上所示。
语句必须在括号中指定。
我们可以遍历这样的数组:
my @a = <A B C D E F G H I J K L>;
loop (my $i = 0; $i < @a.elems; $i++) {
print "|", @a[$i];
}
print "\n";
运行它:
$ raku loop-array
|A|B|C|D|E|F|G|H|I|J|K|L
第一行是一个由单个字符组成的数组。 如第2.7.1节"字符串"中所述。 |
我们本可以这样写的:
my @a = "A" .. "L";
大量代码要遍历数组,很容易使索引错误(通常为1)。 但是,我们有一种更有效的方式来执行此操作,我们将在下一部分中进行演示。
1.33. for
for
循环是最常用的循环类型,几乎可以用于任何循环。
上一节中的 «loop-array» 程序可以这样编写得紧凑得多:
my @a = <A B C D E F G H I J K L>;
for @a -> $elem {
print "|", $elem;
}
print "\n";
→
语法在以下代码块中引入了局部变量(隐式 my
变量),该变量包含数组中的每个值。
运行它给出相同的结果:
$ raku for-array
|A|B|C|D|E|F|G|H|I|J|K|L
注意区别; 这里我们迭代实际值,而不是索引。
还要注意,我们获得了该值的只读版本,因此尝试更改它将会失败:
print "|", $elem; $elem ~= ".";
Cannot assign to a readonly variable or a value in block <unit> at ./for-array-error line 7
练习 4.1 该程序的输出是什么?
my @a = <A B C D E F G H I J K L>;
my $elem = 99;
for @a -> $elem {
; # Do nothing
}
say $elem;
1.33.1. for 作为计数器
我们也可以执行特定次数的循环。 第4.2节"范围(简短介绍)"中描述的范围简短形式非常适合迭代:
for ^5 {
say "I like school.";
}
输出:
I like school.
I like school.
I like school.
I like school.
I like school.
索引是0到4(而不是1到5),但是由于我们不在表达式中使用它们并不重要。 |
练习 4.2
这个程序的输出是什么:
for 5 {
say "I like school.";
}
1.33.2. $_
(主题变量)
«主题变量» $_
是没有显式签名的块的默认参数:
for <a b c> { say $_ } # sets $_ to 'a', 'b' and 'c' in turn
say $_ for <a b c>; # same, even though it's not a block
通常最好使用显式变量(具有第4.4节"for"中所述的→语法),只要该变量具有良好的名称即可。
可以通过省略变量名来缩短在 $_
上调用方法的时间:
.say; # same as $_.say
我们不能使用显式块变量,例如 → $val ,在块外:
|
> $val.say for (1 .. 10) -> $val
===SORRY!=== Error while compiling: Variable '$val' is not declared.
Did you mean '&val'?
------> <BOL>⏏$val.say for ("aa" .. "bb") -> $val
问题是 → $val
仅在以下块中可用。 但是我们在块之前使用了很长时间,并且块丢失了。
这有效:
for (1 .. 10) -> $val {
$val.say;
}
1.33.3. 后缀 for
如果只有一个表达式,则可以使用 for
的后缀版本:
say "Country: $_" for @countries;
请注意,"一个表达式"也可以表示一个块:
> { .say; .say } for 1 .. 10;
或者我们可以用圆括号将表达式分组:
> ( .say; .say ) for 1 .. 10;
但是我不建议这样使用。
块中的最后一条语句不需要结尾的分号,但添加一条也无害。 |
我们可以遍历一个范围:
for 1 .. 10 -> $i {
say $i;
}
如果你已经有 $i 变量,它将被隐藏在 for 主体中,但之后将再次使用原始值。
|
范围短形式 ^10
在10次迭代循环中很方便。 它与 0 ..^ 10
和 0 .. 9
相同:
do-something($_) for 1 .. 10; # 1 .. 10 # 10 iterations
do-something($_) for ^10; # 0 .. 9 # 10 iterations
只要你不依赖于1到10的值即可。
1.34. 无限循环
这些都是一样的:
.say for 1 .. Inf
.say for 1 .. ∞
.say for 1 .. *
.say for ^Inf
请注意,我们必须使用 ^Inf
来获取 Range。 一个 Inf
只是一个值,循环只会运行一次。
另请注意,最后一个从零开始。
或者,如果不需要计数器,则可以使用不带参数的 loop
(并且可以省略括号):
loop { say 'forever' }
无限循环应具有退出策略。 我们将在第4.17.3节"last"中讨论 last
。
1.35. while
只要给定条件为 true,while
语句就会执行该块。
my $x = 1;
while $x < 4 {
print $x++;
}
print "\n";
$ raku while
123
while
也可以用作语句修饰符:
my $x = 1;
print $x++ while $x < 4;
print "\n";
$ raku while2
123
输出是相同的,因为在执行 print
语句之前已经测试了条件。
1.36. until
until
语句是带有否定测试的 while
语句。 只要表达式为假,它将执行该块:
my $x = 1;
until $x > 3 {
print $x++;
}
print "\n";
$ raku until
123
until
也能用于语句修饰符:
my $i = 0;
say "Hello" until $i++ > 10;
1.37. repeat while
repeat { … } while
语句的末尾有测试。 结果是该块至少执行一次。 只要条件成立,就会发生另一次重复。
my $x = 5;
repeat {
print $x++;
} while $x < 1;
print "\n";
$ raku repeat-while
5
可以将 repeat
和 while
语句放在开头:
my $x = 5;
repeat while $x < 1 {
print $x++;
}
print "\n";
即使放在最前面,该条件仍会在循环结束时进行计算。
1.38. repeat until
repeat {…} until
语句是带有否定测试的 repeat {…} while
语句。 它将至少执行一次该块,然后执行一次,只要表达式为 False
:
my $x = 5;
repeat {
print $x++;
} until $x > 1;
print "\n";
$ raku repeat-until
5
可以将 repeat
和 until
语句放在开头:
my $x = 5;
repeat until $x > 1 {
print $x++;
}
print "\n";
即使放在最前面,该条件仍会在循环结束时进行计算。
1.39. 循环总结
结构 |
查看所在章节 |
总是执行一次 |
loop |
4.3, "loop" |
No |
for … |
4.4, "for" |
No |
… for |
4.4.3, "Postfix for" |
No |
while … |
4.6, "while" |
No |
… while |
4.6, "while" |
No |
until … |
4.7, "until" |
No |
… until |
4.7, "until" |
No |
repeat … while |
4.8, "repeat while" |
Yes |
repeat while … |
4.8, "repeat while" |
Yes |
repeat … until |
4.9, "repeat until" |
Yes |
repeat until … |
4.9, "repeat until" |
Yes |
1.40. if
我们可以使用 if
语句有条件地执行一次块:
if $hour == 17 {
say "Time to go home";
}
1.40.1. elsif
我们可以有几个条件:
if $hour == 17 {
say "Time to go home";
} elsif $hour == 8 {
say "Time to go to work";
}
1.40.2. else
我们可以添加一个 else
块,如果 if
和 elsif
都不匹配则执行:
if $time == 17 {
say "Time to go home";
} elsif $time == 8 {
say "Time to go to work";
} else {
say "Stay put!";
}
1.40.3. unlesss
使用 unless
来否定 if-test:
say "Not OK" if not $a;
say "Not OK" unless $a; # The same
请注意,unless 不支持 else 和 elsif ,否则生成的代码将难以理解。 (不,那根本不是假的。)
|
1.41. given
我们可以使用 given
给块儿设置 $_
:
> $_ = 12;
> .say; # -> 12
> .say given 13; # -> 13
> .say; # -> 12
> given 'a' { say $_ }; # sets $_ to 'a'
> say $_ given 'a'; # same, even though it's not a block
在执行块或(在这种情况下)前缀语句之后,将恢复旧值(如果有)。
given
也可以用来形成类似 switch 的语句(与 when
结合使用); 它将在"高级 Raku"课程中介绍。
1.42. with
with
语句就像 if
,但是测试定义性而不是真实性。 此外,它还对条件进行了主题化(将 $_
设置为值):
> say "OK" if 0; # -> ()
> say "OK" with 0; # -> OK
> with 12 { .^name.say } # -> Int
尝试输出未定义的值(Any
,Nil
和 Int
)在字符串中失败:
> my $a = Any;
> say "Not OK: $a;
Use of uninitialized value of type Any in string context.
Methods .^name, .perl, .gist, or .say can be used to stringify it to something meaningful.
所以我们可以使用 with
来检测它们:
for (0, "2", Any, Nil, Int, pi, "hello") -> $input {
with $input {
say "OK: $_";
} else {
say "Not OK: undefined value";
}
}
$ raku with
OK: 0
OK: 2
Not OK: undefined value
Not OK: undefined value
Not OK: undefined value
OK: 3.141592653589793
OK: hello
它提到的错误消息是说一种方法。 让我们尝试:
my $a; $a.say; # -> (Any)
请参见第6.3节"输出",以获取有关为什么 say 对未定义值起作用的讨论,但对在字符串中内插时不起作用的讨论。
|
因此,我们相应地重写了代码:
for (0, "2", Any, Nil, Int, pi, "hello") -> $input {
with $input { say "OK: $_"; }
else { print "Not OK: "; .say; }
}
我已将 else
中的 say
更改为 print
以避免额外的换行符。
$ raku with2
OK: 0
OK: 2
Not OK: (Any)
Not OK: Nil
Not OK: (Int)
OK: 3.141592653589793
OK: hello
1.42.1. given vs. with
given
(请参阅第4.12节"given")和 with
语句修饰符的使用有些相似:
表达式 |
given |
with |
.say XXX 12 |
12 |
12 |
.say XXX 0 |
0 |
0 |
.say XXX Nil |
Nil |
() |
.say XXX Any |
Any |
() |
.say XXX NaN |
NaN |
() |
仅当我们提供未定义的值时它们才有所不同:given
返回未定义的值,而 with
返回空列表。
请勿使用 with 作为语句修饰符。
|
1.42.2. orwith
with
具有 orif
,就像 if
具有 elsif
:
for ( 0, "2", 3/11, pi, "hello") -> $input {
with $input.Numeric { say "Number: $input"; }
orwith $input.Str { say "Str: $input"; }
else { say "??: <unknown type>"; }
}
$ raku orwith
Number: 0
Number: 2
Number: 0.272727
Number: 3.141592653589793
Str: hello
我们写一个数字,如果可能的话,将数值转换成数字。 如果没有,我们尝试将其转换为字符串。 这适用于所有定义的值,因此永远不会使用 else
部分。
(如果我们通过了例如 Any
,则对于 else
部分,with
条件会导致运行时错误。)
我们可以混合基于 if
和 with
的子句。
# This says "Yes"
if 0 { say "No" } orwith Nil { say "No" } orwith 0 { say "Yes" };
1.42.3. without
without
和 with
的关联与 unless
与 `if `的关联相同:否定测试。
for (1, "2", Any, Nil, Int, pi, "hello") -> $input {
without $input { print "Not OK: "; .say: }
}
$ raku without
Not OK: (Any)
Not OK: Nil
Not OK: (Int)
没有 else 子句,原因与 unless 子句相同。
|
我们也可以使用 with
和 without
作为语句修饰符:
> my $variable = 12; say "$_ is of type { .^name } with $variable;
12 is of type Int
> my $answer; say "undefined answer" without $answer;
undefined answer
1.43. ?? !!
这是一个紧凑的 if-then-else:
my $x = $y == True ?? 5 !! 4;
如果表达式位于 ??
的左侧 计算第一个参数(在 ??
之后) 为 True
,否则使用第二个参数(在 !!
之后)。
或者,以通常的详细方式:
my $x; if $y == True { $x = 5; } else { $x = 4; }
1.44. do
do
是一个块构造,返回块内的最后一条语句。
我们可以像这样使用 do
来缩短前一段代码:
my $x = do { if $y == True { 5; } else { 4; } }
或者甚至更紧凑:
my $x = do { $y == True ?? 5 !! 4; }
1.45. when
when
块看起来与 if
块相似,但是行为略有不同。
我们从常规的 if
块开始:
for True, False {
if $_ { say "if $_"; } ① ③
say "if $_ 2"; ② ④
}
① True → Executed ② True → Always executed ③ False → Not executed ④ False → Always executed
$ raku if-when
if True
if True 2
if False 2
when
块的行为有所不同,因为紧随其后的代码在同一块级别中被视为隐式 else
块:
for True, False {
when $_ { say "when $_"; } ① ③
say "when $_ 2"; ② ④
}
① True → Executed ② True → Not executed because «1» was ③ False → Not executed ④ False → Executed because «3» was not
我不得不使用 so
来避免警告。
$ raku if-when
when True
when False 2
1.46. 循环操纵
我们可以早点退出循环,跳过迭代或再次循环。
1.46.1. once
带有 once
前缀的块将只执行一次,即使它位于循环或递归例程中也是如此:
for ^5 {
once { say "once"; }
say "many ($_)";
}
$ raku for-once
once
many (0)
many (1)
many (2)
many (3)
many (4)
我们可以使用 FIRST 移相器实现相同的目的(我们将在"高级 Raku"课程中再次介绍它。)
|
1.46.2. next
next
命令开始循环的下一个迭代:
for ^5 {
next if $_ == 2;
say "many ($_)";
}
$ raku for-next
many (0)
many (1)
many (3)
many (4)
1.46.3. last
last
命令立即退出循环:
for ^Inf {
last if $_ == 5;
say "many ($_)";
}
$ raku for-last
many (0)
many (1)
many (2)
many (3)
many (4)
注意 last
给出的表达式。 如果该值从未达到 5
,则将发生无限循环。 (就如 last if $_ == 4.5
)。
也可以使用 die 或 exit (将在"高级 Raku"课程中进行说明)退出循环,但它们也将终止程序。
|
练习 4.3 编写一个程序,计算从1到向上的所有整数的和,直到和达到指定的上限(例如1000),并显示最后一个达到或超过极限的整数。
提示:使用无限循环,然后使用 last
退出该循环。
对于1000,输出可以是:«在值45处达到限制1000(1035)。»
1.46.4. redo
redo
命令重新启动循环块,而无需再次计算条件。
my $sum;
for 1 .. 1000 -> $i {
$sum += $i;
redo if $sum.is-prime;
}
say "Sum: $sum";
$ raku loop-redo
Sum: 546089
此处发生的是,如果总和是质数,我们将数字加1至1000,然后第二次(或更多)相加(请参见第5.12节"is-prime(素数)")。
如果没有 redo
,我们将得到 «500500»。(这很容易用 sum
来计算(请参见第9.16.1节"«sum»方法"):sum(1 .. 1000)
。)
1.46.5. LABEL
所有循环结构(while
,until
,loop
和 for
)都可以有一个标签,用于标识它们的 next
,last
和 redo
。 如果我们有嵌套循环,这将很有用,因为否则这些语句仅适用于同一作用域。
FIRST-ONE:
for 1 .. 20 -> $a {
NEXT-ONE: for 0 .. 2 -> $b {
next FIRST-ONE if ($a + $b).is-prime;
next NEXT-ONE if ($a + $b) % 3;
say "$a -> $b";
}
}
$ raku loop-labels
6 -> 0
8 -> 1
9 -> 0
12 -> 0
14 -> 1
15 -> 0
18 -> 0
20 -> 1
练习 4.4
在第4.17.1节"once"的 «for-once» 程序中在 once
后添加一个冒号后会发生什么情况?
== 数字
数字是数字值,例如 «2»,«0» 和 «3.14»。
1.47. 八进制、十六进制和二进制…
数字不能以零开头,除非指定数字系统(或基数):
数字系统 |
短形式 |
一般语法 |
Decimal |
123 |
:10<123> |
Octal |
0o123 |
:8<123> |
Hexadecimal |
0x12A39F |
:16<12A39F> |
Binary |
0b10101010 |
:2<10101010> |
> say :2<0b10101010>; # -> 170
请注意,小写和大写字母在数字上相等:
> :16<12A39F> == :16<12a39f>; # -> True
我们也可以使用过程式语法: |
> say :2("0b10101010"); # -> 170
将值放在变量中有效,但只能使用圆括号语法:
> my $a ="0b10101010";
> say :1<$a>; # -> Compile time error
> say :1($a); # -> 170
练习 5.1
Raku 支持多少个数字系统(不同的基数或进制)?
使用 REPL
。
1.47.1. base
我们可以使用 base
方法在其他数字系统(不同的底数)中显示数字:
> say 1200.base(8); # -> 2260
> say 170.base(2); # -> 10101010
> say 256.base(16); # -> FF
冒号语法
可以使用或不使用括号来指定函数的参数:
> say("12", "34"); # -> 1234
> say "12", "34"; # -> 1234
方法的参数通常用括号指定。 但是,如果要忽略它们,可以使用特殊的冒号语法:
> say 1200.base(8); # -> 2260
> say 1200.base: 8; # -> 2260
1.47.2. parse-base
parse-base
是 base
反义:
> say "FF".parse-base(16); # -> 255
往返行程:
> say "FF".parse-base(16).base(16); # -> FF
1.47.3. 其它转换
我们还可以使用格式字符串将数字转换为其他基数,包括 printf
,sprintf
和 fmt
:
指令 |
描述 |
例子 |
%b |
把无符号整数转换为二进制数 |
255.fmt('%b'); # → 11111111 |
%x |
把无符号整数转换为十六进制数 |
255.fmt('%x'); # → ff |
%X |
和 %x 一样, 但是是大写 |
255.fmt('%X'); # → FF |
查看 6.5 节, "printf" (和 6.5.3, "sprintf" 和 6.5.4, "fmt") 获取详情。
1.48. Unnicode 数
Unicode 有很多被视为数字的字符。 如果要引起混淆,请使用它们:
½ # 这是单个字符 Ⅷ # 单个字符(代码点 U+2168)
好。 如果你的终端机或打印机支持这些字符,你可能不会感到困惑,但是该怎么办:
1.49. 非数值
如果以字母(或下划线)开头,则可能是过程调用,预定义值或错误:
> say # -> error: Missing parameter
> True # -> True
> False # -> False
> abcdic # -> error: Undeclared routine
True
和 False
是内置的。 请参见第2.11节"True 和 False"。
1.50. N_U_M_B_E_R_S
你可以在数字中添加下划线,以使代码更具可读性。 编译器将忽略它们。 下划线的两边都必须有一个数字。
> my $number1 = 1000000000; # -> 1000000000
> my $number2 = 1_000_000_000; # -> 1000000000
> my $number3 = 1_0_0_0 ; # -> 1000
最后一个是合法的,但是很愚蠢。
1.51. 浮点数
Raku 有几种数字类型(除了整数之外(我们已经讨论过))。
1.51.1. pi
pi
是内置的:
> say pi; # -> 3.14159265358979
> say pi.^name; # -> Num
术语"浮点数"源自以下事实:小数点前后没有固定的位数; 也就是说,小数点可以浮动。 Raku 称他们为 Num
。
练习 5.2
将 pi
显示为二进制数:
> pi.say; # -> 3.141592653589793
使用 REPL
。
1.51.2. e
也可以使用欧拉数 e
(或 Unicode 版本 e
):
> say e; # -> 2.718281828459045
1.51.3. tau
与 tau
(或 Unicode 版本 τ
)一样:
> say tau; # -> 6.283185307179586
> say tau / pi; # -> 2
tau
是圆周与半径之比。
1.51.4. 浮点数错误
> my $one-third = 1/3; # -> 0.333333
这是预期的结果(唯一的意外是显示了3的实际数字)。 将其中三个相加应得出 0.999999:
> say $one-third * 3; # -> 1
单你是没有。我们确实得到的是 1。
1.51.5. 有理数
Raku 拥有内置的 Rat
(有理数)类型。
> my $one-third = 1/3; # -> 0.333333
> $one-third.^name; # -> Rat
> 0.3.^name; # -> Rat
是的,最后一个是有效的语法!
如果可能,Rat
类型将自动用于带有小数部分的值。 否则,将使用浮点类型 Num
。
Rat
类型在内部使用两个整数。 实际值是第一个("分子")除以第二个("分母")。
我们可以使用 nude
(«Numerator» + «Denominator»; «Nu» + «De»)方法获取值:
> (1/3).nude; # -> (1 3)
> (0.1).nude; # -> (1 10)
> 0.2.nude; # -> (1 5)
那么 0.333333 是从哪里来的呢? 转换发生在我们打印值时,因为它将值转换为字符串。 这种情况发生在我们无法(或应该)打印出来的东西之后,它是带有点的零和无穷多个3。 |
有关详细信息,请参见第6.2节"字符串化"。
内部尽量减少有理数:
> say (3/9).nude; # -> (1 3)
> say (8/16).nude; # -> (1 2)
我们可以添加有理数,因为 Raku 支持带分数的算术:
> my $sum = 1/2 + 1/3; # -> 0.833333
> say $sum.nude; # -> (5 6)
1.52. narrow
返回强制保留为最窄类型且不损失精度的数字。
> say (4.0 + 0i).narrow.perl; # -> 4
> say (4.0 + 0i).narrow.^name; # -> Int
也可以直接应用类型,但可能会降低精度:
> say pi.Int; # -> 3
1.53. sign
sign
将值转换为数字并返回符号; 如果数字为 0
,则为 0;对于正数,为 1;对于负数,则为 -1
。
> say 6.sign; # -> 1
> say (-6).sign; # -> -1
> say "0".sign; # -> 0
1.54. 凑整
我们可以通过几种方式将非整数强制为整数。
1.54.1. round
使用 round
将调用者(如有必要,转换为数字)四舍五入为最接近的整数:
> say 1.7.round; # -> 2
> say (−0.5 ).round; # -> 0
> say ( .5 ).round; # -> 1
round
可以使用第二个参数,指定一个我们要四舍五入为(多个)的值:
> say 1.07.round(0.1); # -> 1.1
> say 21.round(10); # -> 20
1.54.2. 截断
使用 truncate
将调用者(如有必要,转换为数字)向零四舍五入。
> say 1.2.truncate; # -> 1
> say truncate -1.2; # -> -1
1.54.3. floor
使用 truncate
将调用者(如有必要,转换为 Numeric)向下舍入到最接近的整数。
> say "1.99".floor; # > 1
> say "-1.9".floor; # -> -2
1.54.4. ceiling
使用 ceiling
将调用者(如有必要,转换为数字)向上舍入到最接近的整数。
> say "1".ceiling; # -> 1
> say "-0.9".ceiling; # -> 0
> say "42.1".ceiling; # -> 43
1.54.5. gcd (最大公约数)
将两个参数都转换为整数将返回最大公约数,即可以将它们都整数除的最大数字)。
> say 10 gcd 12; # -> 2
1.54.6. lcm (最小公倍数)
将两个参数都转换为整数,并返回最小公倍数,这是两个参数可均分的最小整数。
> say 10 lcm 12; # -> 60
> say 10 lcm 2; # -> 10
> say 2 lcm 3; # -> 6
1.54.7. msb (Most Significant Binary)
如果数字为0,则 msb
返回 Nil
。否则,它从数字的二进制表示形式的最高有效(最高值)数字1的右边返回位置(从零开始的索引):
> say 0b00001.msb; # -> 0
> say 0b00011.msb; # -> 1
> say 0b00101.msb; # -> 2
> say 0b01010.msb; # -> 3
> say 0b10011.msb; # -> 4
1.55. NaN(Not a Number)
NaN
的值为 Num
类型,用于表示浮点 «Not a Number» 值。 它被用作一些数学函数的返回值,这些数学函数没有实际的数值答案-但是数值仍然可以接受。
NaN
已定义,并且在布尔上下文中转换为 True
。 它在数值上不等于任何值,包括其自身。
> say cos ∞; # -> NaN
> say (0/0).Num; # -> NaN
1.55.1. isNaN
使用 isNaN
方法(或 ===
运算符)测试 NaN
:
> say (0/0).isNaN; # -> True
如果我们要执行以下操作,则可以使用值相等运算符 ===
(请参见第3.7.7节 "===")显式测试 NaN
:
> say (0/0).Num === NaN; # -> True
1.56. =~=
使用近似等于的运算符 =~=
(或 Unicode 版本 ≅
)来确定两个值在数值上是否几乎相等。 如果差异小于特殊动态值`$*TOLERANCE`(默认值为 1e-15
),则返回 True
,否则返回 False
。
我们用来解释 Rat
的示例(请参见第5.6节"有理数")为 1/3
。 没有 Rat
,这将是结果:
> my $b = 0.3333333333333333 * 3;
> say $b; # -> 0.9999999999999999
> say $b == 1; # -> False;
> say $b =~= 1; # -> True
注意,在实际中,Rat
类型减少了对 =~=
的需要。
1.57. is-prime (素数)
质数是一个只能被1除以的数字(整数)。
使用 is-prime
来确定给定值是否是质数:
> 7.is-prime; # -> True
> 1.4.is-prime; # -> False
最后一个显示为什么方法或过程名称不能以数字开头。 |
练习 5.3
编写一个程序,将所有素数(按数字递增的顺序)从1加到100_000(均包括在内),以显示总和是否为素数。
显示这些和中有多少是质数。
1.58. 取模和变体
数值 |
Int |
描述 |
% |
mod |
取模运算符 |
%% |
整除运算符 |
1.58.1. %
使用取模运算符 %
可以得到除法后的余数:
> say 10 % 3; # -> 1 # 3 * 3 + 1
> say 11 % 3; # -> 2 # 3 * 3 + 2
> say 12 % 3; # -> 0 # 4 * 3 + 0
模运算符 %
在除法之前将值转换为数值,并且也可以用于非整数:
> say 12 % "3.1"; # -> 2.7
1.58.2. mod
mod
是 %
的整数版本。 值必须是整数:
> say 7 mod 3; # -> 1 # 2 * 3 + 1
> say 8 mod 3; # -> 2 # 2 * 3 + 2
> say 9 mod 3; # -> 0 # 3 * 3 + 0
1.58.3. %%
除数运算符 %%
是模运算符的双胞胎姐妹。 如果余数为零,则模运算符返回除法的结果,而除数运算符 %%
返回 True
:
> say 9 %% 3; # -> True
> say 11 %% 3; # -> False
> say 9.3 %% "3.1"; # -> True
除数运算符 %%
在除法之前将值转换为数值,并且也可以用于非整数。
除数运算符是以下操作的快捷方式:$a % $b == 0
。
练习 5.4 编写一个程序,将1到1000之间每个不能被7整除的数字加在一块儿。
1.59. 其它运算符
Raku 有很多内置的数学运算符。
1.59.1. sqrt
数字的平方根:
> say sqrt(9); # -> 3
> say sqrt(-1); # -> NaN
> say sqrt(-1 + 0i); # -> 0+1i
1.59.2. exp
将参数转换为数值,并将返回的 $base
提高为 $power
的幂。 如果未提供 $base
,则使用 e
(欧拉数;请参阅第5.5.2节"e")。
> $power.exp($base);
> say exp 3; # -> 20.085536923187668
> say 3.exp; # -> 20.085536923187668
> say 2.exp(3); # -> 9
> say exp(2, 3); # -> 9
1.59.3. **
求幂运算符 **
将两个参数都转换为数值,并计算左手边的乘方为右手边的幂:
> say 2 ** 3; # -> 8
> say 3 ** 2; # -> 9
> say e ** 3; # -> 20.085536923187664
注意舍入错误(即使它们应该相同):
> say e ** 3 - exp 3; # -> -3.552713678800501e-15
也可以使用 Unicode 上标数字,例如:2³ 与 2 ** 3 相同。 |
1.59.4. expmod
expmod
将第一个参数提高为第二个元素的幂,并在其上应用第三个参数作为模数:
> say expmod(4, 2, 5); # -> 1 ## The same as: 4 ** 2 mod 5
> say 7.expmod(2, 5); # -> 4 ## The same as: 7 ** 2 mod 5;
方法形式需要整数。 过程版本也接受非整数,但将其截断为整数。
1.59.5. log
log
将对数返回给定值的指定底数(默认为 e
,欧拉数;请参见第5.5.2节"e",如果未给出):
> say log(10); # -> 2.302585092994046
> say log(10, e); # -> 2.302585092994046
结果(2.302585092994046
)是一个数字,当提高到基数(e
)的幂时,得出的值是(10
),或者近似值:
> say exp 2.302585092994046; # -> 10.000000000000002
让我们尝试另一个基数:
> say log(10, pi); # -> 2.0114658675880612
> say exp(2.0114658675880612, pi); # -> 10.000000000000002
如果基数为负,则返回 NaN
;如果基数为 1
,则抛出异常。
log10
log10
将其对数返回以 10 为底的数字,即将其幂提高到 10 时近似产生原始数字的数字。
可以用 log
代替:
> say log10(1001); # -> 3.0004340774793183
> say log(1001,10); # -> 3.0004340774793183
它为负参数返回 NaN
,为 -Inf
返回 0
。
1.59.6. 三角函数
这是完整的列表。 它们都以弧度表示:
函数 |
描述 |
sin |
sine |
asin |
arc-sine |
cos |
cosine |
acos |
arc-cosine |
tan |
tangent |
atan |
arc-tangent |
atan2 |
arc-tangent (two argument form) |
sec |
secant |
asec |
arc-secant |
cosec |
cosecant |
acosec |
arc-cosecant |
cotan |
cotangent |
acotan |
arc-cotangent |
sinh |
sine hyperbolic |
asinh |
inverse sine hyperbolic |
cosh |
cosine hyperbolic |
acosh |
inverse cosine hyperbolic |
tanh |
tangent hyperbolic |
atanh |
inverse tangent hyperbolic |
sech |
secant hyperbolic |
asech |
inverse secant hyperbolic |
cosech |
cosecant hyperbolic |
acosech |
inverse secant hyperbolic |
cotanh |
hyperbolic cotangent |
acotanh |
inverse hyperbolic cotangent |
cis |
cos(argument) + i*sin(argument) |
如果在此表中找不到所需的功能,请查看 «Math::Trig» 模块。 |
我们将安装并仔细研究练习12.1中的"Math::Trig"。
练习 5.5 你有一个内半径为 10cm 的广口瓶(或圆柱体)。 高 50 厘米。 它可以容纳多少升液体?
不记得公式了吗? 查一下 例如 link:https://www.varsitytutors.com/hotmath/hotmath_help/ topics/volume-of-a-cylinder[]。
练习 5.6 为你提供了两个罐子(或圆柱体),第一个罐子的内部半径为 10cm,高度为 35cm,第二个罐子的内部半径为 35cm,高度为 10cm。 你想要最大的一个(就内容而言)。 你选择哪一个? == 基本输入和输出
本章讨论用户的输入并输出到屏幕。
(在第13章,文件和目录中讨论了文件。)
1.60. 换行
默认情况下,所有读取内容(从文件或终端)的函数都会删除尾随换行符。 (然后说再加上一次。)
换行符用一个或两个字符标记:
操作系统 |
字符 |
以字符串方式 |
代码点 |
Windows |
<CR><LF> |
\r\n |
10 + 13 |
Linux |
<LF> |
\n |
10 |
Mac OSX |
<LF> |
\n |
10 |
Mac (old) |
<CR> |
\r |
13 |
特殊变量 $?NL
报告在打印换行符时编译器将使用什么(隐式地使用 say
或显式地使用 "\n")。
$?NL
给出了实际的换行符,因此仅打印变量无济于事。
我们可以使用 ords
(请参见第7.1.6.1节"ords")获取代码点:
> $?NL.ords; # On Linux we get «10»
(10)
此变量是只读的:
> $?NL = "\n";
Cannot assign to an immutable value in block <unit> at <unknown file> line 1
1.60.1. chop
chop
将调用者(或子例程形式的参数)强制转换为字符串,并在除去最后一个字符的情况下返回它。
> say "abcde".chop; # -> abcd
无法以任何方式获得已删除的字符,因此无法使用 chop
遍历字符串(从结尾开始)。 请改用 comb
(参见第7.5节"comb")。
chop
使用一个可选参数,指定要删除的字符数:
> say "abcde".chop(2); # -> abc
1.60.2. chomp
chomp
将调用者(或子例程形式,其参数)强制转换为字符串,并在删除最后一个字符(如果它是逻辑换行符)的情况下返回该字符串。
实际上,这不是很有用,因为标准读取行为会默认删除换行符。
1.61. 字符串化
有三种内置的方法可以对非字符串值进行显式字符串化:gist
,Str
和 raku
(以前称为 perl
)。(尽管前两者大多是隐式应用的,我们将在第6.3节"输出"中显示。)
差异可以总结如下:
表达式 |
gist |
Str |
raku/perl |
注释 |
pi |
3.141592653589793 |
3.141592653589793 |
3.141592653589793e0 |
[1] |
$a = pi |
3.141592653589793 |
3.141592653589793 |
3.141592653589793e0 |
|
1/3 |
0.333333 |
0.333333 |
<1/3> |
[2] |
$b = 1/3 |
0.333333 |
0.333333 |
<1/3> |
|
Any |
(Any) |
Error |
Any |
[3] |
$c = Any |
(Any) |
Error |
Any |
[4] |
(1..5) |
1..5 |
1 2 3 4 5 |
1..5 |
|
@a = 1..5 |
[1 2 3 4 5] |
1 2 3 4 5 |
[1, 2, 3, 4, 5] |
|
(1..Inf) |
1..Inf |
1..* |
1..Inf |
[5] |
@b = (1..Inf) |
… |
… |
Error |
[6] |
[1] pi
常数是一个浮点数(请参见第5.5节"浮点数"),内部类型为 Num
。
[2] 1/3
表达式给出内部类型为 Rat
的有理数(请参见第5.6节"有理数")。
[3] 错误消息是 «No such method 'str' for invocant of type 'Any'. Did you mean 'Str'?»。
[4] 错误消息是 «Use of uninitialized value $a of type Any in string context»。
[5] 1..Inf
表达式是一个惰性列表。 仅在需要时才计算各个值。 有关更多信息,请参见第16.1.1节"懒惰与渴望"。
[6] 错误消息是 «Cannot .elems a lazy list»。
请注意,在赋值给列表时,列表的输出有所不同(懒惰与否)。
1.61.1. gist
词典中 «gist» 的定义是“语音或文本的实质或一般含义。”
每个对象都有一个 gist
方法(继承自基类"Mu", 请参见第3.2节"^mro(方法解析顺序)")。 其任务(根据官方文档)是"返回调用者的字符串表示形式,并对其进行了优化以实现人类的快速识别。"
如果对象很大,则 gist
仅返回有关该对象的部分信息。 一长串列表的上限是第100个元素(后跟 «…»):
> my @a = (1 .. 110); say @a.gist;
[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 ...]
gist
处理未定义值:
> my $a; say $a.gist;
(Any)
1.61.2. Str
Str
展平数据结构:
> my @a = (1,2,3, (4,5,6),7,8,9); say @a.Str;
> say @a.gist
[1 2 3 (4 5 6) 7 8 9]
Str
在未定义的值上失败:
> my $a; say $a.Str
Use of uninitialized value $a of type Any in string context.
1.61.3. raku(perl)
每个对象都有一个 raku
方法。 它可以用于(递归地)转储对象,并在Raku实际存储对象时显示对象。
此方法对于以适合以后传递给程序的格式转储数据结构很有用。
> say (1/3).raku; # -> <1/3>
较早版本的 Raku 没有 raku 方法。 如果无法升级,请使用 perl 。
|
1.61.4. 总结
gist |
Str |
raku |
|
Flattens data structures |
No |
Yes |
No |
Handles undefined values |
Yes |
No |
Yes |
Shows everything |
No |
Yes |
Yes |
旧名称(从将语言从«Perl 6»重命名为«Raku»之前的名称)是 perl ,你可能会在相当一段时间内看到它。
|
1.62. 输出
我们已经使用 say
(有时是 print
)在屏幕上显示数据,并且说它们是相等的,只是添加了带有 say
的换行符。
那不是完全正确。 put
例程在打印时添加了换行符,并且说起来有些神奇。
函数 |
带 \n |
字符串化 |
查看章节 |
say |
Yes |
.gist |
6.3.1, "say" |
put |
Yes |
.Str |
6.3.2, "put" |
No |
.Str |
6.3.3, "print" |
|
.raku |
我们先从 say
开始,然后看一下 put
,最后是 print
。
1.62.1. say
say
是 Raku(而不是 print
)中最常用的输出方法,因为它在末尾添加了换行符。
请注意,如上表所述,在变量或数据结构上说使用要点。
但是只要你将其放入字符串中,例如 "say $a", 它使用 Str
来对字符串进行排序,然后在该字符串上使用 gist
(由于该字符串已经是字符串,因此它不会执行任何操作)。 这也适用于花括号内的字符串:
> my $a; say "The value is: $a";
Use of uninitialized value of type Any in string context.
> say "The value is: { $a }";
Use of uninitialized value of type Any in string context.
必须使用 gist
或 raku
(可处理未定义的值)手动将其字符串化:
> say "The value is: { $a.gist }";
The value is: (Any)
> say "The value is: { $a.raku }";
The value is: Any
如果要控制输出,可以使用 «Defined-or» 运算符 //
(请参阅第2.12节"//"):
> say "foo { $a // "Undefined" } bar"
foo Undefined bar
也可以根据你要实现的目标,使用 defined
(请参见3.5.2节,"defined") 或 try
(请参见3.6.3节,"通过 try 防止运行时错误")来防止输出未定义的值。
1.62.2. put
无论类型如何,put
都会在值上使用 Str
。 它在末尾添加一个换行符。
> put ^10; # -> 0 1 2 3 4 5 6 7 8 9
> say ^10; # -> ^10
再次提醒你在 say
的时候注意插值:
> put "{ ^10 }"; # -> 0 1 2 3 4 5 6 7 8 9
> say "{ ^10 }"; # -> 0 1 2 3 4 5 6 7 8 9
1.62.3. print
print
(还有 say
和 put
)打印到指定的文件句柄,如果不使用一个,则打印到 $*OUT
。
print
与 put
相同,只是它不会在末尾添加换行符。
1.62.4. put vs say
为什么使用 put
而不是 say
(如《Learning Perl 6》一书中所提倡的):
-
say
使用gist
-
put
使用Str
my @a = 1..5 |
结果 |
注释 |
say @a |
[1 2 3 4 5] |
[1] |
say @a, "X" |
1 2 3 4 5]X |
[1] |
say @a ~ "X" |
1 2 3 4 5X |
|
say "@a[]" |
1 2 3 4 5 |
|
put @a |
1 2 3 4 5 |
|
put "@a[]" |
1 2 3 4 5 |
|
print @a, "\n" |
1 2 3 4 5 |
|
print "@a[]\n" |
1 2 3 4 5 |
它们所有人的末尾都有换行符。
[1] 这是 say
实际使用 gist
的唯一情况。 在所有其他情况下,首先字符串插值均适用,使用 Str
。 这也适用于 REPL
模式下的隐式输出(请参见第1.3节“ REPL”)。
结论:如果只获得部分数据结构没关系,请使用 say
。 如果有关系,请使用 put
。
1.63. 字符串化数字
数字是有问题的。 特别是(RAT
类型的)有理数。 例如:
> my $third = 1/3;
> say $third; # -> 0.333333
> say $third.Str; # -> 0.333333
> say $third.gist; # -> 0.333333
> say $third.raku; # -> <1/3>
一个具有无限数字位数的数字(例如 1/3
)将被截断。 在这里,我们在小数点后有六位数。
在字符串化之前,将值强制为浮点数会得到不同的结果:
> (1/3).Num.say
0.3333333333333333
将有理数强制转换为另一种类型,然后再返回,也可能会出现问题:
> 1/3 <=> 0.33333333333333; # -> More
当我们进行三向比较时,1/3
实际上是 1/3
(请参见第3.7.3节"<⇒"),因此这很有意义。
> (1/3).Str.Num <=> 0.33333333333333; # -> Less
在这里 1/3
已被字符串化为 0.333333
,并被强制转换回数字值。
这里的计算是正确的,但字符串化不匹配:
> my $r = 1/3; # -> 0.333333
> my $s = .3333333333333; # -> 0.3333333333333
> say "$r > $s ? {$r > $s}" # -> 0.333333 > 0.3333333333333 ? True
小数点后的位数比分母中的位数多 1,最小值为 6。
> say (7412 / 123456789).raku; # -> <7412/123456789> ## Denominator with 9 digits
> say (7412 / 123456789); # -> 0.0000600372 ## 10 digits
请注意,位数是基于内部减少的值,而不是程序员指定的位数:
> say (123123123 / 321321321).raku; # -> <41/107> ## Denominator with 3 digits
> say (123123123 / 321321321); # -> 0.383178 ## 6 digits, the minimum
1.64. printf
在打印值之前,请使用 printf
(«print formatted»)格式化值。
用作函数时,第一个参数是格式字符串,其余参数是要打印的值:
> my $x = 1;
> printf("%e\n", $x); # -> 1.000000e+00
我建议在格式字符串上使用单引号。 我不得不在上面使用双引号来获取换行符。
上面的行之所以有用,是因为除非在末尾添加 {} ,否则哈希变量不会插在字符串中。 (请记住,我们必须在数组变量之后添加 [] 以进行插值。)
|
> my %e = ( A => 14 );
> my $x = 1;
> printf("%e\n", $x); # -> 1.000000e+00
> printf("%e{}\n", $x);
Your printf-style directives specify 0 arguments, but 1 argument was
supplied in block <unit> at <unknown file> line 1
现在 %e{}
表示哈希变量,并且格式字符串没有指令。 然后,它得到一个参数,并抱怨。
当用作方法时,我们在格式字符串上调用它,并将值作为参数传递:
> "%s\n".printf($x);
该示例使用 %s
表示字符串,它打印我们传递的字符串,并在末尾附加换行符。
指令是:
序列 |
描述 |
% |
A literal percent sign |
%b |
An unsigned integer, in binary |
%c |
A character with the given codepoint |
%d |
A signed integer, in decimal |
%e |
A floating-point number, in scientific notation |
%E |
Like e, but using an uppercase "E" |
%f |
A floating-point number, in fixed decimal notation |
%g |
A floating-point number, in %e or %f notation |
%G |
Like g, but with an uppercase "E" (if applicable) |
%o |
An unsigned integer, in octal |
%s |
A string (stringification with Str) |
%u |
An unsigned integer, in decimal |
%x |
An unsigned integer, in hexadecimal |
%X |
Like x, but using uppercase letters |
一些例子:
> printf("%e\n", 1); 1.000000e+00
> my $name = "Tom";
> printf("Hello %s, and welcome to the jungle!\n", "$name");
Hello Tom, and welcome to the jungle!
字符串内变量的常规插值会降低 printf
的用途:
> my $name = "Tom";
> print "Hello $name, and welcome to the jungle!\n";
Hello Tom, and welcome to the jungle!
1.64.1. 参数索引
如果参数数量与格式字符串不匹配,则会收到错误消息(如上面的警告中所示):
> printf("%s\n", "Tom", 11222);
Your printf-style directives specify 1 argument, but 2 arguments were supplied
in block <unit> at <unknown file> line 1
可以改组它们的使用顺序:
> printf("Hello %s, and %s.\n", "Tom", "Welcome");
Hello Tom, and Welcome.
> printf('Hello %2$s, and %1$s.' ~ "\n", "Tom", "Welcome");
Hello Welcome, and Tom.
第一个参数指定为 1$
,第二个参数指定为 2$
,依此类推。 也可以重用参数:
> printf('Hello %1$s, and %1$s.' ~ "\n", "Tom");
Hello Tom, and Tom.
1.64.2. 标志
我们可以在 %
和字母之间指定标志:
标志 |
描述 |
例子 |
结果 |
space |
Prefix a non-negative number with a space |
printf '§% d§', 12; |
§ 12§ |
printf '§% d§', 0; |
§ 0§ |
||
printf '§% d§', -12; |
§-12§ |
||
|
Prefix a non-negative number with a plus sign |
printf '§%+d§', 12; |
§+12§ |
printf '§%+d§', 0; |
§+0§ |
||
printf '§%+d§', -12; |
§-12§ |
||
- |
Add trailing spaces (instead of leading) |
printf '§%-6s§', 12; |
§12 § |
0 |
Use leading zeros, not spaces, for padding |
printf '§%06s§', 12; |
§000012§ |
# |
Show a leading "0" and the type prefix (hexadecimal with "0x" or "0X", octal with "0o" and binary with"0b" or "0B") |
printf '§%#o§', 12; |
§014§ |
printf '§%#x§', 12; |
§0xc§ |
||
printf '§%#X§', 12; |
§0XC§ |
||
printf '§%#b§', 12; |
§0b1100§ |
||
printf '§%#B§', 12; |
§0B1100§ |
可以将数字指定为参数。 以下几行给出相同的结果: |
> printf '|%6s|', 12; # -> | 12|
> printf '|%*s|', 6, 12; # -> | 12|
这样可以很容易地将宽度指定为变量。
还可以指定要显示的最大字符数。 详情请参阅 https://docs.raku.org/routine/sprintf。
1.64.3. sprintf
sprintf
(«string print formatted»)的行为与 printf
相同(请参见上一节), 但是它返回字符串而不是打印它。
我们可以使用 sprintf
和 say
避免指定换行符:
> printf('Hello %2$s, and %1$s.' ~ "\n", "Tom", "Welcome");
Hello Welcome, and Tom.
> say sprintf('Hello %2$s, and %1$s.', "Tom", "Welcome");
Hello Welcome, and Tom.
sprintf
仅是一个函数。 相应的方法是 fmt
(请参阅下一节):
> my $t1 = $string.fmt($format);
> my $t2 = sprintf($format, $string); # Exactly the same
1.64.4. fmt
fmt
是 sprintf
函数的方法版本(请参见上一节)。
如果使用的 fmt
不带格式参数,则默认为 %s
。
一些例子:
> say 1200.fmt("%o"); # -> octal
2260
> say 1200.fmt("Octal: %o"); # More verbose.
Octal: 2260
> say 1200.fmt('Decimal: %1$d - Octal: %1$o');
Decimal: 1200 - Octal: 2260
1.65. 获取用户输入
1.65.1. prompt
使用 prompt
显示可选消息,然后等待用户键入内容。
my $name = prompt "What's your name? ";
say "Hi, $name! Nice to meet you!";
prompt 是 say 和 get (请参见第13.3.3节"get")的组合。
|
还记得数字和字符串之间的区别吗?
-
引号中的值是一个字符串
-
不带引号的值是数字或错误
那么,当我们从终端输入而不在字符串上使用引号时会发生什么呢? 让我们尝试一下,对填充了 prompt
的变量应用 ^name
:
loop {
my $name = prompt "Enter a value (or return to exit): " or exit;
say "Value $name is of the type { $name.^name }.";
}
$ raku prompt-type
Enter a value (or return to exit): Allan
Value Allan is of the type Str.
Enter a value (or return to exit): 12
Value 12 is of the type IntStr.
Enter a value (or return to exit): "12"
Value "12" is of the type Str.
Enter a value (or return to exit):
1.65.2. IntStr 和异形体
Raku 能够猜测类型,除非我们指定了看起来像数字的东西-不带引号。
然后,类型为 IntStr
,它不能同时是字符串和整数-不需要我们通常需要的类型转换。 IntStr
是同种异体类型; 两种类型的子类,它们可以表现两者之一。 这意味着它们将满足类型约束。
> my Int $a = prompt;
1234 # <- This is user input
> say $a.^name; # -> IntStr
> my Str $a = prompt;
1234 # <- This is user input
> say $a.^name; # -> IntStr
> my Int $a = prompt;
foo # <- This is user input
Type check failed in assignment to $a; expected Int but got Str ("foo")
Str 继承树
我们在 Str
的继承树中找到 IntStr
类型:
![img]()
请注意4种同种异体类型:IntStr
,NumStr
,RatStr
或 ComplexStr
。 我们不会覆盖其中的最后三个,但是如果你了解 IntStr
,那么它们应该微不足道。
有关 Int
继承树,请参见第3.2.1节"Int 继承树"。
> say IntStr.^mro; # -> ((IntStr) (Int) (Str) (Cool) (Any) (Mu))
> say Str.^mro; # -> ((Str) (Cool) (Any) (Mu))
> say Int.^mro; # -> ((Int) (Cool) (Any) (Mu))
请注意,^mro
(方法解析顺序)仅是-应用方法的类的顺序(查找方法的类的顺序)。
在 mro
对象上应用 raku
可以使它更加清楚,这只是一个平面结构(列表):
> say IntStr.^mro.raku; # -> (IntStr, Int, Str, Cool, Any, Mu)
我们通过使用引号避免了 IntStr
值的问题(如果我们将其视为一个问题)。 但是,当我们从命令行输入内容时(shell 引用是一个问题), 这是行不通的; 请参见第10.10.1节"带类型的 MAIN"。
练习 6.1
编写一个要求输入数字的程序,假设输入为二进制,一直到十六进制格式(即以2为底,以3为底,以16为底), 然后以十进制打印该值。
如果失败,请不要打印该值; 例如"12"不是二进制。 == 字符串
本章介绍了字符串以及我们可以使用它们进行的一些基本操作。
字符串是真实的标量值,不能视为字符列表。 (我们可以将其转换为字符列表,我们将在后面看到,但这是另一回事。)
我们不能从字符串中选择单个字符。
1.66. Unicode
所有字符串均以 Unicode 编码。
请注意这些函数的通用命名结构:
一个值 |
几个值 |
描述 |
uniname |
uninames |
获取字符的 unicode 名字 |
uniparse |
获取给定 Unicode 名称的 Unicode 字符 |
|
ord |
ords |
获取字符的 Unicode 代码点 |
chr |
chrs |
获取具有给定代码点的 Unicode 字符 |
缺少的 uniparse 的多值版本很容易使用 hyper 运算符实现。 我们将在«高级 Raku» 课程中介绍这两个方面。
|
1.66.1. chars
使用 chars
来获取字符串中字符的个数:
say "abc".chars; # -> 3
如果用于其他任何东西,它将被转换为字符串:
say chars(pi); # -> 17
我们可以使用 chop
和 chars
来获取字符串中的第一个字符:
my $s = "1234567890";
say $s.chop($s.chars -1); # -> 1
但是稍后我们将展示更好的方法。 请注意,该值是Unicode字素或用户可见字符的数量。 如果它看起来像一个字符,则它是一个字素。
让我们考虑一下斯堪的纳维亚字母“Å”。 它存在于Unicode字符集中(使用代码U + 00C5),可以“开箱即用”使用。 在Unicode中,我们还可以通过将字母«A»(U + 0041)与«上面的组合环»(U + 030A)组合在一起来构成它,如下所示:
my $s = "A\c[Combining Ring Above]"; # -> Å
say $s.chars; # -> 1
say "A\c[Combining Ring Above]" eq "Å"; # -> True
上面的代码向我们展示了 Raku 在从字符串或文件中读取字符串时对其进行规范化。 这意味着你得到的不一定是所提供的。 (换行符(行尾标记,在 Unix,Windows 和 Mac 上也不同)也已标准化,如第6.1节"换行符"中所述。) |
1.66.2. 组合字符
组合字符(或多个字符)在基本字符之后。 (这意味着编译器每次一次读取一个字符的字符串时,必须至少向前看一个字节;例如, for $string.comb → $char { … }
。)
1.66.3. codes
这为我们提供了 Unicode 代码点的数量。 通常与字符相同。
say "12øøæåsaåsæ".codes; # -> 11
say "12øøæåsaåsæ".chars; # -> 11
say "A\c[Combining Ring Above]".codes; # -> 1
say "A\c[Combining Ring Above]".chars; # -> 1
由于 Unicode 具有 Å 字符。 我们可以尝试 Unicode 没有的功能:
say "O\c[Combining Ring Above]".codes; # -> 2
say "O\c[Combining Ring Above]".chars; # -> 1
1.66.4. uniname
使用 uniname
获取字符串中第一个字符(字素)的 Unicode 名称:
say "A\c[Combining Ring Above]".uniname;
# LATIN CAPITAL LETTER A WITH RING ABOVE
say "abc".uniname;
# LATIN SMALL LETTER A
我们可以使用 Unicode 名称指定任何字符:
say "\c[LATIN SMALL LETTER A]"; # -> a
say "\c[Latin Small Letter a]"; # -> a
1.66.5. uninnames
使用 uniname
获取字符串中所有字符的 Unicode 名称:
say "O\c[Combining Ring Above]".uninames;
# (LATIN CAPITAL LETTER O COMBINING RING ABOVE)
我们可以使用 raku
方法获得更好的列表:
say "O\c[Combining Ring Above]".uninames.raku;
# ("LATIN CAPITAL LETTER O", "COMBINING RING ABOVE").Seq
say ‘»ö«’.uninames.raku;
# «("RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK", "LATIN SMALL LETTER O WITH DIAERESIS", "LEFT-POINTING DOUBLE ANGLE QUOTATION MARK").Seq»
1.66.6. uniparse
我们可以对 Unicide 名称使用 uniparse
来获得字符:
say "LATIN SMALL LETTER A".uniparse; # -> a
如果我们通过非法的东西,它就会失败:
uniparse("LATIN SMALL LETTER A WITH VERTICAL LINE BELOW AND ACUTE");
# Unrecognized character name [LATIN SMALL LETTER A WITH VERTICAL LINE BELOW AND ACUTE]
# in block <unit> at <unknown file> line 1
1.66.7. ord
使用 ord
获取一个字符的 Unicode 代码点(一个数字):
"A".ord; # -> 65
"Abba".ord; # -> 65
1.66.8. ords
使用 ords
获取字符串中所有字符的 Unicode 代码点(数字):
"Abb".ords; # -> (65 98 98)
我们可以重新访问在7.1.3"代码"部分中显示的组合字符:
say "A\c[Combining Ring Above]".ords; # -> (197)
say "O\c[Combining Ring Above]".ords; # -> (79 778)
请注意,Unicode 将第一个字符,«A» 和组合器合并为一个单独的 Unicode 字符。
对于 ord
,请务必谨慎,因为你认为这是单个字符:
say "O\c[Combining Ring Above]".ord; # -> 79
1.66.9. chr
使用 chr
将整数转换为 Unicode 字符。
say 65.chr; # -> A ## Decimal
say 0x41.chr; # -> A ## Hexadecimal
1.66.10. chrs
使用 chrs
将整数列表转换为 Unicode 字符字符串:
say <67 97 109 101 108 105 97>.chrs; # -> Camelia
我们可以再次尝试合并的字符:
say (79, 778).chrs.ords; # -> (79 778)
say "O\c[Combining Ring Above]".chrs.ords; # -> (79 778)
请注意,他们并不总是往返。
say 197.chr; # -> Å
say (65, 778).chrs; # -> Å
say (65, 778).chrs.ords; # -> (197)
请注意,«A» 和组合器将替换为单独的 Unicode 字符。
练习 7.1
编写一个要求输入(循环)的程序,然后在屏幕上打印之前,将每个小写字母(仅a-z)替换为大写字母,反之亦然。 其他字符保持不变。
使用 ord/ords 和 chr/chrs 。
|
1.67. join
我们可以使用 join
将字符串列表粘合在一起:
say <1 2 3>.join; # -> 123
如果我们指定要 join
的参数,则将在元素之间使用该字符串。 我们可以像这样生成 CSV 文件行:
say <12 hello 3.14 bingo 87>.join(";"); # -> 12;hello;3.14;bingo;87
1.68. split
这是 join
的反面:
say "12;hello;3.14;bingo;87".split(";"); # -> (12 hello 3.14 bingo 87)
注意,我们分割的文本(在本例中为;)不包括在结果中。
split
使用可选的第二个参数,一个整数告诉它将其拆分为多少部分:
"12;hello;3.14;bingo;87".split(";", 2)
# (12 hello;3.14;bingo;87)
1.69. words
我们可以使用 split
和空格字符将字符串拆分成单词:
my @words = $text.split(" ");
请注意,如果我们彼此之间有多个空格,这还不太可行。 (我们可以使用正则表达式作为 split
的参数来解决此问题,我们将在第11章,正则表达式简介中进行介绍。)
但是 word
方法是不言自明的,它处理多个空格:
my @words = $text.words;
words 不能很好地处理标点符号:
|
"This is it, isn't it? Or perhaps not. 2nd try".words.join("|");
# This|is|it,|isn't|it?|Or|perhaps|not.|2nd|try
一种可能的解决方案是在应用 words
之前,将所有不是字母或数字的内容替换为空格。
也可以使用 <one two …> Quote Words 构造。 请参见第8.3节"<xxx>(Quote Word)"。
1.70. comb
我们可以使用 split
从字符串中获取单个字符的列表:
say "12345".split(""); # -> ( 1 2 3 4 5 )
say "12345".split("").elems; # -> 7
注意空的第一个和最后一个元素。
elems 给 出列表中元素的个数。
|
最好使用 comb
:
say "12345".comb; # -> (1 2 3 4 5)
say "12345".comb.elems; # -> 5
可以获取字符组:
say "12345".comb(2); # -> (12 34 5)
练习 7.2
编写一个在循环中要求整数值的程序。
如果该值为整数,则计算所有数字的总和。 提示:从第6.5.1节"prompt"中的"提示类型"开始。
|
练习 7.3
编写一个要求循环输入的程序。
- 显示最后一个字符
- 显示第二个字符
|
1.71. flip
我们可以使用 comb
, reverse
和 join
来反转字符串:
say "abc123".comb.reverse.join; # -> 321cba
comb
为我们提供了一个单字符列表,reverse
反转了此列表的顺序,join
通过字符串将列表合并在一起。
但是使用 flip
方法来反转字符串更容易:
say "abc123".flip; # -> 321cba
1.72. substr(部分字符串)
使用 substr
(substring)获取字符串的一部分,从指定的偏移量位置开始(因此第一个字符位于位置0),然后返回其余部分:
say "1234567890".substr(3); # -> 4567890
也可以指定长度(字符数):
say "1234567890".substr(3, 2); # -> 45
我们可以获取字符串的最后一个字符(参见练习7.3):
my $s = "123456";
say $s.substr($s.chars -1); # -> 6
这也适用,其中 *
表示"从末尾开始":
$s.substr(* -1)
1.72.1. substr-eq (部分字符串)
substr-eq
是 substr
和 eq
的组合:
say "abc123".substr-eq("c123", 3); # -> False ## As "123" ne "c123"
say "abc123".substr-eq("c123", 2); # -> True ## As "c123" eq "c123"
注意,我们不能像使用 substr
那样指定长度。
1.72.2. substr-rw (部分字符串)
substr-rw
是 substr
的版本,它向字符串的指定部分返回可写视图(与仅返回副本的 substr
相对)。
my $s = "abc"; $s.substr-rw(1, 1) = "Q"; $s.say; # -> aQc
my $s = "abc"; substr-rw($s, 1, 1) = "Q"; $s.say; # -> aQc
我们不受长度的限制:
my $s = "abc"; $s.substr-rw(1, 1) = "QQQ"; $s.say; # -> aQQc
第二个参数决定要删除的字符数。 我们可以将其设置为零以进行插入:
my $s = "abc"; $s.substr-rw(1, 0) = "ZZ"; $s.say; # -> aZZbc
我们可以使用绑定为子字符串创建别名(请参见第2.6.2节":=(绑定)"):
my $string = "abc*123*ABC";
my $partial := $string.substr-rw(4, 3);
say "$string - $partial"; ## -> abc*123*ABC - 123
$partial = "9876543210";
say "$string - $partial"; ## -> abc*9876543210*ABC - 987
$string = "123|abc|456";
say "$string - $partial"; ## -> 123|abc|456 - abc
请注意,即使我们将3个字符(123)替换为10(9876543210),别名在原始字符串中的位置和长度也相同。
1.73. 改变大小写
在练习 7.1 中,我们使用 ord
和 chr
将字母从大写改为小写,反之亦然,但是使用内置函数更容易。
函数 |
描述 |
lc |
小写 |
tc |
标题大写 |
tclc |
标题小写 |
uc |
大写 |
fc |
折叠大小写 |
wordcase |
单词大小写 |
1.73.1. lc(小写)
返回字符串的副本,其中所有字符都转换为小写。
say "this is IT!".lc; # -> this is it!
1.73.2. tc(Title Case)
返回字符串的副本,其中第一个字符转换为标题大小写(如果标题大小写不可用,则转换为大写),其余字符保持不变。
say "this is IT!".tc; # -> This is IT!
«Title Case» 与第一个字母的 «Upper Case» 几乎相同。 如果你感到好奇,请参阅 http://unicode.org/faq/casemap_charprop.html 了解详细信息。 |
1.73.3. tclc(Title Case Lower Case)
返回字符串的副本,其中第一个字符转换为标题大小写(如果标题大小写不可用,则转换为大写),其余字符转换为小写。
say "this is IT!".tclc; # -> This is it!
1.73.4. uc(Upper Case)
返回字符串的大写版本。
say "this is IT!".uc; # -> THIS IS IT!
1.73.5. fc(Fold Case)
使用 Unicode «fold case» 方法返回字符串的版本。 建议仅将其用于字符串比较。
say "this is IT!".fc; # -> this is it!
为什么推荐 «Case Folding»:我们可以将字符串转换为大写: |
say "Saß".uc; # -> "SASS"
say "Sass".uc; # -> "SASS"
德语的小写字母 «double s» "ß" 转换为大写字母 "SS"(并且字符串的长度已更改)。 因此,如果我们比较字符串 "Saß".uc 和 "Sass".uc,它们是相等的。
转换为小写字母似乎更安全,而且我还没有找到一个不起作用的示例。 (请随时为我提供帮助。)但是,保证 «Case Folding» 能够正常工作。
1.73.6. wordcase
返回字符串的副本,每个单词的第一个字符转换为大写,其余的转换为小写。
say "this is IT!".wordcase; # -> This Is It!
请注意,wordcase
接受两个可选参数:
-
:filter
- 代替内置wordcase
使用的函数 -
:where
-一个布尔表达式,用于打开/关闭每个单词的转换
say "this is IT!".wordcase(:where({ .chars == 2 }) ); # -> this Is It!
say "this is IT!".wordcase(:filter(&uc), :where({ .chars == 2 })); # -> this IS IT!
我们在圆括号内的花括号中指定代码。 指定的过程不包含花括号,但带有 &
前缀。
你为什么认为此函数没有正常的两个字母名称? |
练习 7.4 重写习题 7.1 中的 «swap-case»,以便它也转换 unicode 字母,即 a-z 以外的字母。
1.74. x(字符串重复运算符)
使用字符串重复运算符 x
将左侧的字符串重复右侧给出的次数:
say "123 " x 2; # -> 123 123
say "123 " x pi; # -> 123 123 123
重复计数必须是数字(或可以转换为数字的数字),除非已经是整数,否则它将被截断。
不要将 x 用作乘法运算符。 不是。 在数字上使用它将对它们进行字符串化处理:
|
say 3 x 4; # -> 3333 say 4 x 3; # -> 444
但是你可以使用 Unicode 乘法符号 ×
(带有代码点 «U+00D7»):
say 3 × 4; # -> 12
say 4 × 3; # -> 12
1.75. succ
在数字上使用的 succ
(后继)给我们的数字加一。
say pi.succ; # -> 4.141592653589793
say 109.succ; # -> 110
但这对字符串更有用(和更神奇):
say 'aa'.succ; # -> ab
say 'az'.succ; # -> ba
say 'α'.succ; # -> β
say 'a9'.succ; # -> b0
如果字符串中没有点(句点或 .
),则最后一个字母数字序列递增。 如果有一个或多个点,则第一个点之前的最后一个字母数字序列递增。
say 'a.a.a.a.a'.succ; # -> b.a.a.a.a
say 'img001.png'.succ; # -> img002.png
当到达字符范围的末尾(数字 0-9,字母 a-z 或其他 Unicode 范围)时,它将添加另一个字符(在数字情况下是正常的):
say 99.succ; # -> 100
say 'z'.succ; # -> aa
如果你已经拥有最高值,则什么都不会发生:
say True.succ; # -> True
say False.succ; # -> True
Inf.succ; # -> Inf
1.76. pred
数字上使用的 pred
(前代)使我们的数字减一。
say pi.pred; # -> 2.141592653589793
say 100.pred; # -> 99
在字符串上使用:
say 'ab'.pred; # -> aa
say 'ba'.pred; # -> ax
say 'β'.pred; # -> α
say 'b0'.pred; # -> a9
如果字符串中没有点(句号或 .
),则最后一个字母数字序列递减。 如果有一个或多个点,则第一个点之前的最后一个字母数字序列将递减。
say 'b.a.a.a.a'.pred; # -> a.a.a.a.a
say 'img002.png'.pred; # -> img001.png
say 'img000.png'.pred; # -> imf999.png
但这不会减少字符数:
say 'aaaa'.pred; # -> Decrement out of range ...
say '100'.pred; # -> 099
(这与 succ
不同,后者在必要时添加另一个字符。)
如果你已经具有最低值,则将得到一个错误(如上所述)或相同的值:
say False.pred; # -> False
say "a".pred; # -> Decrement out of range ...
say -Inf.pred; # -> -Inf
1.77. quoting
我们在第 2.7.1 节"字符串"中描述了单引号和双引号,但是我们还有更多的引号构造:
短形式 |
字符串 |
结果 |
描述 |
单引号 |
'ABC$a' |
ABC$a |
没有插值 |
Q |
Q#ABC$a# 或 Q{ABC$a} |
ABC$a |
没有插值 |
q |
q*ABC$a* |
ABC$a |
没有任何插值。 注意开始和结束字符 |
q:c |
q:c/ABC$aX{$a}/ |
ABC12$aX12 |
只有闭包被插值 |
双引号 |
"ABC$a {$a}" |
ABC12 12 |
变量和闭包被插值 |
qq/ABC$a {$a}/" |
ABC12 12 |
变量和闭包被插值 |
|
qw |
引用单词; 查看 7.12.1 一节, qw(引用单词) |
||
qqw |
引用单词(带插值); 查看 7.12.2 一节, qqw(带有插值的引用单词) |
||
qx |
执行程序, 查看 «高级 Raku» 课程 |
||
qqx |
执行程序, 查看 «高级 Raku» 课程 |
(鉴于我们在某处指定了 my $a = 12
。)
闭包是在花括号中指定的东西。
1.77.1. qw(引用单词)
这与在单引号字符串上应用 words
相同:
say '1 $aaaaa 17'.words; # -> (1 $aaaaa 17)
say qw/1 $aaaaa 17/; # -> (1 $aaaaa 17)
1.77.2. qqw(带插值的引用单词)
这与在双引号字符串上应用 words
相同:
my $aaaaa = "X";
say "1 $aaaaa 17"words; # -> (1 X 17)
say qqw/1 $aaaaa 17/; # -> (1 X 17)
1.78. 多行字符串(Heredocs)
打印多行字符串看起来不太好看,尤其是当我们需要嵌入换行符时:
print "Line 1\nLine2\nline3\n"; # Line 1 # Line2 # line3
一个更方便的方法是 heredoc:
say q:to/END/;
Here is
some multi-line
string
END
heredoc 的内容总是从下一行开始。 结尾可以是任何文字字符串,只要我们预先指定即可:
say q:to/BLAH/;
Here is
some multi-line
string
BLAH
如果缩进了终止符(在本例中为 «END»),那么该缩进量将从字符串文字中删除。
这个 heredoc;
say q:to/END/;
Here is
some multi line
string
END
产生如下输出:
Here is
some multi line
string
1.78.1. heredocs 中的插值
上面介绍的 heredocs 中没有任何内容。 但是我们可以使用第 7.12 节"引用"中描述的 q
, q:c
和 qq
引用机制:
开始 |
插值 |
内容 |
结果 |
q:to/END/; |
nothing |
$name and {$age}. |
$name and {$age}. |
q:to:c/EOF/; |
只有闭包插值({} 中的变量) |
$name and {$age}. |
$name and 15. |
qq:to/EOF/; |
闭包和插值 |
$name and {$age}. |
Tom and 15. |
(在我们声明: my $name = "Tom"; my $age = 15;
的 地方。)
1.78.2. 缩进
indent
方法由 heredocs 在内部使用来管理缩进,但可以直接使用(仅在字符串上):
"abc";
abc
"abc".indent: 2;
abc
"abc".indent(3);
abc
"abc".indent: 10;
abc
2. 数组和列表
数组是可变的,列表是不可变的。
创建数组并在方括号中显示(在输出中),并用括号显示列表:
> say [1,2,4,5].^name; # -> Array
> say (1,2,4,5).^name; # -> List
将列表分配给数组变量会将其转换为数组:
> my @something = (1,2,4,5); # -> [1 2 4 5]
> say @something.^name; # -> Array
区别(和区别)并不重要,除非涉及到惰性列表。 没有延迟数组之类的东西,因此将延迟列表分配给数组会强制对其进行求值。
可以将列表分配给标量变量:
> my $something = (1,2,4,5); # -> (1 2 4 5)
> say $something.^name; # -> List
将列表分配给标量变量将保留列表类型。 这意味着该变量是只读的: |
> $something[2] = 99;
Cannot modify an immutable List ((1 2 4 5))
请注意,例如 say
可以接收一个列表。 然后它将打印所有粘贴在一起的列表值:
say "ABC" ~ "123" # -> ABC123
say "ABC, "123" # -> ABC123
2.1. ,(列表操作符)
使用 ,
(逗号)列表运算符来生成列表:
> "rune", "helge", "tom", "jerry";
(rune helge tom jerry)
如果可以使你感觉更好,请添加圆括号…(圆括号只是分组运算符;请参见2.12.1,"()(分组运算符)"
> ("rune", "helge", "tom", "jerry");
(rune helge tom jerry)
字符串必须加引号,如上所示,但是如果值不包含空格,则可以使用缩写形式,如下一节所述。
2.2. <xxx>(引用单词)
这是 Quote Words 语法。 指定一个这样的字符串,它将转换为部分字符串的列表,并以空格(和空格,例如字符)作为分隔符。
> my @a = <Peter Paul Mary>; # -> [Peter Paul Mary]
> say @a.raku; # -> ["Peter", "Paul", "Mary"]
> say <rune helge tom jerry>; # -> (rune helge tom jerry)
另请参见第7.12.1节"qw(Quote Words)"。
请注意,变量不会插值;
> my $a =12;
> say <123 $a ss>
(123 $a ss)
第一个值看起来像一个数字,但是 Raku 是否将其当作一个值? 我们可以检查:
> <123 $a ss>.perl
(IntStr.new(123, "123"), "\$a", "ss")
正如 IntStr
的同种异体类型所报告的,它既是字符串,也是数字。 有关详细信息,请参见第6.6.2节"IntStr和同种异体"。
如果你不想使用同素异体字,请改用 words (请参见第7.4节"words"),因为它会产生字符串:
|
> say "123 $a ss".words.map( *.^name );
(Str Str Str)
2.2.1. 列表生成和异形体
如果使用"逗号运算符"生成列表,则会得到要求的内容:
> say (1,2,3)[0].^name; # -> Int
> say ("1","2","3")[0].^name; # -> Str
但是,如果使用 <>
缩写形式,则将获得看起来像数字的事物的同种异体,如第6.6.2节"IntStr和同种异体"中所述。
> say <1 2 3 4>[0].^name; # -> IntStr
> say <1.2 2 3>[0].^name; # -> RatStr
> say <A12 2 3>[0].^name; # -> Str
2.3. [](数组构造器)
使用 [
和 ]
数组构造器创建一个显式数组:
> say [1, 2, 3, 4].^name; # -> Array
请注意,为数组变量(@
符号)分配某些内容将其强制为数组:
> my @a = <rune helge tom jerry>; # -> [rune helge tom jerry]
> say @a.^name; # -> (Array)
> say <rune helge tom jerry>.^name; # -> (List)
注意优先级。 这是可行的,因为绑定运算符 :=
的优先级低于列表运算符 ,
:
> my $a := 1, 2, 3; # -> (1 2 3)
> say $a.^name; # -> List
> say $a; # -> (1 2 3)
但是用赋值运算符执行此操作无效:
> my $a = 1, 2, 3; # -> (1 2 3)
> say $a.^name; # -> Int
> say $a; # -> 1
第一个值分配给 $a
,然后返回。 然后,我们将 2
和 3
附加到该值以形成一个列表(返回并由 REPL 打印)。 但是该列表没有分配给变量,因此会丢失。
解决方法是,使用数组构造函数 []
或分组运算符 ()
:
> my $a = [1, 2, 3]; # -> [1 2 3]
> say $a.^name; # -> Array
> my $b = (1, 2, 3); # -> (1 2 3)
> say $b.^name; # -> List
列表是只读的,因此 $b 本质上是常量:
|
> $a[1] = 4; # -> 4
> say $a; # -> [1 4 3]
> $b[1] = 4;
Cannot modify an immutable List ((1 2 3))
in block <unit> at <unknown file> line 1
它对数组有作用:
> my @a = 1, 2, 3; # -> [1 2 3]
> say @a; # -> [1 2 3]
2.3.1. 手动创建异形体
我们可以创建一个具有显式(IntStr.new(123, "123"
) 的同种异体,如上所示。
但这样做更容易:
> my $a = <12>; say $a.^name; # -> IntStr
> my ba = <12.1>; say $b.^name; # -> RatStr
你不必指定同种异体类型(实际上不能)。 Raku会为你解决这个问题。
如果你在字符串(或变量)中包含值,则可以将其转换为同种异体,如下所示:
> my $a = "12"; say $a.^name; # -> Str
> say <<$a>>.^name; # -> IntStr
或者你可以使用 val
例程:
> say val($a).^name; # -> IntStr
它只作用于字符串:
> say val("12").^name; # -> IntStr
> say val("12.1").^name; # -> RatStr
> say val(12).^name;
Value of type Int uselessly passed to val()
请注意,val
无法用作方法。
2.4. [xxx](带插值的单词引用)
如果我们将"引号"加倍,则可以进行插值:
> my $a = 12;
> say <<123 $a ss>>; # -> (123 12 ss)
带有 unicode 字符 «
和 «
的«法语引号»同样适用:
> say «123 $a ss»; # -> (123 12 ss)
查看章节 7.12.2, "qqw(带插值的单词引用)"
2.5. Empty
使用 "Empty" 获取一个空列表。
所有这些都是相等的:
my @a; # No values.
my @b = (); # Explicit
my @c = Empty; # Also possible
2.6. 列表元素
你可以按索引访问单个项:
> say ("a" .. "z")[25]; # -> z
> my @d = <helge tom barry>; say @d[0]; # And NOT $d[0] as in perl5!
helge
即使我们使用了标量赋值,它也可以工作:
> my $a = [1, 2, 3, 4]; say $a[2]; # -> 3
> my $b = (1, 2, 3, 4); say $b[2]; # -> 3
2.7. pop / push / shift / unshift
我们有一些运算符可从列表中添加或删除值:
函数 |
描述 |
pop |
Remove one element from the end |
push |
Add the element(s) at the end |
shift |
Remove one element from the beginning |
unshift |
Add the element(s) at the beginning |
初始化数组:
my @a = <11 22 33 44 55>;
从末尾移除一个元素(55
):
@a.pop; # -> 55
在末尾添加一个元素:
@a.push(66);
从开头移除一个元素(11
):
@a.shift; # -> 11
在开头添加一个元素:
@a.unshift(77);
我们可以通过 push
和 unshift
同时添加多个元素,如下所示:
my @a = <11 22 33 44>;
@a.push(55, 66);
请注意,pop
和 shift
只会一次删除一项。
2.7.1. elems(列表大小)
使用 elems
获取列表中元素的数量:
> my $number-of-elements = @d.elems;
> my $number-of-elements = elems @d;
该值是只读的,不能用于更改元素数。
没有 «length» 方法或函数。 |
我们还可以通过在数字上下文中计算列表来获取元素数量:…
> say +("a" .. "z"); # -> 26
end
使用 end
获取列表中最后一个元素的索引:
> say ("a" .. "z").end; # -> 25
该值比 elems
返回的值小 1(因为第一个元素的索引或偏移量为0)。
对于空数组,它给出 -1
:
my @a; say @a.end; # -> -1
请注意,列表中的元素数实际上是列表中最后定义的元素: |
> my @a;
> say @a.elems; # -> 0
> say @a.end; # -> -1
> my @b = (1);
> say @b.elems; # -> 1
> say @b.end; # -> 0
> @b[10] = 's';
> say @a.elems; # -> 10
> say @a.end; # -> 11
那么我们可以用它们做什么呢?
2.7.2. 栈
堆栈是一种易于实现且没有太多开销的数据结构,这就是为什么它在低级编程中被大量使用的原因。 添加到堆栈中的最后一个元素是第一个检索到的元素。
使用 unshift
和 pop
进行堆栈:
my @stack = ...;
@stack.unshift($customer-id); # Add one element to the stack
my $current = @stack.pop; # Get one element from the stack
2.7.3. 队列
队列是一种数据结构,其中条目按添加顺序返回。 这通常是你对待等待客户的方式。
使用 push
和 shift
制作队列:
my @queue = ...;
@queue.push($customer-id); # Add one element to the queue
my $current = @queue.shift; # Get one element from the queue
2.8. rotate(列表旋转)
列表旋转可以通过 push/shift
(向左)和 unshift/pop
(向右)来完成,但是使用内置的 rotate
更容易:
(1,2,3,4,5,6).rotate; # Left. The same as rotate(1)
(2 3 4 5 6 1)
> (1,2,3,4,5,6).rotate(2)
(3 4 5 6 1 2)
> (1,2,3,4,5,6).rotate(-2) # Right
(5 6 1 2 3 4)
请注意,rotate 不会更改原始列表,但会返回修改后的版本。
|
2.9. 列表的列表
Raku 不会自动展平列表(与 Perl 相对),因此将列表添加(通过 push
或 unshift
)到列表中的结果是具有一个项目的列表-第二个列表:
my @a = <11 22 33 44>
my @b = <55 66>; @a.push(@b)
可能不是你的想法。
2.10. 展平列表
如果要将第二个列表的各个值插入列表,请使用 prepend
(而不是 unshift
和 append
(而不是 push
)):
> my @list1 = 1,2,3,4,5;
[1 2 3 4 5]
> my @list2 = 8,9;
[8 9]
> @list1.append(@list2); # push the individual values
[1 2 3 4 5 8 9]
> @list1.prepend(@list2); # unshift the individual values
[8 9 1 2 3 4 5 8 9]
我们可以使用 append
和 prepend
同时添加多个元素,此处显示为 append
:
my @a = <11 22 33 44>;
my @b = <55 66>; @a.append(@b);
2.11. 数组切片
我们可以访问几项(称为数组切片):
> my @a = 1,2,3,4,5,6,7,8,9,10,11,12,13;
> @a[0 .. 9]; # The same as [0,1,2,3,4,5,6,7,8,9]
(1 2 3 4 5 6 7 8 9 10)
它们不需要是连续的:
> say @a[0,9,2]; # - > (1 10 3)
数组切片是可写的,即我们可以为其赋值:
> my @a = <10 9 8 7 6 5 4 3 2 1 0>;
> say @a[2,4,6,8,10,0]; # -> (8 6 4 2 0 10)
> say @a[2,4,6,8,10,0].=sort; # -> (0 2 4 6 8 10)
> say @a; # -> [10 9 0 7 2 5 4 3 6 1 8]
我们从一个数字从10到0的列表开始。然后我们选择了其中一些,给出了一个新的值列表。 然后,我们对该列表进行排序,然后将排序后的列表分配回去(使用赋值运算符 =
的 .=
形式)。 最后,我们显示结果。
另一个示例(可能会有帮助的插图):
> my @array = 10, 4, 1, 8, 12, 3;
> my @indices = 0, 2, 5;
> my @values = @array[@indices]; # -> (10 1 3)
> my @sorted = @values.sort; # -> (1 3 10)
> @array[@indices] = @sorted; # -> (1 3 10)
> say @array; # -> [1 4 3 8 12 10]
![img]()
我们可以把代码写的更紧凑:
> my @array = 10, 4, 1, 8, 12, 3;
> @array[0,2,5].= sort;
> say @array;
[1 4 3 8 12 10]
尝试删除值并不能完全解决:
> my @a = 1..10; # -> [1 2 3 4 5 6 7 8 9 10]
> @a[2,4,5] = (); # -> ((Any) (Any) (Any))
> say @a; # -> [1 2 (Any) 4 (Any) (Any) 7 8 9 10]
但是我们可以使用 splice
,我们将在下一节中介绍。
2.12. splice
使用 splice
从列表中删除一些元素。 返回删除的元素。
我们可以像这样从给定索引中删除到结尾:
> @a.splice(5); # Leave the first 5 items, and remove the rest.
我们可以指定要删除的项数:
> @a.splice(5,2); # Leave the first 5 items, remove the next 2, and leave the rest.
我们可以插入另一个列表来代替我们删除的元素:
> @a.splice(5,2, @list); # Leave the first 5 items, remove the next 2 and replace
# with `@list`, and leave the rest as well.
请注意,替换列表的大小不必与已删除项目的数量相同。
替换值也可以指定为标量。
总结:
代码 |
@a 的值 |
@b 的值 |
my @a = "a" .. "n" |
[a b c d e f g h i j k l m n] |
|
my @b = @a.splice(5) |
[a b c d e] |
[f g h i j k l m n] |
my @b = @a.splice(5,2) |
[a b c d e h i j k l m n] |
[f g] |
my @b = @a.splice(5,2, <A B C>) |
[a b c d e A B C h i j k l m n] |
[ f g] |
my @b = @a.splice(5,2, 9,9,9,9) |
[a b c d e 9 9 9 9 h i j k l m n] |
[f g] |
请注意,在每次 splice
之前,我们都必须将 "@a" 重置为初始值。
可以使用 splice 在列表中插入一个(或多个)新值,而无需通过将 0 指定为第二个参数来删除任何内容。
|
> my @b = @a.splice(10, 0, 3.14);
[]
> say @a;
[a b c d e f g h i j 3.14 k l m n]
2.12.1. |(展平运算符)
我们可以通过在列表前添加 |
(竖线)来拼合列表:
> my @list1 = 1,2,3,4,5;
[1 2 3 4 5]
> my @list2 = 8,9;
[8 9]
> @list1.push(@list2);
[1 2 3 4 5 [8 9]]
> @list1.push(|@list2); # push the individual values, and not the list
[1 2 3 4 5 8 9]
请注意,仅在顶层(而不是递归方式)执行展平。
2.12.2. flat
或使用更冗长(和明确)的 flat
方法:
> (1, (2, (3, 4)), 5).flat;
(1 2 3 4 5)
展平不会递归展平数组,而只是列表(如上所示): |
> my @a = (1,2); # -> [1 2]
> my @b = (1, 2, @a, 1, 2); # -> [1 2 [1 2] 1 2]
> @b.flat; # -> (1 2 [1 2] 1 2)
我们可以使用超运算符强制它使数组变平:
> @b».List.flat; # Unicode version
> @b>>.List.flat; # ASCII version
超运算符将在"高级 Raku"课程中介绍。
2.13. map
使用 map
将一些代码应用于列表中的每个元素。 它使原始列表保持不变,并返回修改后的版本:
> my @a = 1..10; # -> [1 2 3 4 5 6 7 8 9 10]
> @a.map({ $^a + 1 }); # -> (2 3 4 5 6 7 8 9 10 11)
我们使用花括号来传递代码块。 对每个值执行该块,并将结果放置在结果列表中。
请注意,map 仅关心其所用列表的顶层。 有关详细信息,请参见 deepmap (在第8.13.4节 "deepmap" 中)。
|
2.13.1. 占位符变量
twigil(请参见第2.2.2节"Twigils") ^
表示它是一个占位符变量。 只要有 twigil,它就可以随便命名。
练习 8.1 这个代码的结果是什么:
> (1 .. 10).map({ $^a + $^b });
有关占位符变量的详细说明,请参见第10章,过程和第10.4节"占位符变量"。
2.13.2. Block 块儿
我们可以将任何代码块作为参数传递。 主题变量保存当前值:
> @a.map( { .sqrt } );
请注意,map
是惰性的,因此直到需要它们时才计算值。
2.13.3. *(Whatever Star)
如果我们只需要一个方法调用,则可以使用 Whatever Star 节省一个花括号块:
> (1..10).map( *.sqrt );
(1 1.4142135623730951 1.7320508075688772 2 2.23606797749979)
我们也可以有简单的表达式:
> (1..10).map(* + 1); # -> (2 3 4 5 6 7 8 9 10 11)
请注意,当我们使用 Whatever Star 时,花括号是非法的。
而且我们可以对表达式取反(因此, Whatever Star 都不必成为表达式中的第一个字符):
> say (1 .. 25).grep(! *.is-prime);
grep
在第8.21.1节"grep"中进行了描述。 它选择通过条件的值。
2.13.4. deepmap
使用 deepmap
可以将代码应用于列表中的每个元素,而不仅仅是像 map
那样在顶层应用。
map
和 deepmap
之间的区别可以总结如下:
> my @a = ((1,2),(3,(4,5)));
> @a.map( * +1 ); # -> (3 3)
> @a.deepmap( * +1 ); # -> [(2 3) (4 (5 6))]
deepmap
递归遍历结构,向每个元素加一。
map
仅看到第一级,即第一个是包含两个项目(1和2)的列表,然后是另一个包含两个项目(3和一个新列表(包含项目4和5))的列表。 +1
将值强制为数字,列表的数字值为大小。 所以我们得到2,因为它有2个项目。 下一个列表也有两个项目(一个值和一个新列表),因此还有2个。+1
给出最终结果(3,3)。
也有 flatmap
(不建议使用),nodemap
和 duckmap
。 详情参见《高级 Raku》课程。
2.14. sort
使用 sort
对列表进行排序。 它知道类型,因此在给定数字时将按数字排序,在给定字符串时将按字符串排序。
> (1, 2, 11, 0, 3, -1).sort; # -> (-1 0 1 2 3 11)
如果你混合使用数字和字符串,则会得到有趣的结果:
> (1, 2, 11, 0, "3", -1).sort; # -> (-1 0 1 2 11 3)
当我们对字符串进行排序时,它使用 Unicode 规范中的字符顺序,这与英语字母(A-Z和a-z)的 Isolatin 和 ASCII 顺序相同。 因此,大写字母先于所有小写字母。
如果我们有一个单词列表,有些带有一个首字母大写,有些没有,那么我们将在小写之前得到大写单词。
我们可以通过给它一个自定义的比较代码块来告诉 sort
如何排序:
@words.sort( { $^a.fc cmp $^b.fc } );
在比较它们之前,我们应用 fc
(折叠式;请参见第7.8.5节"fc(Fold Case)")将所有字符串转换为无大小写的版本。
块的内部指定了在编译器进行排序时如何比较任何两个元素; $^a
是第一个,$^b
是第二个。 我们使用这两个占位符变量名称(作为单个 *
无论星号如何均无效),并且如果需要,它们可以在表达式中多次使用。
cmp
(请参阅第3.7.1节"cmp")是三向比较运算符,它为我们(或更确切地说,为排序)做着工作。
当我们内联指定比较代码时,需要花括号。 可以像这样为我们指定完成此过程的过程的引用: |
.sort( &compare-elements )
如果计算的代码比上面的简单示例更多,请执行此操作。
注意 &
符号。 它告诉编译器传递对代码的引用。 如果我们跳过它,该过程将立即执行。
有关过程的介绍,请参见第10章,过程。
有一种感知排序版本叫 collate
。 详情参见《高级 Raku》课程。
2.15. reverse
我们可以使用 reverse 方法翻转列表:
> my @num1 = 1 .. 10;
[1 2 3 4 5 6 7 8 9 10]
> my @num2 = @num1.reverse
[10 9 8 7 6 5 4 3 2 1]
我们只是使用 reverse 来生成向下计数的范围,但这确实是我们应该使用序列的方式。 我们将在第16章,范围和序列中讨论它们。
|
我们可以对列表进行排序:
my @num3 = @num1.sort({$^b.fc <=> $^a.fc});
2.15.1. 交换两个变量
在一个«普通»编程语言中交换两个变量需要一个临时变量:
my $a = 1;
my $b = 2;
my $tmp = $a; $a = $b; $b = $tmp;
我们可以用一个列表赋值来完成:
($a, $b) = ($b, $a);
或使用 reverse
:
($a, $b) .= reverse;
2.16. 数组限制
我们可以指定一个数组的大小限制:
> my @d[10] = <rune helge tom jerry>;
[rune helge tom jerry]
> my @d[3] = <rune helge tom jerry>;
Index 3 for dimension 1 out of range (must be 0..2)
注意,限制是项的数量,而不是最后一项的索引。 |
2.17. 带类型的数组
我们可以在数组的值上添加类型约束,如3.1节"强类型"所述:
> my Int @values;
> my @values of Int;
使用 of
以获取类型约束:
> my Int @values; say @values.of; # -> (Int)
> my @values; say @values.of; # -> (Mu)
2.18. Shaped Array
形数组是一个具有多个维度的数组。
我们访问这样一个形状数组中的单个单元格;
> my @a; @a[1;2;3] = 2;
> my @a; @a[1][2][3] = 2; # The same
我们可以像这样访问其中的"一行":
> @a[1;2;*]; # -> ((Any) (Any) (Any) 2) ## List
> @a[1][2]; # -> [(Any) (Any) (Any) 2] ## Array
不是类型上的细微差别。
而是整个数组:
> @a; # -> [(Any) [(Any) (Any) [(Any) (Any) (Any) 2]]]
2.18.1. 固定大小的数组
我们也可以限制形状数组的大小:
> my @a[3;3;3];
> @a[3;3;3] = 12
Index 3 for dimension 3 out of range (must be 0..2)
in block <unit> at <unknown file> line 1
常规索引规则适用,因此第一项的索引(偏移量)为 0
。
我们可以为形状数组分配一个列表列表:
> my @a[3;3] = ((1,2,3), (4,5,6), (7,8,9))
[[1 2 3] [4 5 6] [7 8 9]]
2.18.2. shape
以列表形式返回数组的形状。 请注意,当我们仅给数组指定显式形状时,此方法有效。
my @foo[2;3] = ( < 1 2 3 >, < 4 5 6 > ); # Array with fixed dimensions
say @foo.shape; # -> (2 3)
my @bar = ( < 1 2 3 >, < 4 5 6 > ); # Normal array (of arrays)
say @bar.shape; # -> (*)
2.18.3. Shaped Arrays Usage
形状阵列可以像矩阵一样使用。 没有内置的运算符可以处理矩阵,因此像 «Math::Matrix» 这样的模块是更安全的选择。
2.19. unique(不重复的列表)
使用 unique
获得列表的副本,但不重复:
> (1,1,2,3,4,5,1,6).unique
(1 2 3 4 5 6)
如果你知道列表已排序,请使用 squish
而不是 unique
:
> (1,1,2,3,4,5,5,6).squish # OK
(1 2 3 4 5 6)
> (1,1,2,3,4,5,1,6).squish # Wrong usage.
(1 2 3 4 5 1 6)
2.19.1. repeated
repeated
方法与 unique
方法相反,因为它仅返回重复项:
> (1, 2, 1, 2, 3,4,5,6,1).repeated;
(1 2 1)
每次重复,它们都会发生一次,并且列表不会排序。 我们可以修复重复项并对其进行排序:
> (1, 2, 1, 2, 3,4,5,6,1).repeated.sort.squish;
(1 2)
2.20. xx(列表重复运算符)
使用 xx
重复左侧的列表或值,次数为右侧给出的次数:
> "abc" xx 3
(abc abc abc)
> "abc " xx 2
(abc abc)
>
该数字必须是整数,或者可以强制为整数的值。 零或负整数将返回一个空字符串:
> "abc" xx 0; # -> ()
> my $a = (True, False) xx 3; # -> ((True False) (True False) (True False))
> my $b = |(True, False) xx 3; # -> (True False True False True False)
我们可以通过在右边指定一个 *
(一个"Whatever Star")来生成一个无限列表:
> my $c = |(True, False) xx *
有关用例,请参见第16.3.6节"列表重复运算符和序列"。
注意与字符串重复运算符 x 相似(如第7.9节"x(字符串重复运算符)"所述)。
|
2.21. 列表选择
使用 map
将更改应用于所有值,并使用 grep
选择一些值。
grep
返回一个列表。 如果只需要第一个值,请改用 first
。 (请参见第8.21.2节"first"。)
如果你仅对至少存在一个匹配项感兴趣,那么 any
(与第3.4.1节"Nil & Any"中所述的"Any"都不相同)可能会有用。 有关详细信息,请参见《高级 Raku》课程。
2.21.1. grep
使用 grep
从列表中选择一些值。
不可被3整除的整数:
> say (1 .. 25).grep(* % 3);
(1 2 4 5 7 8 10 11 13 14 16 17 19 20 22 23 25)
只选择非素数:
> say (1 .. 25).grep(! *.is-prime);
(1 4 6 8 9 10 12 14 15 16 18 20 21 22 24 25)
只选择整数:
> say (1, 1.5, 2, 2.5, 3, 3.5, 4).grep(Int);
(1 2 3 4)
练习 8.2
获取所有两位数的质数,并计数。
提示:使用 grep
和 is-prime
(请参阅第5.12节"is-prime(素数)")。
2.21.2. first
first
方法(和函数)类似于 grep
,不同之处在于它仅返回第一个匹配项。 如果你只需要一次匹配,请使用此选项,因为这样会更快。
在(包括)1000之后的第一个素数:
> say (1000 .. Inf).first(*.is-prime);
1009
如果未找到匹配项,则返回 Nil
:
> say (0.1 .. 0.9).first(*.is-prime);
Nil
没有单独的 «last» 方法,但是使用可选的命名参数:end 表示搜索应从列表的末尾开始,而不是从头开始。
|
最高素数低于 1000:
> say (1 ..^ 1000).first(:end, *.is-prime);
997
2.21.3. head
从列表的开头返回指定的项目数。 如果未指定大小,则默认为1:
> (1 .. Inf).head
1
> (-1 .. Inf).head(2)
(-1 0)
> <a b c d 12>.head
a
2.21.4. tail
从列表末尾返回指定数量的项目。 如果未指定大小,则默认为1:
> <a b c d 12>.tail
12
不要在无限列表上使用 tail
:
> (-Inf .. Inf).tail
Cannot tail a lazy list
in block <unit> at <unknown file> line 1
但是 head
工作(我会说令人惊讶):
> (-Inf .. Inf).head
-Inf
2.22. min/max
min
返回列表中的最小值, max
返回列表中的最大值。
使用数字:
> (1 .. 10).min; # -> 1
> max 1 .. 10; # -> 10
> (1 .. *).max; # -> Inf
使用字符串:
> <aa a abc d f ff e>.min; # -> a
> <aa a abc d f ff e>.max; # -> ff
它们也可以用作中缀运算符,并且可以堆叠:
> say 8 min 10; # -> 8
> say 8 min 10 min 2 min 99 min -19; # -> -19
未定义的值将被忽略,因此以下代码也有效:
> say (5, Nil, 100, 2).min; # -> 2
2.23. 随机值
随机值很重要,并且很难实现。 大多数编程语言都使用伪随机数,看起来有些随机,但实际上不是。 Raku 也不例外。 这足够好,除非你需要真正的随机性进行加密。
2.23.1. rand
rand
作为函数给出介于零(包含)和1(非包含)之间的伪随机数(Num
类型):
> rand; # -> 0.4214056307236411
> rand; # -> 0.7753853239550014
当用作值的方法时,它返回介于零(包含)和给定值(非包含)之间的伪随机数(类型为 Num
):
> 100.rand; # -> 51.322528184845
练习 8.3
编写代码,以选择介于10到99之间的随机整数(均包括在内)。
2.23.2. pick
随机值通常用作列表的索引。 Raku 有一个常规的选择,可用于列表(或范围)以从中获取随机元素:
> @colours.pick; # From an array
> (10 .. 99).pick; # From a range
提示最后一个比我们在上面的练习中使用的 rand
代码更容易编写和理解。
如果该值用作数组索引,请直接在数组上使用 pick
:
> ("red", "blue", "green", "yellow").pick
我们可以同时要求更多的值:
> (10 .. 99).pick(10);
这样可以从范围中随机选择10个值,而无需重复。 如果你想要多个值,请问:
> ("red", "blue", "green", "yellow").pick(2);
(red yellow)
此 pick
调用将不重复这些值。
如果你要求提供更多的价值,它将提供尽可能多的价值(无需抱怨):
> ("red", "blue", "green", "yellow").pick(9);
(green yellow red blue)
如果希望全部使用,请使用以下语法使其显而易见:
> <red blue green yellow>pick(*);
(green yellow blue red)
这是一种以随机顺序对列表进行排序的简便方法。
练习 8.4
编写一段代码(在 REPL
中),该代码返回介于1和指定数字之间的随机质数。
使用100和1000作为限制。
有重复的 pick
通过应用循环可以实现重复:
> say (1 .. 6).pick for ^3 5
2
5
这可以用于掷骰子很多次。
将它们放在一行上的小技巧:
> (1 .. 6).pick.fmt("%d ").print for ^10; say "";
1 5 3 6 2 1 5 6 2 1
> say join(" ", ( (1 .. 6).pick for ^10) );
5 4 3 6 4 4 4 5 6 2
或者我们可以使用列表重复运算符 xx
(请参见第8.20节"xx(列表重复运算符)"):
> say (1 .. 6).pick xx 10
(6 5 5 4 4 6 6 2 4 2)
> say (1 .. 6).pick xx 10
(6 3 3 6 6 5 5 4 5 2)
2.23.3. roll
roll
旨在提醒你骰子的滚动。 它的行为与 pick
相同,只是它可以重复这些值:
> (1..6).roll(3); # -> (1 4 6)
> (1..6).roll(3); # -> (1 2 6)
> (1..6).roll(3); # -> (3 3 6)
计数默认为1:
> (1..6).roll; # -> 6
如果传递 *
作为计数,我们可以得到一个无限的惰性序列:
> (1..6).roll(*); # -> (3 3 6 ...)
我们可以在列表或类型对象的布尔值上使用它:
> (True, False).roll; # -> True
> Bool.roll; # -> True
我们也可以作弊:
> (True, True, False, False, False,).roll; # -> True
练习 8.5
生成一个随机的十个字符的字符串作为密码。 使用字母,数字和一些特殊字符(例如 «!» 和 «@»)。
练习 8.6
我们可以检查随机数生成器的质量,即分布是否均匀。
编写一个程序,在1..100范围内选择一百万个随机数,并打印一个频率表。
同时显示最小和最大计数。
2.23.4. srand
随机数并不是真正的随机数。 Raku 具有伪随机数序列,其随机性在于,编译器每次运行程序时都会通过调用 srand
在该序列中的不同位置启动。
我们可以自己调用 srand
来解决这个问题:
> srand(1234567890); say rand; say rand;
0.9168008342654074
0.297372052451493
> srand(1234567890); say rand; say rand;
0.9168008342654074
0.297372052451493
再次重置,并且它从同一位置开始。
请注意,实际值可能会有所不同,具体取决于操作系统和编译器的版本,但是每次都会获得相同的序列。
用另一个整数值调用 srand
,你将获得另一个序列。
> srand(112); (1 .. 6).pick.fmt("%d ").print for ^10; say "";
6 1 4 6 6 4 5 4 6 3
> srand(112); (1 .. 6).pick.fmt("%d ").print for ^10; say "";
6 1 4 6 6 4 5 4 6 3
srand
也影响 pick
:
> srand(1); (1..10).pick; # -> 4
> srand(1); (1..10).pick; # -> 4
> srand(1); (1..10).pick; # -> 4
2.24. 排列
使用 permutations
来获取列表的所有可能排列:
> say <a b c>.permutations;
((a b c) (a c b) (b a c) (b c a) (c a b) (c b a))
它关心位置,而不是实际值。 因此,重复值将导致排列重复:
> .say for <a b b>.permutations;
(a b b)
(a b b)
(b a b)
(b b a)
(b a b)
(b b a)
如果用作带有数字值的函数,它将把该值视为一个范围。 例如 permutations 3
与 permutations ^3
相同(与 permutations 0..2
相同)
> say permutations 3;
((0 1 2) (0 2 1) (1 0 2) (1 2 0) (2 0 1) (2 1 0))
练习 8.7
编写一个程序,从 permutations
的输出中删除重复项。
> say <a b b>.permutations-without-duplicates; # Doesn't exist.
((a b b) (b a b) (b b a))
2.25. 组合
使用 combinations
从列表中获取零和更多元素的所有可能组合:
> say <a b c>.combinations;
(() (a) (b) (c) (a b) (a c) (b c) (a b c))
输入列表中的重复值是允许的,并且将导致结果重复:
> <a b b>.combinations
(() (a) (b) (b) (a b) (a b) (b b) (a b b))
这是因为 combinations
使用的是位置,而不是实际值。
我们可以指定只需要给定数量的元素的组合:
> <a b c>.combinations(1)
((a) (b) (c))
我们可以指定一个范围来选择长度:
<a b c>.combinations(1..3)
((a) (b) (c) (a b) (a c) (b c) (a b c))
作为函数,我们将其数量作为参数。 例如。 3 将导致列表 0..2
:
> combinations 3
(() (0) (1) (2) (0 1) (0 2) (1 2) (0 1 2))
我们还可以添加要选择的元素数量,作为值或范围:
> combinations 3,1; # -> ((0) (1) (2))
> combinations 3,2; # -> ((0 1) (0 2) (1 2))
> combinations 3,1..2; # -> ((0) (1) (2) (0 1) (0 2) (1 2))
2.26. but(数组)
我们在第3.8节"but(True 和 False, but …)"中引入了 but
运算符,并说明了它如何处理标量值。
它不适用于数组(@
):
> my @a = <54 12> but False; # -> [54 12]
> say @a.^name; # -> (Array)
但是(双关语意)如果我们将其分配给标量,它确实可以工作:
> my $a = <54 12> but False; # -> (54 12)
> say $a.^name; # -> (List+{<anon|5>})
> say $a[0].^name; # -> (IntStr)
> say $a[1].^name; # -> (IntStr)
> say so $a; # -> False
> my $b = [54, 12] does False; # -> [54 12]
> say $a.^name; # -> (Array+{<anon|4>})
2.26.1. does(数组)
这也适用于 does
运算符。
第3.8.1节"does"进行了介绍。
2.27. 合在一块儿
假设你在手机(和 GPS)问世之前的某个时间遇到了一个废弃的火车站,并且非常需要知道该地点的名称。 问题在于电台名称以单独的字母显示,其中一些已经消失。
值得庆幸的是,他们躺在地上,一堆。 你将它们整理出来,但无法理解。
你碰巧是一名程序员,有一台笔记本电脑(在手机使用前的时间里…),所以程序可以解决问题。
(或者你可以说这是一个地名测验。)
墙上的名字是 "B??TO? ??N?R",缺少的字母是 AMNOOS。 打印所有可能的组合,阅读列表,看看是否有铃声响起。
my $name = "B??TO? ??N?R";
## 012345678901
my $letters = "AMNOOS";
my @name = $name.comb;
my @letters = $letters.comb;
my @blanks = (1,2,5,7,8,10);
for @letters.permutations.unique -> @current {
@name[@blanks] = @current;
say @name.join;
}
该程序为你提供720条建议。 正确的是 "BOSTON MANOR"(如果可以找到的话)。
问号的硬编码索引不是最佳的。 在第10.9.1节"单元过程"中,我们将研究让程序为我们进行计算。 == 对儿和散列 |
散列由成对儿的键和值组成。
在研究散列之前,我们先来看看 Pair
类型。
2.28. Pair
哈希(以及我们将在 [高级 Raku] 课程中研究的一些其他类型)由一对键和值组成。Pair
是内置类型,它是单个键和值的组合。它们可以单独操作(单个 Pair
),但通常更有用的数字较大。
2.28.1. ⇒ (Pair 构造器)
我们可以使用 Pair
构造器 ⇒
构造 Pair
。我们可以使用 Pair
, 但不是必须的:
> Pair(1 => 2).WHAT
(Pair)
> (1 => 2).WHAT # The same, but shorter
(Pair)
> Pair.new(1, 2).WHAT # Without the "fat arrow"
(Pair)
请注意,只要未引用的文本不包含空格,就允许它作为键: |
> my $a = (pi => pi);
pi => 3.141592653589793
第一个 «pi» 被视为文本文本,第二个 «pi» 被视为内置 pi
常量。
我们可以以几种方式构造 Pair
:
Pair('key' ⇒ 'value') |
如上所述 |
('key' ⇒ 'value') |
同上 |
Pair.new('key' ⇒ 'value') |
同上。这是标准方式 |
'key' ⇒ 'value' |
不需要圆括号 |
:key<value> |
同样 |
:foo(127) |
foo ⇒ 127 的简写 |
:127foo |
与 foo ⇒ 127 相同。当值是数字的时候才有效。 |
如果该值是布尔值,我们可以缩短表达式:
:key |
和 key ⇒ True 相同 |
:!key |
和 key ⇒ False 相同 |
我们可以将任何变量转换为 Pair
,变量名称为键:
> my $age = 14;
> my $p = :$age;
> say $p; # -> age => 10
2.28.2. key
使用 key
获取 Pair
的键值(胖箭头的左侧):
> my $a = (1 => 2); # -> 1 => 2
> $a.key; # -> 1
2.28.3. value
使用 value
获取 Pair
的值(胖箭头的右侧):
> $a.value; # -> 2
2.28.4. antipair
使用 antipair
交换 Pair
的键和值:
> ("a" => "r").antipair; # -> r => a
2.29. Hash
如果 Pair
对象(零个,一个或多个),则哈希是一个集合,其中我们将键用作索引(查找)。我们可以给出一个哈希值,当我们像这样声明它时:
> my %trans = ("a" => "1", "b" => "9");
> my %population = (Oslo => 500_000,
Paris => "unknown",
"Buenos Aires" => "too many");
键(⇒
的左侧)可以不带引号指定 - 如果它们不包含空格的话。
我们可以跳过这个圆括号。
我们可以用列表填充哈希。它将第一个值作为键,第二个值作为值,等等:
> my %a = (11 .. 20)
{11 => 12, 13 => 14, 15 => 16, 17 => 18, 19 => 20}
列表中的项数必须是偶数:
{11 => 12, 13 => 14, 15 => 16, 17 => 18, 19 => 20}
> my %a = (11 .. 21)
Odd number of elements found where hash initializer expected: Found 11 (implicit) elements: ...
可以从两个单独的列表创建哈希,一个用于键,另一个用于值:
> my @keys = 1..10; > my @vals = 91..100;
> my %hash; %hash{@keys} = @vals;
> say %hash;
{1 => 91, 10 => 100, 2 => 92, 3 => 93, 4 => 94, 5 => 95, 6 => 96, 7 => 97, 8 => 98, 9 => 99}
2.30. Hash 构造器 {}
仅当将哈希分配给标量时,才需要哈希构造函器 { … }
:
> my $trans = {"a" => "1", "b" => "9"};
{a => 1, b => 9}
请注意,如果我们忘记了花括号,我们会得到一个 Pair
的列表:
> my $trans = ("a" => "1", "b" => "9");
(a => 1, b => 9)
如果我们将某些内容分配给哈希,则会强制将其强制为 Pair
对象,然后插入到哈希中:
> my %hash = 1..10; # -> {1 => 2, 3 => 4, 5 => 6, 7 => 8, 9 => 10}
只要参数数是偶数:
> my %hash = 1..11;
Odd number of elements found where hash initializer expected: Found 11 (implicit) elements:
Last element seen: 11
in block <unit> at <unknown file> line 1
我们可以使用其它方式生成 Pair
:
my %months = :jan('January'), :feb('February'), ...;
2.31. 哈希赋值和值
如果我们赋值给哈希变量,则任何现有值都将丢失。我们可以添加新值,或更改现有值的值,如下所示:
> %population{"Buenos Aires"} = "too many";
> %population<Oslo> = 500_000;
> %population{"Oslo"}
500000
> %population<Oslo> # The same
500000
> say %population<Buenos Aires> # An error
((Any) (Any))
最后一个等价于:
> say ( %population{"Buenos"}, %population{"Aires"} );
((Any) (Any))
2.32. keys
keys
方法给出(一组)所有键:
for %population.keys -> $city {
say "City $city has %population{$_} people";
}
我们也可以在列表上使用 keys
,它将返回索引:
即使我们用 Pair
对象填充了列表:
(1 => 2, 2 => 3, 4 => 5).keys
(0 1 2)
keys
按随机顺序提供键。如果需要顺序,则排序:
> for %population.keys.sort -> $city { ... }
键返回的顺序是半随机的。只要哈希未更改,此顺序就保持不变(这意味着调用 keys 和 values 以相同的顺序提供值)。
|
2.33. values
values
给出所有值:
for %population.values -> $population {
say "Unknown City with %population{$_} people";
}
请注意,Pair 对象列表上的 values
返回所有内容,因为它使用索引作为键:
> (1 => 2, 2 => 3, 4 => 5).values
(1 => 2 2 => 3 4 => 5)
除了执行手动搜索之外,无法从哈希值开始并返回键。这是典型的"大海捞针问题"。我们将在第 10.13.4.2 节"干草堆问题的针"中了解程序时对此进行了解。
2.34. kv (keys + values)
我们可以使用 kv
方法(对于键值)同时获取键和值:
for %population.kv -> $city, $population {
say "City $city has $population people";
}
2.35. 带类型的 Hash
我们可以在哈希值上添加类型约束,如第 3.1 节"强类型"中所述:
> my Int %h;
> say %h.WHAT; # -> (Hash[Int])
> %h<a> = 12.1;
Type check failed in assignment to %h; expected Int but got Rat (12.1)
也可以像这样指定约束:
> my %h of Int;
我们还可以在哈希的键上添加类型约束:
> my %h{Str};
> say %h.WHAT; # -> (Hash[Any,Str])
我们可以两个都用:
> my Int %h{Str};
> say %h.WHAT; # -> (Hash[Int,Str]) # The first is the keys, the second is the values
现在,我们要求键是字符串,并且值是整数。
2.35.1. keyof
keyof
将返回的键的类型约束。
没有键约束的哈希:
> my %h; say %h.keyof; # -> (Str(Any))
带键约束的哈希:
> my %h{Int}; say %h.keyof; # -> (Int)
2.35.2. of
of
返回的表示值的类型约束。
不限制值的哈希:
> my %h; say %h.of; # -> (Mu)
2.36. Shaped Hash
定型哈希不像定型数组那样有意义(如第 8.17 节"定型数组"中所述)。
我们可以指定这样的形状:
> my %hash{10;10;10};
数字将被忽略,但我们不能使用比指定更多的索引(本例中为三个):
> %hash{"A";"E";"C"}
> %hash{"A";"E";"C"}
> %hash{"A";"E"}
> %hash{"A";"E";"J";"O"} = "A"
Type Str does not support associative indexing.
不过,错误消息有点令人困惑。
另请注意,我们可以为哈希分配任何值,而不仅仅是整数(就好像我们只有指定一个整数;例如,my %hash{10};
- 和是指定整数被视为与 Int 相同)。
大小声明是非常无用的(除了上限),可以删除:
> my %hash;
> %hash{"A"; "B"; "C"} = 12;
> %hash{"A"; "B"; "D"} = 13;
那么,什么是定型的哈希?让我们来看看:
> say %hash; # -> {A => {B => {C => 12, D => 13}}}
它是一种树状结构,哈希哈希的哈希。(我故意保持简单。)
![img]()
我们还可以像这样来指定它:
> my %hash;
> %hash{"A"}{"B"}{"C"} = 12;
> %hash{"A"}{"B"}{"D"} = 13;
> say %hash; # -> {A => {B => {C => 12, D => 13}}}
请注意,当我们处理子树时,细微的差别:
> %hash{"A"; "B"}; # -> ({C => 12, D => 13}) # A list
> %hash{"A"}{"B"}; # -> {C => 12, D => 13} # A hash
2.37. invert
我们可以使用 invert
来交换哈希的键和值:
> my %a = (1 => 2, 3 => 4); # -> {1 => 2, 3 => 4}
> my %b = %a.invert; # -> {2 => 1, 4 => 3}
重复值将变为相同的键,但可以:
> my %a = (1 => 2, 3 => 4, 5 => 2); # -> {1 => 2, 3 => 4, 5 => 2}
> say %a.invert; # -> (2 => 1 2 => 5 4 => 3)
我们得到了 Pair 对象的列表,并且该列表不关心重复项。当我们将其分配给哈希时,重复的键将被压掉,如下所示:
> my %b = %a.invert; # -> {2 => 5, 4 => 3}
当我们将 Pair 对象列表分配给哈希时,当我们有重复的键时,最后一个对象获胜。这是最后一个是任何人的猜测,因为哈希是无序的。(这意味着每次运行程序时,你都会得到不同的结果,从而得到细微的错误。
2.38. antipairs
与在哈希上调用 invert
具有相同的效果。
但是在 Pair
对象列表中,它们有所不同:
(1 => 2, 2 => 3, 4 => 5).antipairs # ((1 => 2) => 0 (2 => 3) => 1 (4 => 5) => 2)
(1 => 2, 2 => 3, 4 => 5).invert # (2 => 1 3 => 2 5 => 4)
antipairs
使用列表索引作为键,使用 Pair
作为值,并交换它们。
2.38.1. pairs
使用 pairs
将散列转换为 Pair
对象列表:
my %a = (1=>2, 2=>3); # -> {1 => 2, 2 => 3}
%a.pairs; # -> (1 => 2 2 => 3)
2.39. 散列切片
就像我们有数组切片一样(请参见第 8.11 节"数组切片"), 我们还有哈希切片:
my %translate = ( one => "ein", two => "zwei", three => "drei" );
# {one => ein, three => drei, two => zwei}
say %translate{"two", "one"}
# (zwei ein)
say %translate<two one> # This does not work on arrays.
# (zwei ein)
如果选择条件更复杂,请使用 grep
:
%translate{%translate.keys.grep(*.chars == 3)}
# (zwei ein)
2.40. 散列查询
我们如何检查哈希中是否存在值?
my %h;
%h<a> = 0; # => 0
%h<b> = False; # => False
%h<c> = Nil; # => (Any)
它们都在布尔上下文中计算为 False
。
使用 :exists
副词:
%h<a>:exists # => True
%h<b>:exists # => True
%h<c>:exists # => True
%h<d>:exists # => False
2.41. 散列删除
使用 :delete
副词从哈希表中删除条目:
my %h = a => 1, b => 2, c => 3
# {a => 1, b => 2, c => 3}
%h<b c>:delete
# (2 3)
%h<a>:delete
# 1
返回一个或多个已删除的值。
2.42. 散列重复值
哈希(显然)不允许使用相同的键重复值:
my %hash;
%hash<M> = 12;
%hash<M> = "nobody";
say %hash<M>
# nobody
但是我们可以通过使用列表作为值来解决这个问题,并为其添加新的值。 我们可以通过 push
自动执行此操作:
my %hash;
%hash<M>.push(12);
say %hash<M>
# [12]
%hash<M>.push("nobody");
say %hash<M>
# [12 nobody]
say %hash<M>[1];
# nobody
但是我们必须从一开始就这样做,因为第一次推送将删除已经存在的所有标量值:
my %hash;
%hash<M> = 12;
%hash<M>.push("nobody");
say %hash<M>
# [nobody]
2.43. 散列用法
我们在本章中显示的第一个代码段是一个哈希,其中我们在城市及其人口规模之间进行了映射。
我们可以尝试添加这些值:
my $total = 0;
for %population.values -> $population {
$total += $population;
}
由于字符串值(“未知”和“太多”),这会导致运行时错误:
Cannot convert string to number: base-10 number must begin with valid digits or '.' in '⏏too many' (indicated by ⏏)
2.43.1. «sum» 方法
我们可以使用 sum
方法而不是遍历值:
my $total = %population.values.sum;
同样,由于非数字值,此操作失败。
2.43.2. 智能匹配
在添加值之前,我们可以使用智能匹配运算符 ~~
(在第 11.3 节" ~~(智能匹配运算符)"中介绍)来检查该值是否为 Int
:
for %population.values -> $population {
$total += $population if $population ~~ Int;
}
我们可以使用 $population.isa(INT) (请参见第3.7.8节"isa"),结果相同。
|
只要类型是 Int
,它们都可以工作。 如果是例如 Rat
,比较将失败-即使值本身是整数:
say 5.0 ~~ Int; # -> False
2.44. grep 和智能匹配
在应用 sum
之前,我们可以使用 grep
摆脱非法值(和循环):
my $total = %population.values.grep(* ~~ Int).sum;
我们将在"高级 Raku"课程中回到该示例。
2.45. 哈希(方法)
只要元素数量为偶数,我们就可以使用 Hash
方法将列表转换为哈希:
my %hash = (1..10).Hash;
# {1 => 2, 3 => 4, 5 => 6, 7 => 8, 9 => 10}
练习 9.1
如果我们删除 .Hash
部分会发生什么?
例如:
my %hash = (1..10);
练习 9.2
如果有重复值会发生什么?
例如:
<1 2 1 3 1 4 1 5>.Hash;
2.46. but(散列)
在第 8.26 节 "but(Array)"中,我们显示了 but
运算符不适用于数组(@
),但不适用于标量列表/数组($
)。
同样适用于哈希,但无效:
my %a = (54 => 12) does False; # -> {54 => 12}
say %a.^name; # -> Hash
say so %a: # -> True
这有效,但仅适用于整个数据结构(这是 Pair
对象的集合):
my $a = (54 => 12) does False; # -> 54 => 12
say $a.^name; # -> Pair+{<anon|5>}
say so $a; # -> False
say $a<54>; # -> 12
say so $a<54>; # -> True
2.46.1. does(散列)
行为与 but
相同。 有关详细信息,请参见第 8.26 节"but(Array)"。
== 过程
过程是编写可维护代码的传统基本构建块,可将大型程序分解为较小的单元。 (对象定向是另一种方法,我们将在第17章,类中重新讨论。)
2.47. 不带参数的过程
定义这样的无参数过程:
sub hello {
say "Hello";
}
hello; # Call it like this,
hello(); # or this.
2.48. 带参数的过程
具有明确的参数列表:
sub add ($first-value, $second-value) {
return $first-value + $second-value;
}
say add(1, 2); # Call it like this,
say add 1, 2; # or this.
注意优先级,并使用圆括号避免混淆:
say add(1, 2), 3; # -> 33
say add 1, 2, 3;
===SORRY!=== Error while compiling:
Calling add(Int, Int, Int) will never work with declared signature ($first-value, $second-value)
如果我们尝试使用文本字符串怎么办?
my $result = add "10", 2; # -> 12
字符串"10"被强制为数字(10),并且可以使用。
之所以有效,是因为"10"可以转换为数字。
但是,如果尝试某些无法转换的操作,则会出现运行时错误:
> my $result = add "ten", 2;
Cannot convert string to number: base-10 number must begin with valid digits or '.' in '⏏ten' (indicated by ⏏)
in sub add at <unknown file> line 1
2.49. @_
我们可以使用没有签名的程序。 传递的所有参数都可以在 @_
变量中找到:
sub test { .say for @_; }
test; # Nil
test 1, 2, 3;
# 1
# 2
# 3
如果我们具有过程签名,则 @_ 不可用:
|
> sub test ($arg) { .say for @_; }
Placeholder variable '@_' cannot override existing signature ------> sub⏏ test ($al) { .say for @_; }
@_
将列表弄平,但不会散列。 小心甚至更好:请勿使用。
2.49.1. $_
我们可以使用 $_
作为过程变量:
sub x ($_) { .say; .say; } # 12
x(12) # 12
2.50. 占位符变量
我们在第 8.13.1 节"占位符变量"中介绍了占位符变量。 它们可以与任何过程一起使用:
sub test {
say "Argument 1: $^a"; # -> Argument 1: A
say "Argument 2: $^b"; # -> Argument 2: B
}
test("A", "B");
当我们使用占位符变量时,它们就会存在。 它们是按字母顺序而不是第一次使用的顺序分配给值的。
sub test {
say "Argument 2: $^b"; # -> Argument 2: B
say "Argument 1: $^a"; # -> Argument 1: A
}
test("A", "B");
另一个例子:
(1 .. 10).map({ $^a - $^b }); # -> (-1 -1 -1 -1 -1)
(1 .. 10).map({ $^b - $^a }); # -> (1 1 1 1 1)
这两行都包含两个和元素。 第一行采用第一个元素,然后减去第二个元素,得出 1-2、3-4、5-6、7-8 和 9-10。 第二行将顺序更改为 2-1、4-3、6-5、8-7 和 10-9。
请注意,在将占位符变量存在后,可以将它们称为普通变量(不带 ^ )。
|
{ say $^a ~ $^b; say $b ~ $a; }("P","6")
# P6
# 6P
sub test {
say "Argument 1: $^a";
say "Argument 2: $^b";
say "Normal variables: $a $b";
}
test("A", "B");
2.50.1. 命名占位符变量
我们可以使用命名占位符变量。 在符号和名称之间指定一个冒号:
sub test {
say "Argument 1: $:first"; # -> Argument 1: 12
say "Argument 2: $:second"; # -> Argument 2: 23
}
test(first => 12, second => 23);
2.51. 过程作为变量
我们可以将对过程的引用存储在变量中,然后在以后执行它:
my &code = sub { say "12345"; }; # sub { }
&code(); # 12345
或在标量中(但不是):
my $code = sub { say "12345"; } # sub { }
$code() # 12345
2.51.1. anon
上面的匿名过程没有任何参数,可以很好地工作。 如果要引入参数,则必须将其命名为:
my &code = sub something($arg) { say $arg ~ "123"; }
这给我们起了个名字,我们可以用它来调用过程(以正常方式)。 我们可以通过使用 anon sub
来防止这种情况:
> my &code = anon sub something($arg) { say $arg ~ "123"; }
> something;
===SORRY!=== Error while compiling:
Undeclared routine: something used at line 1
请注意,如果你删除赋值,你将获得不可调用的匿名函数。
2.52. 类型约束
我们可以使用类型约束来防止将字符串自动转换为数字(并获得编译时错误):
sub add (Numeric $first-value, Numeric $second-value) {
return $first-value + $second-value;
}
say add "10", 2;
$ raku num-add-err
===SORRY!=== Error while compiling ./num-add-err
Calling add(Str, Int) will never work with declared signature (Numeric $first-value,
Numeric $second-value) at ./num-add-err:7 ------> say ⏏add "10", 2;
请注意,Numeric
还允许使用未定义的值:
sub add (Numeric $first-value, Numeric $second-value) {
return $first-value + $second-value;
}
my Numeric $a; my Numeric $b;
say add $a, $b;
$ raku num-add-err2
Use of uninitialized value of type Numeric in numeric context
in sub add at ./num-add-err2 line 3
Use of uninitialized value of type Numeric in numeric context
in sub add at ./num-add-err2 line 3 0
解决方案是添加 :D
副词(请参见第3.5节":D(定义副词)"):
sub add (Numeric:D $first-value, Numeric:D $second-value) {
return $first-value + $second-value;
}
my Numeric $a; my Numeric $b;
say add $a, $b;
$ raku num-add-err3
Parameter '$first-value' of routine 'add' must be an object instance of type 'Numeric', not a type object of type 'Numeric'. Did you forget a '.new'?
in sub add at ./num-add3 line 3
in block <unit> at ./num-add3 line 10
类型约束提供了更好的错误消息,并且在我们运行任何代码之前在编译时完成了检查(因此我们可以避免在可能导致文件系统或数据库混乱的中间程序崩溃)。 |
2.53. return
使用 return
停止执行当前过程或方法,并将指定的值(如果有,否则为 Nil
)提供给调用方(作为返回值)。
请注意,如果我们已经设置了相关的 Phasers(请参阅《高级Raku》课程),它们将在控制权返回给调用者之前运行。
如果我们指定了返回值约束(请参见第 10.7.2 节"返回值约束"),将对照该值进行检查。 如果检查失败,则会引发异常。
请注意,return 是作为过程而不是关键字实现的,因此过程优先级规则适用。
|
2.53.1. return-rw
return
会返回一个值,而不是一个容器,并且你不能更改该值。
这将失败:
sub abc { return 123; }
say ++abc();
但是我们可以使用 return-rw
(如《返回读写》中所述)来获取可以更改的容器。
这也会失败:
sub abc { return-rw 123; }
say ++abc(); # -> 124
由于 return-rw
尝试返回容器,因此失败,并且由于没有容器,它返回值 123
。 这是完全合法的,但是前缀 ++
将失败。
sub abc {
my $a = 123;
return-rw $a;
}
say ++abc(); # -> 124
$ raku return-rw
124
这允许将过程调用用作匿名变量。 在实践中它可能不是很有用。
2.53.2. 返回值约束
如第 10.6 节"类型约束"所示,我们可以对返回值以及输入值进行类型约束。
我们可以通过几种方式指定返回类型约束:
sub X ($a, $b --> Int) { $a + $b }
sub X ($a, $b ) returns Int { $a + $b }
sub X ($a, $b ) of Int { $a + $b }
my Int sub X ($a, $b) { $a + $b }
如果尝试返回不符合限制的值,则会得到异常。
可以返回一个明确的值:
sub random( --> 12 ) { rand }
say random; # -> 12
即使存在返回类型约束,我们也可以将 Nil
作为错误值(或“我放弃”)返回:
sub abc ( --> Int) { Nil; }
abc; # -> Nil
即使我们坚持定义的返回值:
sub abc ( --> Int:D) { Nil; }
abc; # -> Nil
2.54. @*ARGS
我们可以使用动态变量 @*ARGS
从命令行获取输入:
say "Hello, @*ARGS[0]!";
@*ARGS
是带有参数的列表,其中 @*ARGS[0]
是第一个参数,依此类推。
> raku hello-args NPW
Hello, NPW
占位符变量在这里会很好,但是由于我们没有过程,因此无法使用。
2.55. MAIN
我们可以使用特殊的 MAIN
过程并声明过程参数,而不是访问 @*ARGS
:
sub MAIN ($name) {
say "Hello, $name!";
}
编译器将首先执行程序中的任何代码,然后再调用 MAIN
例程。 在 MAIN
之外具有任何代码通常不是一个好主意。
使用所需的名称和所需的名称声明 MAIN
。 如果你给程序提供错误数量(或类型)的参数,程序将失败并显示用法消息:
$ raku hello
Usage:
hello <name>
$ raku hello all!
hello, all!
$ raku hello all you
Usage:
hello <name>
2.55.1. unit 过程
我们可以改用 unit procedure
,为我们节省一个块级:
unit sub MAIN ($name);
say "Hello, $name!";
当我们只有一个过程时,这很有用,因为我们通常使用 MAIN
写简短程序。
另一方面,我们获得的是块级数量的减少,这在短程序中通常不是问题。
练习 10.1 重写第 8.27 节"将所有内容组合在一起"中的名称测验程序,以便你可以指定任意名称和缺少的字母,如下所示:
$ name-quiz2 "B??TO? ??N?R" AMNOOS
$ name-quiz2 L??DON NO
由于名称中包含空格字符,因此第一行中名称的引号是必需的。
2.55.2. 更好的用法信息
程序名称和变量名称可能无法全部说明。 在 MAIN
过程上方添加一个特殊的注释行:
#| Person to greet
sub MAIN ($name) {
say "Hello, $name!";
}
$ raku hello-usage all you
Usage:
hello-usage <name> -- Person to greet
特殊注释也可以在 MAIN
过程之后指定:
sub MAIN ($name) {
say "Hello, $name!";
}
#= Person to greet
--doc
如果使用 #|
表格,当我们要求编译器提供文档时,注释将显示:
$ raku --doc hello-usage
sub MAIN(
$name,
)
Person to greet
在第二种形式上执行此操作(过程后为 #=
)不起作用:
$ raku --doc hello-usage2
2.56. WHY
我们也可以为普通过程添加这样的注释,并使用 WHY
来获取它们:
#| This is one
sub a1 { ; }
#| This is two
sub a2 { ; }
#= Still two
sub a3 { ; }
say &a1.WHY;
say &a2.WHY;
say &a3.WHY;
$ raku usage
This is one
This is two
Still two
No documentation available for type 'Sub'.
Perhaps it can be found at https://docs.raku.org/type/Sub
请注意,我们必须在过程名称前加上 &
前缀,以免执行过程,并对返回值应用 WHY
。
实际上,我们可以对任何块执行此操作,只要它具有名称或指向它的指针即可。
块注释是内联文档子语言 pod 的一部分。 我们会回到"高级 Raku"课程。
|
WHY
是从一个世界到另一个世界的桥梁…
2.56.1. 带类型的 MAIN
我们可以在传递给 MAIN
的参数中添加强类型,但是存在与第 6.6.1 节"prompt"中描述的相同问题,即字符串不带引号,因此编译器假定它们是字符串。 不带引号的数字可以是数字-或不带引号的字符串。
有了 prompt
,我们可以通过用引号指定数字来强制编译器将其视为字符串来避免这种情况,但这在 shell 中不起作用,因为 shell 使用引号对文本进行分组并在将内容传递给程序之前将其删除 。
空格用于分隔参数,如果要在字符串中加空格,则必须用引号引起来。
say "Argument: «{ $_ }»" for @*ARGS;
并运行它:
$ raku args 123 "456 789" '10 11 12' 13
Argument: «123»
Argument: «456 789»
Argument: «10 11 12»
Argument: «13»
两种类型(单精度和双精度)的 shell 均使用引号。
那么,我们如何指定引号,以使 shell 不用理会引号,然后将其发送给程序?
我们可以尝试同时使用单引号和双引号:
$ raku args "'456'" '"10"'
Argument: «'456'»
Argument: «"10"»
那行得通。 至少对于 «bash»,我正在使用的 shell。 其他 shell 可能会有所不同。
练习 10.2 编写一个程序,显示输入的类型,以及该类型的对象(或值)可用的方法的排序列表(按字母顺序)。
例如:
$ raku type-methods "3+4i"
3+4i (of type ComplexStr) supports: (abs ACCEPTS acos acosec acosech acosh acotan acotanh asec asech asin asinh atan atan2 atanh base base-repeating Bool Bridge BUILDALL ceiling cis Complex conj cos cosec cosech cosh cotan cotanh denominator DUMP exp FatRat floor gist Int is-prime isNaN log log10 narrow new norm nude Num numerator Numeric raku polymod pred rand Range Rat Real roots round sec sech sign sin sinh sqrt Str succ tan tanh truncate unpolar WHICH)
提示:请以例如 Int 类型开始并在 REPL 中试用。
我们获得的输入类型(如第 6.6.2.1 节"Str 继承树"中所示)是:
普通类型 |
输入类型 |
Int |
IntStr |
Num |
NumStr |
Rat |
RatStr |
Complex |
ComplexStr |
2.56.2. 签名
显示的方法例如 IntStr
有一些重复项。 它们是由继承引起的,其中几个继承的类具有自己的版本。 实际上,这是不对列表进行排序的一个很好的理由。
这是一个例子:
say IntStr.^methods[1].^name; # -> Int
say IntStr.^methods[30].^name; # -> Int
现在,我们有没有办法判断一个方法属于哪个类?
签名方法为我们提供了答案,上下颠倒:
say IntStr.^methods[1].signature; # -> (IntStr:D: *%_)
say IntStr.^methods[30].signature; # -> (Int: *%_ --> Int)
第一个显然属于 IntStr
类,第二个显然属于 Int
类。
2.57. IntStr 问题
这看起来很好, 对吗?
multi MAIN (Int $number) {
say "Integer: $number";
}
multi MAIN (Str $string) {
say "String: $string";
}
运行它:
$ raku intstr-gotcha qwwe
String: qwwe
但是整型不起作用:
$ raku intstr-gotcha 12
Ambiguous call to 'MAIN(IntStr)'; these signatures all match: :(Int $number)
:(Str $string)
in block <unit> at content/code/intstr-gotcha line 8
问题在于我们得到了 IntStr
类型,并且由于它继承自 Int
和 Str
,因此我们无法在 «MAIN» 候选对象之间进行选择。
练习 10.3 解决这个问题。
2.58. 多重分派
我们可以使用 multi
关键字指定具有不同参数列表(或"签名")的过程的不同版本:
multi sub do-something ($file1) { ... }
multi sub do-something ($file1, $file2) { ... }
如果需要,可以在指定 multi 时跳过 sub 。
|
使用类型约束:
multi add (Numeric $value1, Numeric $value2) { ... }
multi add (Str $value1, Str $value2) { ... }
2.58.1. 存根运算符
我们可以指定占位符代码(也称为"存根"或«Yada,yada,yada»运算符,而不是普通代码。
该代码将编译,但是如果我们尝试执行它,将会抱怨:
代码 |
动作 |
… |
fail |
!!! |
die |
??? |
warn |
它们将在«高级 Raku课程» 中介绍。
yada
我们可以使用 yada
检查过程是否存在存根:
sub a { 1; }; say &a.yada; # -> False;
sub b { ... }; say &b.yada; # -> True;
sub c { !!! }; say &c.yada; # -> True;
sub d { ??? }; say &d.yada; # -> True;
2.58.2. 斐波那契数
这是前 10 个斐波那契数字:«1、2、3、5、8、13、21、34、55»。
第一个值是 1,第二个也是 1,其后每个值是两个先前值的和。
我选择显示以 1 开头的数字的版本。还有另一个以 0 开头的版本(索引已关闭)。 "给我第三个斐波那契数"的答案是 1 或 2。 |
这是一些打印给定斐波那契数的程序。
使用循环:
sub MAIN (Int $n) {
say fibonacci $n;
}
sub fibonacci (Int $n) {
return 1 if $n == 1 or $n == 2;
my @fib = (1, 1);
for 2 .. $n -1 -> $i {
@fib[$i] = @fib[$i -1] + @fib[$i -2]
}
return @fib.tail;
}
我们可以使用递归(一个反复调用自身的过程):
sub MAIN (Int $n) {
say fibonacci $n;
}
sub fibonacci (Int $n) {
return 1 if $n == 1 or $n == 2;
return fibonacci($n-1) + fibonacci($n-2)
}
更短,实际上比循环版本更容易理解。
我们可以使用 multi
来排除前两个值:
sub MAIN (Int $n) {
say fibonacci $n;
}
multi fibonacci (1) { 1 }
multi fibonacci (2) { 1 }
multi fibonacci (Int $n where $n > 2) {
fibonacci($n - 2) + fibonacci($n - 1)
}
那么我们可以使用斐波那契数来做什么呢? 除了炫耀我们的数学知识和 Raku 的力量外,也没什么。
(我们将在第 16.3.1 节"斐波那契数列"中展示更多。)
递归版本比循环版本慢。 我们将展示这一点,并在第15.5节"计时斐波那契"中说明原因。 |
«高级 Raku»课程中详细解释了多次调度。 在那里,我们介绍了 proto
,以及将执行推迟到其他 proto
候选者的方法。
2.59. 过程参数
默认情况下,传递给过程的值是只读的:
sub increment ($value) {
$value++;
return $value;
}
my $number = increment(12);
$ raku increment
Cannot resolve caller postfix:<++>(Int);
the following candidates match the type
but require mutable arguments:
(Mu:D $a is rw)
(Int:D $a is rw)
2.59.1. is rw
该错误消息给出了一个提示:is rw
(«is read write»)。 这是一个特征,我们可以将其添加到参数中。
让我们尝试:
sub increment ($value is rw) {
$value++;
return $value;
}
my $number = increment(12);
$ raku increment2
Parameter '$value' expected a writable container,
but got Int value
in sub increment at increment2 line 3
in block <unit> at increment2 line 9
而且这也失败了。 问题是 is rw
告诉过程它可以更改调用代码中的变量,但是我们使用一个值对其进行了调用。 并且值不能更改:
> 12 = 13;
Cannot modify an immutable Int (12)
in block <unit> at <unknown file> line 1
这有作用:
my $value = 12;
my $result = increment($value);
say $value; # -> 13
但是,应该避免过程调用会在自身之外更改变量值而没有赋值的副作用。
如果你尝试调整传递的值,它将再次失败:
my $result = increment($value + 1);
2.59.2. is copy
is copy
特性是更简单的证明。 你将获得一个实变量,并将值的副本传递给它,并且可以随意更改它(副本)。
sub increment ($value is copy) {
$value++;
return $value;
}
say increment(12);
$ raku increment3
13
2.59.3. 可选参数
可以为参数指定默认值,使其可选:
sub do-something ($value, $optional = "") { ... }
我们可以拥有更多:
sub do-something-else ($value, $optional1 = 5, $optional2 = $value * 2) { ... }
不能为 $optional2
分配值,而不能为 $optional1
分配值。
do-something-else(11, 101);
2.59.4. 命名参数
一个带有很多参数的过程可能是一个问题。 迟早有人会弄错参数的顺序。
通过在变量列表中以 :
(冒号)作为前缀来指定命名实参,可以消除该问题,因为现在顺序无关紧要了:
sub aaa (:$a, :$b) { return 2*$a + $b; }
say aaa(a => 2, b => 3); # -> 7
say aaa(b => 3, a => 2); # -> 7
命名参数使得可以具有许多可选参数,并且你可以根据需要使用任意数量的变量,而不必考虑顺序:
sub bbb (:$a = 12, :$b = 13, :$c = 12, :$d = 13 )
aaa(a => 1);
aaa(d => 3, a => 4);
你可以混合使用普通(或位置)和命名参数,但位置参数必须排在首位:
sub ccc ($a, $b, :$c, :$b) { ... }
命名参数可能会让你想起 Pair 语法(请参见第9.1节"Pair")。 这不是巧合,因为它们确实是 Pair 。
|
具名参数简写
使用好的变量名可能会导致以下情况:
sub cost (:$height, :$width) {
return $height * $width * 4;
}
my $height = 12;
my $width = 512;
say cost(height => $height, width => $width);
只要变量名相同,我们可以将其缩短为:
say cost(:$height, :$width);
运行它们:
$ raku named
24576
$ raku named2
24576
大海捞针问题
让我们回顾第9.6节"值"中介绍的"大海捞针问题",从哈希中找到给定值的键。
这需要蛮力:
sub find-value-in-hash (%hash, $value, :$all = False, :$verbose = False) {
say "Looking for $value:";
for %hash.kv -> $key, $val {
say "- Checking $key" if $verbose;
if $val eq $value {
say "- Found it: $key -> $val";
last unless $all;
}
}
}
my %haystack = ( A => "Bike", Q => "Beetle", "#" => "Book", 12 => "Needle",
17 => "Frog", 29 => "DVD player (defective)", 76 => "Bike");
find-value-in-hash(%haystack, "Beetle");
find-value-in-hash(%haystack, "Bike", :verbose);
find-value-in-hash(%haystack, "Beetle");
find-value-in-hash(%haystack, "Bike", :all);
我在这里使用了命名可选参数。
并运行它:
$ raku haystack
Looking for Beetle:
- Found it: Q -> Beetle
Looking for Bike:
- Checking 17
- Checking 12
- Checking 29
- Checking Q
- Checking A
- Found it: A -> Bike
Looking for Beetle:
- Found it: Q -> Beetle
Looking for Bike:
- Found it: A -> Bike
- Found it: 76 -> Bike
2.59.5. 命名强制参数
我们可以使用 is required
特征将一个命名参数强制为必须参数, 或使用 !
简写形式:
sub ccc ($a, $b, :$c!, :$d is required) { ... }
这给出了一个很好的错误消息:
> ccc(1, 2);
Required named parameter 'c' not passed in sub ccc at ...
默认值对于强制性参数毫无意义。 但是,编译器不会提出抗议。 所以 :$d is required = False
是合法的,即使默认值是无用的也是如此。
2.59.6. 副词
在过程调用中指定命名参数时,可以使用其他副词语法:
sub aa (:$a, :$b) { say "A: $a B: $b"; }
aa(a => 1, b => 2); # -> A: 1 B: 2
aa(:a(1), :b(2)); # -> A: 1 B: 2
aa(:1a, :2b); # -> A: 1 B: 2
aa(a => "r", b => "h"); # -> A: r B: h
aa(:a<r> , :b<h>); # -> A: r B: h
副词也可以与内置功能一起使用。
到目前为止,这些命名参数都已取值。 没有任何其他约束,也没有参数值,命名参数是布尔值。 没有值(没有约束)的副词形式为 True
(因为这是 Pairs 所做的):
aa(:a, :b); # -> A: True B: True
副词名称的前面的 !
使它成为 False
值:
aa(:a, :!b); # -> A: True B: False
我们可以使用 `%_` 来允许任何命名参数(就像我们可以使用 `@_` 来允许任何东西一样):
sub named { say %_ }
named( name => 'Tom' ); # -> {name => Tom}
named( name => 'Phil', age => 12 ); # -> {age => 12, name => Phil}
named; # -> {}
我们可以结合命名和位置:
sub both { say @_; say %_; }
both( 12, 13, name => "Tom", age => 45 );
both( 12, name => "Tom", age => 45, "19C" );
它们可以被混合,主要是增加了混乱。
2.60. *
(Slurpy 运算符)
如果我们尝试将标量传递给需要数组的过程,则会收到错误消息:
> sub a (@values) { say "ok"; }
> a(1,2,3,4,5);
===SORRY!=== Error while compiling:
Calling a(Int, Int, Int, Int, Int) will never work with declared signature (@values) ------> <BOL>⏏a(1,2,3,4,5);
我们可以通过使用一个稀疏(或“可变参数”)数组纠正过程签名,以将所有剩余的标量值作为列表获取。 在列表参数之前添加 `*`: *@values
:
sub a (*@values) { say "ok"; }
a(1,2,3,4,5); # ok
通过确保始终使用列表作为参数来调用该过程,可以避免此问题,但是不适用于在命令行上传递的参数(因为它们始终是标量值)。 有关详细信息,请参见下一部分。
或者,我们可以删除签名,并在过程主体中访问 @_
。
2.60.1. Slurpy MAIN
在命令行中传递的参数始终是标量,但是我们可以使用 Slurpy 数组通过在 list 参数之前添加 `*` 来捕获所有参数:*@words
:
sub MAIN (*@words) {
@words.grep({ .contains("a") }).say;
}
运行它:
$ raku words absn kakak alala 9099 00 00
(absn kakak alala)
$ raku words
()
吞噬参数允许零参数,这不是一件好事。
我们可以强迫它至少要求一个参数:
sub MAIN ($word1, *@words) { ... }
祝你解释代码好运…
使用类型约束给出不言自明的代码:
#| One or more words to search for lines with the letter 'a'
sub MAIN (*@words where @words.elems >= 1) {
@words.grep({ .contains("a") }).say;
}
(而且添加明确的使用注释当然也没有什么坏处。)
contains
做你心中所想; 检查左侧的字符串是否包含右侧的字符串。 查看有关详细信息,请参见第 11.4.2 节"contains(部分字符串)"。
我们可以稍微完善一下 where
条件:
sub MAIN (*@words where so @words)
2.60.2. 再谈随机素数
让我们回顾一下在练习 8.3 中编写的随机素数代码:
(1 .. 100).grep(*.is-prime).pick.say; # -> 13
(1 .. 1000).grep(*.is-prime).pick.say; # -> 1861
我们可以将其重写为程序,以上限为参数:
sub MAIN ($upper-limit) {
(1 .. $upper-limit).grep(*.is-prime).pick.say;
}
$ raku random-prime aaaaa
Cannot convert string to number: base-10 number must
begin with valid digits or '.' in '⏏aaa' (indicated by ⏏)
in sub MAIN at random-prime line 3
in block <unit> at random-prime line 3
如果在输入参数中添加 Int
约束,则会得到更好的错误消息:
sub MAIN (Int $upper-limit)
$ raku random-prime2 aaaaa
Usage:
random-prime2 <upper-limit>
Int
约束确保仅允许整数。 但是负整数呢?
$ raku random-prime2 -100
Nil
不,素数都是正数。
如果你不知道,REPL 可以为你提供帮助:
> (-17).is-prime
False
(1 .. -100)
构造将尝试生成从 1 到 -100 的整数。 那是不可能的,所以返回 Nil
(一个空列表)。 然后,我们从一个空列表中选择一个随机值,得到 Nil
。
如果你听说过序列(例如 1 … -100
); 是的,他们会在这里工作的,不,这没关系。 稍后我们将介绍序列。
最好将负输入值设为非法,这是可能的。
我们可以添加一个约束:
sub MAIN (Int $upper-limit where * > 0) {
(1 .. $upper-limit).grep(*.is-prime).pick.say;
}
$ raku random-prime3 -100
Usage:
random-prime2 <upper-limit>
Raku 的 UInt 类型为 «Unsigned Int»,我们当然可以使用。
|
2.60.3. 更好的错误信息
用法消息并没有真正告知我们合法值。 例如我们可以将变量重命名为 $upper-limit-as-a-positive-integer-larger-than-zero
。
但是谁愿意输入这样的变量名呢? 我们可以在用法消息中添加文本,如下所示:
#| A random prime number between 1 and ...
sub MAIN (Int $upper-limit where * > 0) {
(1 .. $upper-limit).grep(*.is-prime).pick.say;
}
将用法行放置在过程之前。
$ raku random-prime4 -100
Usage:
random-prime2 <upper-limit> -- A random prime \
number between 1 and the given integer upper limit
练习 10.4
随机素数程序有什么问题?
重写程序以避免此问题。
2.61. 块重新访问
我们在第 4.1 节"块"中介绍了块,并将其定义为:«块是被视为整体的代码的集合。 一对大括号内设置了块。»
我们可以将它们赋值给变量,然后执行它们:
my $block = { "Hello, $_."; };
say $block("Thomas"); # Hello, Thomas.
在这里,我们有一个参数,传入 $_
。
我们可以使用占位符变量(如第10.4节"占位符变量"中所述),就像过程。 如果我们要传递不止一个参数,则它们很有用:
my $block = { "Hello, $^a $^b."; };
say $block("Thomas", "Mann"); # Hello, Thomas Mann.
2.61.1. →
我们还可以将命名变量中的参数传递给块(与过程一样),→
和块之间有签名:
my $add = -> $a, $b = 2 { $a + $b };
say $add(40); # 42
可选参数(具有默认值)起作用,如上所示($b = 2
)。
过程本质上是命名块。 但它们还有一些额外的花哨,主要是多重分派(请参见 10.12,"多重分派“)。 还有更好的语法。
2.61.2. <→ / is rw
默认情况下,变量是只读的,但是我们可以使用 is rw
副词使它们变为读写:
my $swap = -> $a is rw, $b is rw { ($a, $b) = ($b, $a) };
my ($a, $b) = (2, 4);
$swap($a, $b);
say $a; # -> 4
或者我们可以使用双向箭头 <→
代替单向箭头,但是这会将所有参数变为读写。
my $swap = <-> $a, $b { ($a, $b) = ($b, $a) };
2.62. 调用在变量中指定的过程
我们可以调用一个在变量中具有名称的过程:
sub AAA { say "ok"; }
my $sub = "AAA";
&::($sub)(); # -> ok
使用调度表是一个更好的解决方案:
sub aaa {
say "12345";
}
sub bbb {
say "FOOBAR";
}
my %table;
%table{"a"} = &aaa();
%table{"b"} = &bbb();
&(%table<a>); # Execute "aaa"
my $p = "b";
&(%table{$p}); # Execute "bbb"
也可以调用存储在变量中的方法。 请参见第17.21节"调用变量中指定的方法"。 |
2.63. 过程中的过程
可以将过程定义放在另一个过程中。 结果是内部过程仅在外部过程内部可见(在范围内),并且只能由该过程调用。
sub A {
say "I am A";
B;
}
sub B {
say "I am B";
}
A;
B;
sub A {
say "I am A";
B;
}
sub B {
say "I am B";
}
A;
B; # This is line 15
左边的一个起作用:
$ raku procedure-procedure
I am A
I am B
I am B
但是正确的代码在程序的最后一行给出了编译错误:
$ raku procedure-procedure2
===SORRY!=== Error while compiling procedure-procedure2 Undeclared name:
B used at line 15
3. 正则表达式介绍
正则表达式是一种子语言,具有自己的语法和规则…
有些人在遇到问题时会想:“我知道,我会使用正则表达式。”现在,他们有两个问题了。 — http://regex.info/blog/2006-09-15/247
处理此“问题”的一种方法是首先避免使用正则表达式。
本章介绍一些典型的正则表达式,然后介绍存在它们的非正则表达式替代方案。非正则表达式的替代品通常要快得多。
矛盾的是,非正则表达式替代品非常容易替换几乎不容易搞砸的正则表达式,而实际上可以使用替代品的更高级的正则表达式显然没有它们。
如果你在本章中使用了非正则表达式版本,那么在将来某个时候实际需要制作正则表达式时,你将处于不利地位。
Raku 更喜欢名称 «Regex»(和复数 «Regexes»)而不是 «Regular Expressions»,因为它们早已与 «regular» 起源不同。你可能会看到两个名字。 (建议使用名称“模式”和“规则”,但并没有流行。你可能会在较早的博客文章中碰到它们。)
3.1. 什么是正则表达式?
正则表达式是一种与表达式匹配的方式(可以通过多种方式进行解释),而不是静态文本。
我们可以从第10.9节 "MAIN" 中的 "hello" 程序开始。
sub MAIN ($name) {
say "Hello, $name!";
}
我们的任务是对其进行调整,以使消息因名称而异:
-
如果名称是 «Steve», «Neve» 或 «Barry»,我们打印 «Go away, <name>!»。
-
如果名称的长度为5个字符,中间的字符为元音,则我们打印 "Hello, <name>. Whatsup?»
-
对于所有其他名称,我们打印 «Hello, <name>»
sub MAIN ($name) {
if $name eq "Steve" or $name eq "Neve" or $name eq "Barry" {
say "Go away, $name!";
}
elsif $name.chars == 5 and ($name.substr(2,1) eq "a"
or $name.substr(2,1) eq "e"
or $name.substr(2,1) eq "i"
or $name.substr(2,1) eq "o"
or $name.substr(2,1) eq "u")
{
say "Hello, $name. Whatsup?";
} else {
say "Hello, $name!";
}
}
到目前为止,一切都很好。 很多代码,但是可以用。 但是,如果我们决定只坚持名称中的字母(一个5个字符的字母)怎么办?
我们可以将其重写为:
elsif $name ~~ /^ \w\w <[aeiou]> \w\w $/
-
«/» 字符标记正则表达式的开始和结束,默认情况下,空格和换行符都会被忽略。 «^»字符绑定到字符串的开头,«$» 绑定到字符串的结尾。
-
«\w» 匹配一个»单词字符»,它或多或少是我们想要的(一个字母)。
-
«<[aeiou]>» 是一个字符组,我们匹配其中给定的字符之一。
当你掌握 Regex 的知识时,它会更紧凑,更容易理解。
3.2. 制作正则表达式
生成正则表达式的最简单方法是将其放在两个 /
中:
/abc/
/12345/
独立的正则表达式(像这样)与 $_
(主题变量)匹配:
> $_ = "abc"; say so /abc/; # -> True
> say so /abcd/; # -> False
我们可以使用 given
(参见章节4.12 "given") 为我们设置 $_
:
> say so /abc/ given "abc"; # -> True
3.3. ~~ (智能匹配运算符)
«Smartmatch 运算符» ~~ 是 Regexes 的基础。 我们可以使用它来比较几乎所有内容和几乎所有其他内容(而不仅仅是 Regex)。
如果我们与 $_
匹配(如上一节所述),则可以删除 ~~
。 这些都是等价的:
> $_ ~~ m/1234/;
> $_ ~~ /1234/;
> /1234/;
3.3.1. m/…/
我们在正则表达式中添加 m
前缀(用于 «match»),可以将斜杠(/
)与其他任何字符互换。 如果我们在正则表达式本身中有一个斜杠,这将很有用:
> m|/usr/bin/|;
我们可以使用带有开始和结束版本的字符:
> m{123};
3.3.2. !~ (否定智能匹配运算符)
使用 «否定的智能匹配运算符» !~ 反转匹配。
3.4. 部分字符串
我们可以检查给定的字符串是否包含另一个字符串:
> say so "12345" ~~ /23/; # -> True
> say so "12345" ~~ /33/; # -> False
我们可以跳过右侧的斜线,但这会改变含义:
> say so "12345" ~~ "23"; # -> False
现在,我们正在智能匹配两个字符串,这与普通字符串比较相同,因为它们不相等,因此返回 False
。
我们可以将左侧放在变量中:
> my $val = "12345";
> say so $val ~~ /23/; # -> True
我们可以使用正则表达式执行相同的操作:
> my $b = /23/;
> say so "12345" ~~ $b; # -> True
> say $b.WHAT; # -> (Regex)
我们可以将一个类型进行比较:
> say so "12345" ~~ Int; # -> False
> say so 12345 ~~ Int; # -> True
3.4.1. Regex(类型)
我们可以使用类型系统:
> my Regex $b = /23/;
3.4.2. contains(部分字符串)
部分字符串是如此有用,以至于我们有专门的 contains
函数:
> "12345".contains("23"); # -> True
> "12345".contains("33"); # -> False
3.4.3. index(部分字符串)
使用 index
获取一个字符串在另一个字符串中的位置。 如果找到,它将返回索引,否则返回 NIL
。
> "12345".index("1"); # -> 0
> "12345".index("0"); # -> Nil
第一个字符的位置(或偏移量)为 0,因此请注意返回值:
"12345".index("1").defined; # -> True
"12345".index("0").defined; # -> False
so "12345".index("1"); # -> False
so "12345".index("0"); # -> False
TIPS: 如果你不需要该职位,请使用 contains
。 然后,你不必担心定义性。
3.4.4. rindex(部分字符串)
rindex
与 index
类似, 但是从右侧搜索, 给出上一次匹配的位置:
> "121212121".index(1); # -> 0
> "121212121".rindex(1); # -> 8
3.4.5. indices(部分字符串)
indices
类似于 index
,但是搜索在另一个字符串中出现的所有字符串。 如果找不到,它将返回一个空列表。
> say "banana".indices("a"); # -> (1 3 5)
> say "banana".indices("ana"); # -> (1)
> say "banana".indices("ana", 2); # -> (3)
> say "banana".indices("b"); # -> (0)
> say "banana".indices("X"); # -> ()
如果指定了可选参数 :overlap
,则搜索将从字符串中的下一个位置继续,而不是默认情况下在匹配之后:
> say "banana".indices("ana"); # -> (1)
> say "banana".indices("ana", :overlap); # -> (1 3)
> say "aaaaaaaaaa".indices("aaa"); # -> (0 3 6)
> say "aaaaaaaaaa".indices("aaa", :overlap); # -> (0 1 2 3 4 5 6 7)
3.5. 字符串的开头或结尾
上一节中的正则表达式将匹配,无论在字符串中的何处找到它。 我们可以使用锚来强制匹配仅考虑字符串的开始,结束或两者。
锚点 |
例子 |
描述 |
^ |
/^123/ |
只匹配字符串的开头 |
$ |
/345$/ |
只匹配字符串的结尾 |
3.5.1. starts-with(部分字符串)
从字符串的开头匹配:
> say so "12345" ~~ /^23/; # -> False
> say so "12345" ~~ /^123/; # -> True
我们可以使用 starts-with
代替:
> "12345".starts-with("23"); # -> False
> "12345".starts-with("123"); # -> True
3.5.2. ends-with(部分字符串)
从字符串的末尾匹配:
> say so "12345" ~~ /123$/; # -> False
> say so "12345" ~~ /45$/; # -> True
我们可以使用 ends-with
代替:
> "12345".ends-with("123"); # -> False
> "12345".ends-with("45"); # -> True
3.5.3. equal
我们可以使用两个锚,这与使用 eq
的普通字符串比较具有相同的意义(因为我们还没有使用过任何Regex特殊字符):
> say so "12345" ~~ /^12345$/; # -> True
> "12345" eq "12345"; # -> True
或者甚至:
> say "12345" ~~ "12345"; # -> True
3.6. 正则表达式元字符
在正则表达式中,按字母顺序输入字母数字字符(字母和数字)和下划线(_),忽略空格,其他所有字符都是具有特殊含义的元字符。
Regexes中的空格和换行符默认情况下会被忽略,因此可以随意添加它们以增强可读性。
我们可以通过引号(带反斜杠)来获得文字元字符:
> say so "12/34" ~~ /2\/3/; # -> True
3.7. $/(Match 对象)
$/
对象保存上一次正则表达式匹配的结果(如果没有匹配,则为 Nil
):
> "12345" ~~ /12/; say $/; # -> 「12」
> "12345" ~~ /67/; say $/; # -> Nil
请注意有趣的尖括号。 当我们打印匹配对象时使用它们,以提醒我们 $/
是匹配对象,而不是字符串。
使用显式字符串化:
> say $/.Str; # -> 12
> say ~$/; # -> 12
请注意,put
(请参见第6.3.3节"put 与 say")会进行字符串化,并隐藏了问题:
> "12345" ~~ /12/; put $/; # -> 12
将匹配对象传递给期望字符串的代码可能会导致错误。 如果有问题的代码通过 Nativecall 使用外部库,则该程序几乎肯定会崩溃。
Nativecall 将在"高级 Raku"课程中详细介绍。
3.8. 特殊字符
.
(单个点)只匹配一个字符:
> say so "12345" ~~ /1.3.5/; # -> True
我们可以在任何字符后添加一个量词:
量词 |
贪婪性 |
描述 |
? |
No |
匹配零次或一次 |
+ |
Yes |
匹配一次或多次 |
* |
Yes |
匹配零次、一次或多次 |
** nnumber |
No |
精确匹配 «number» 次 |
** min..max |
Yes |
最少匹配 «min» 次, 最多匹配 «max» 次 |
注意,某些量词(如所示)使匹配贪婪。 只要它能够匹配表达式,它就会尽可能匹配:
> say so "111111111111112345" ~~ /1+2345/; # -> True
> say so "111111111111112345" ~~ /1+2345/; # -> True
> say so "12345" ~~ /123459/; # -> False
> say so "12345" ~~ /123459*/; # -> True
> say so "011111111111111234" ~~ /01 ** 1..20 234/; # -> True
> say so "011111111111111234" ~~ /01 ** 1..10 234/; # -> False
3.9. 捕获和分组
到目前为止,我们仅展示了如何匹配(或不匹配),但是我们可以将匹配分为几个部分。
3.9.1. ()(捕获)
我们可以使用圆括号来捕获匹配项,并稍后将它们引用为 $0
,$1
(依此类推):
> "12345" ~~ /(2)(.4)/;
> say $0.Str; # -> 2
> say $1.Str; # -> 34
我们可以使用 match 对象代替 $0
,$1
(依此类推):
> say $/
「234」
0 => 「2」
1 => 「34」
我们可以查找单个匹配项:
> say $/[1].Str; # -> 23456
这也可以:
> say $[1].Str; # -> 23456
3.9.2. 捕获编号
圆括号对儿从零开始从左到右编号:
> say "0: $0; 1: $1" if 'abc' ~~ /(a) b (c)/; # -> 0: a; 1: c
捕获可以嵌套,并根据级别编号:
if 'abc' ~~ / ( a (.) (.) ) / {
say "Outer: $0"; # -> Outer: abc
say "Inner: $0[0] and $0[1]"; # -> Inner: b and c
}
匹配对象:
> say $/;
「abc」
0 => 「abc」
0 => 「b」
1 => 「c」
3.9.3. [](非捕获分组)
如果我们不需要捕获,可以跳过它。 我们的 Regex 需要它们进行分组,但是我们可以改用非捕获括号:
> my $valid-ipv4 = /^ [\d ** 1..3] ** 4 % '.' $/;
不捕获的好处:
-
匹配对象不会被未使用的东西弄乱
-
比捕获更快
3.9.4. <()>(捕获标记)
当我们有一个匹配项时,无论我们使用了多少个捕获(如果有),匹配对象都会包含整个字符串。
通过使用 <(
和/或 )>
标记,我们可以防止部分匹配最终出现在 match 对象中:
标记 |
描述 |
<( |
不捕获该标记之前的东西 |
)> |
不捕获该标记之后的东西 |
say 'abc' ~~ / a <( b )> c/; # -> 「b」
say 'abc' ~~ / a ( b ) c/; # -> 「abc」; 0 => 「b」
使用捕获标记时,有可能在匹配之前和之后获得字符串的一部分。 在 match 对象上使用 prematch
和/或 postmatch
方法:
> say 'abc' ~~ / a <( b )> c/; # -> 「b」
> say $/.prematch; # -> a
> say $/.postmatch; # -> c
这些方法返回字符串(而不是匹配的对象)。
无论捕获标记如何,我们也可以获取原始字符串:
> say 'abc' ~~ / a <( b )> c/; # -> 「b」
> say $/.orig; # -> abc
> say $/.target # -> abc
orig
返回一个对象,而 target
返回它的字符串化版本。 当我们从字符串('abc')开始时,它们都在此处返回字符串。
还有其他可用于匹配对象的方法。 有关详细信息,请参见 https://docs.raku.org/type/Match#Methods。
3.10. 字符类
我们可以使用字符类来匹配不同种类的字符。
我们在以下定义了前导反斜杠(称为«反斜杠字符类»):
字符类 |
匹配 |
否定 |
\n |
换行符(查看6.1章换行中的 $?NL) |
\N |
\t |
制表符 |
\T |
\h |
水平空白符 |
\H |
\v |
垂直空白符 |
\V |
\s |
空白符(水平或垂直) |
\S |
\d |
数字(包括 unicode 数字) |
\D |
\w |
单词字符;字母,数字或下划线(包括 unicode 字母和数字) |
\W |
除非与量词结合使用,否则它们将完全匹配一个字符。 否定的版本匹配所有内容-除了普通版本。
\s
匹配换行符和空白符:
> say so "abc abf" ~~ /\s/; # -> True;
> say so "abcXabf" ~~ /\s/; # -> False;
> say so "abcXabf\n" ~~ /\s/; # -> True;
我们还有更冗长的命名字符类。 最有用的是:
字符类 |
别名 |
描述 |
<alnum> |
\w |
<alpha> 加上 <digit> |
<alpha> |
字母字符,包括下划线 |
|
<blank> |
\h |
水平空白符 |
<cntrl> |
控制字符 |
|
<digit> |
\d |
十进制数 |
<graph> |
<alnum> 加上 <punct> |
|
<lower> |
<:Ll> |
小写字符 |
<print> |
<graph> 加上 <space>, 但是不含 <cntrl> |
|
<space> |
\s |
空白符 |
<upper> |
<:Lu> |
大写字符 |
<xdigit> |
十进制数字 [0-9A-Fa-f] |
我们没有显示仅代表字母的字符类。 我们在 Unicode 类别中有几种可以使用的类别。 最有用的(在很长的列表中)是:
短 |
长 |
描述 |
<:L> |
<:Letter> |
|
<:Ll> |
<:Lowercase_Letter> |
|
<:Lu> |
<:Uppercase_Letter> |
|
<:N> |
<:Number> |
匹配数字, unicode 数字和 «1⁄2» 这样的东西。 |
<:P> |
<:Punctuation> 或 <:punct> |
|
<:S> |
<:Symbol> |
|
<:Sc> |
<:Currency_Symbol> |
匹配 «£», «$», «€» 和其它货币符号。 |
> "1234sksjsjsjs1919" ~~ /(<:N>+)/; # $0 -> 「1234」
> "1234sksjsjsjs1919" ~~ /(<:N>+)(<:L>)/; # $0 -> 「1234」, $1 -> 「s」
仅计算字符串中的字符数:
sub MAIN ($string) {
my $count = $string.comb.grep(* ~~ /<:L>/).elems;
say "The string contains $count letters.";
}
$ raku letter-count 12Aw
The string contains 2 letters.
$ raku letter-count 12Aw#@ß
The string contains 3 letters
请注意,我们可以缩短选择位:
my $count = $string.comb.grep(/<:L>/).elems;
关于 Unicode 的进一步阅读:
我们可以通过在 :
(冒号)和类名之间的插入来 !
(感叹号)否定它们:
say so "1234sksjsjsjs1919" ~~ /<:!L>/; # -> True
say so "abcdefghijklmnopq" ~~ /<:!L>/; # -> False
我们可以组合几个 Unicode 类别。 在尖括号里面用 +
来将它们加起来(作为集合并集),或用 -
减去右侧(差集):
say so "1234sksjsjsjs1919" ~~ /<:!L-:N>/; # -> False
say so "1234sksjsjsjs1919." ~~ /<:!L-:N>/; # -> True
第一个检查不是字母(!L
)的所有字符,然后删除数字。 那让我们一无所有。 第二个相同,但是只剩下一个句点(.
)。
3.10.1. uniprop
使用 uniprop
可以显示给定字符串中第一个字符的 Unicode 类别:
> "a".uniprop; # -> Ll
> "A".uniprop; # -> Lu
> "ß".uniprop; # -> Ll
> '$'.uniprop; # -> Sc
我们可以检查特定的属性:
say 'a'.uniprop('Alphabetic'); # -> True
3.10.2. uniprops
使用 uniprops
获取字符串中每个字符的值:
"Fix 10!".uniprops; # -> (Lu Ll Ll Zs Nd Nd Po)
"Fix 10!".uniprops("Letter"); # -> (1 1 1 0 0 0 0)
3.11. 自定义字符类
我们可以使用 <[
和 ]>
指定我们自己的字符类:
"abcdefghijklmn" ~~ /(<[fed]>+)/; # $0 -> 「def」
注意字符类本身只匹配一个字符。
我们可以否定字符类:
"abcdefghijklmn" ~~ /(<-[fed]>+)/; # $0 -> 「abc」
我们可以使用 -
组合字符类(与 Unicode 属性一样), 并使用范围:
"1234567890" ~~ /(<[1..9] - [5]>+)/; # $0 -> 「1234」
3.12. 非贪婪
我们已经证明某些正则表达式量词是贪婪的,因为它们尽可能地匹配。 我们可以通过在贪婪量词后面添加 ?
使它们变得非贪婪(或节俭):
"12345A" ~~ /(\d*?)/; # $0 -> 「」 # zero or more gives zero.
"12345A" ~~ /(\d+?)/; # $0 -> 「1」 # one or more gives zero.
"12345A" ~~ /(\d+?)A/; # $0 -> 「12345」
如上例所示,非贪婪仅适用于向右方向。 我们仍然从头开始匹配(如果可能的话),并获取所有数字。
3.12.1. 用法
我们可以用一个简单的(如愚蠢的)html 解析器来说明这一点。 我们要提取图像标签:
"AAA <img src='12.png' alt='High Noon'> BBB" ~~ /(\<img\s.*\>)/;
# 0 => 「<img src='12.png' alt='High Noon'>」
我添加了 \s
,以便我们不匹配另一个名称相似的标签。 (不应该有任何蜜蜂,但是通常通过将 html 标签重命名为未使用的标签来注释掉 html 标签。浏览器会忽略未知标签,我们也应该这样做。)
这看起来很有希望。 但是 .*
是贪婪的,如果有更多的话,它将一直持续到行中的最后一个 >
:
"AAA <img src='12.png' alt='High Noon'> BBB <b>CCC</b> DDD" ~~ /(\<img\s.*\>)/;
# 0 => 「<img src='12.png' alt='High Noon'> BBB <b>CCC</b>」
非贪婪:
"AAA <img src='12.png' alt='High Noon'> BBB <b>CCC</b>" ~~ /(\<img\s.*?\>)/;
#「<img src='12.png' alt='High Noon'>」
3.13. 向后引用
可以在正则表达式中引用我们已经与 $0
,$1
等匹配的东西。
«img» 标签没有结束标签,例如 «b»。 我们可以尝试编写一个通用的匹配任何标签的正则表达式,并一直持续到匹配结束标签为止。
"This is <b>bold <em>and cursive</em> and not</b>." ~~ /\<(.*?)\>(.*)\<\/$0\>/
#「<b>bold <em>and cursive</em> and not</b>」
# 0 => 「b」
# 1 => 「bold <em>and cursive</em> and not」
这个正则表达式有一个问题。问题是什么?
3.13.1. :ignorecase / :i
我们可以用 :ignorecase
(和缩写形式 :i
) 副词指定不区分大小写的匹配:
say so "abcdefghijkl" ~~ /EFG/; # -> False
say so "abcdefghijkl" ~~ /:i EFG/; # -> True
3.14. 使用正则表达式
正则表达式通常以斜杠开头和结束。 例如 /abc/
。 (我们将在«高级Raku»课程中介绍其他方法。)
我们仅看到与字符串匹配的正则表达式。 但是它们也可以用来更改我们应用它们的字符串。
我们可以通过不同的方式将正则表达式应用于字符串:
函数 |
方法 |
描述 |
查看章节 |
m/…/ |
匹配 $_ |
11.3.1, m/…/ |
|
rx/…/ |
Regex 对象 |
||
/…/ |
match |
Regex 对象( |
11.2, 制造正则表达式 |
s/…/…/ |
就地替换 |
11.15, 字符串替换 |
|
S/…/…/ |
subst |
非破坏性替换 |
11.15.2 subst(字符串替换) 和 11.15.3 S/…/…/(字符串替换) |
tr/…/…/ |
就地翻译 |
11.17, 翻译 |
|
TR/…/…/ |
trans |
非破坏性翻译 |
11.17.2 trans(翻译) 和 11.17.4, TR/…/…/(翻译) |
除了第三个定界符(/…/
)外,我们都可以对它们使用任何定界符(而不是 /
)。 请注意,打开和关闭字符的版本(例如 {
和 [
)仅适用于匹配项。
替换和翻译使用三个斜杠,但第三个应为斜杠并不清楚。
`match` 方法在"高级 Raku"课程中介绍。
3.15. 字符串替换
使用字符串替换将一个字符序列替换为另一字符序列。
3.15.1. s/…/…/(字符串替换)
s/…/…/
运算符在左侧变量上进行更改:
my $s = "one two three four";
$s ~~ s/two/zero/;
say $s; # -> one zero three four
默认情况下,替换只发生一次。
my $s = "one one one one";
$s ~~ s/one/zero/;
say $s; # -> zero one one one
3.15.2. :global / :g
我们可以指定 :global
(或 :g
简短形式)副词来尽可能多地进行替换:
my $s = "one one one one";
$s ~~ s:g/one/zero/;
say $s; # -> zero zero zero zero
它不能递归工作:
my $s = "111111111111";
$s ~~ s:g/11/1/;
say $s; # -> 111111
我们的字符计数程序的另一种形式:
sub MAIN ($string is copy) {
$string ~~ s:g/<-:L>+//;
say "The string contains { $string.chars } letters.";
}
我们删除所有不是字母的内容,然后计算剩余的内容。
3.15.3. subst(字符串替换)
subst
返回调用字符串,其中第一个字符串被第二个字符串替换(如果找不到匹配项,则返回原始字符串)。
my $s = "one two three four";
my $t = $s.subst("two", "zero");
say $s; # -> one two three four
say $t; # -> one zero three four
它不会更改被调用的字符串或变量,因此我们可以这样做:
my $t = "one two three four".subst("two", "zero");
除非使用 :g
(全局)副词,否则替换仅执行一次:
"1010101010101020202020202020".subst("10", "X")
# X10101010101020202020202020
"1010101010101020202020202020".subst(:g, "10", "X")
# XXXXXXX20202020202020
如果要更改我们所调用的变量,请把新值赋值回来:
$variable .= subst($replace, $with);
3.15.4. S/…/…/(字符串替换)
s/…/…/
更改在其上使用的字符串。 如果要保持字符串不变,请改用 S/…/…/
:
$_ = "one two three four";
my $t = S/two/zero/;
say $t; # -> one zero three four
say $_; # -> one two three four
你不能将智能匹配与 S/…/…/
运算符一起使用(甚至不能在 $_
上显式使用)。
但是我们可以使用 given
隐式设置 $_
(请参见第4.12节"given"):
my $t = S/two/zero/ given "one two three four";
3.15.5. 替换调优
我们可以用 :x
副词指定要替换的数量:
# targeted substitution. Number of times to substitute. Returns back unmodified.
$str.subst(/foo/, "no subst", :x(0));
$str.subst(/foo/, "bar", :x(1)); #replace just the first occurrence.
我们可以用 :nth
副词指定要替换的匹配项:
$str.subst(/foo/, "bar", :nth(3)); # replace nth match alone. Replaces the third foo.
# Returns Hey foo foo bar
3.16. 副词
正则表达式副词)我们可以使用副词来更改正则表达式的工作方式:
副词 |
短形式 |
On |
描述 |
:continue |
:c |
M |
从哪里开始搜索 |
:exhaustive |
:ex |
M |
所有可能的匹配,包括重叠匹配 |
:global |
:g |
M |
所有匹配, 不仅仅是第一个。查看 11.15.1.1, ":global/:g" |
:ignorecase |
:i |
R |
查看 11.13.1,":ignorecase/:i" |
:ignoremark |
:m |
R |
只比较基字符串。见下文。 |
:overlap |
:ov |
M |
和 |
:pos |
:p |
M |
从指定位置(子串索)锚定匹配 |
:ratchet |
:r |
R |
不回溯 |
:samecase |
:ii |
S |
匹配时忽略大小写,但将其应用于替换。 见下文。 |
:samemark |
:mm |
S |
和 |
:samespace |
:ss |
S |
和 |
:sigspace |
:s |
R |
使空白有意义。 见下文。 |
«On» 列意味着:
-
M - 仅用于匹配
-
R - 用于所有正则表达式
-
S - 仅用于替换
«参见下文»没有显示的副词未在本书中显示。 有关详细信息,请参见 https://docs.raku.org/language/regexes#Adverbs。
3.16.1. :ignoremark / :m
正则表达式:仅比较基字符,忽略重音符号:
say so /:ignoremark abc/ given "åbc"; # -> True
say so /:ignoremark abc/ given "øbc"; # -> False
say so /:ignoremark obc/ given "øbc"; # -> True
3.16.2. :samemark / :mm
仅替换:和 :ignoremark
一样,并将重音应用于替换。
say S:samemark:global/a/o/ given "åbäcà"; # -> o̊böcò
3.16.3. :samecase / :ii
仅替换:匹配时忽略大小写,但将其应用于替换。 因此,«abc» 和 «Abc» 将与 «abc»,«Abc»,«ABC» 等匹配。 替换字符串的大小写与匹配中指定的大小写相同:
say S:samecase/abc/def/ given "xABCxabcABx"; # -> xDEFxabcABx
与常规替换比较:
say S/abc/def/ given "xABCxabcABx"; # -> xABCxdefABx
3.16.4. :sigspace / :s
正则表达式: 让空白有意义。
say so "abc abc" ~~ /abc abc/; # -> False (as «/abcabc/» does not match)
say so "abc abc" ~~ /:sigspace abc abc/;
请注意,第一个空格(在副词和正则表达式中的第一个 «a» 字母之间)用作分隔符,将被忽略。
3.16.5. :samespace /:ss
仅替换:和 :sigspace
一样,并将空白替换。
say S:samespace/a ./c d/ given "a b"; # -> c d
say S:samespace/a ./c d/ given "a\tb"; # -> c\td
3.17. 翻译
翻译是用一个字符替换另一个字符的过程。
3.17.1. tr/…/…/(翻译)
tr/…/…/
运算符在左侧变量上进行更改:
my $s = "1234567890";
$s ~~ tr/129/ABx/; # -> AB345678x0
如果我们没有指定足够的替换字符,则使用最后一个(重用):
my $s = "1234567890";
$s ~~ tr/129/A/; # -> AA345678A0
我们还可以移除字符:
my $s = "1234567890";
$s ~~ tr:delete/129//; # -> 3456780
3.17.2. trans(翻译)
使用 trans
将一个字符换成另一个,指定一个 Pair
:
say "abcabc".trans("a" => "1"); # -> 1bc1bc
我们可以同时进行多个翻译:
say "abcabc".trans("a" => "1", "b" => "9"); # -> 19c19c
我们 还可以使用散列:
my %trans = ("a" => "1", "b" => "9");
say "abcabc".trans(%trans); # -> 19c19c
也可以使用范围:
"secret text".trans( ['a' .. 'z'] => ['b' .. 'z', 'a'] );
3.17.3. Rotate 13
最古老的著名加密算法是"Rotate 13",从罗马帝国就知道了。 相同的功能进行加密和解密,因为(罗马)字母中有26个字符。 (罗马人没有"j"和"v", 因此他们只有 24 个字符,但我们将忽略这一历史性异常。) 实现«rotate13»。 仅处理 a-z 和 A-Z。 其他所有字符均保持不变。 把字符串 "Hello, raku programmers" 翻译为 "Uryyb,enxh cebtenzzref!" 反之亦然。
3.17.4. TR/…/…/(翻译)
tr/…/…/
运算符更改其使用的字符串。 如果要保持字符串不变,请使用 TR/…/…/
代替:
$_ = "one two three four";
my $t = TR/oe/xx/;
say $t; # -> xnx twx thrxx fxur
say $_; # -> one two three four
3.18. trim/trim-leading/trim-trailing
删除前导和/或尾随空格是一项常见任务。 因此 Raku 具有以下函数:
函数 |
描述 |
trim |
移除前导和尾部空格 |
trim-leading |
移除前导空格 |
trim-trailing |
移除尾部空格 |
say "X" ~ " 123 " ~ "X"; # -> X 123 X
say "X" ~ " 123 ".trim ~ "X"; # -> X123X
say "X" ~ " 123 ".trim-trailing ~ "X"; # -> X 123X
say "X" ~ " 123 ".trim-leading ~ "X"; # -> X123 X
我已将它们用作方法,但它们也可以用作函数。
写一个 trim-leading, trim-trailing 和 trim 的正则表达式版本:
方法 |
正则表达式用法 |
$y = $x.trim-leading |
$x ~~ /XXXX/; $y = $0.Str; |
$y = $x.trim-trailing |
$x ~~ /XXXX/; $y = $0.Str; |
$y = $x.trim |
$x ~~ /XXXX/; $y = $0.Str; |
3.19. split 和 grep
请注意,split
(请参见第7.3节,“ split”)和 grep
(请参见第8.20.1节,"grep")可以使用正则表达式作为参数(而不是普通字符串):
my @words = $text.split(/\s/);
这解决了我们在第 7.3 节 "split" 中指出的多空格问题。
任何两位数字,其中第二个是 «2»:
(1..100).grep: /^(\d)2$/; # -> (12 22 32 42 52 62 72 82 92)
3.20. 注释
我们可以在正则表达式中放置注释。 不建议使用内联注释,因为建议你使用换行符。
代替:
"12345" ~~ /(2)(.4)/;
像这样写:
"12345" ~~ /(2) # A literal "2"
(.4) # Any character followed by a literal "4"
/;
4. 模块
模块是一种有用的封装技术,可以将较大的任务分解为更易于实现的较小部分,从而使整个事情成为可能。
如果有人编写了一个模块来满足你的需求或其中的一部分,请使用该模块,而不要自己重新设计轮子。 这样可以节省你的时间和精力。
模块的质量和成熟度相差很多,其中一些可能不再维护。 如果有更多选择,使用哪个模块本身就是一本书的主题。 与使用设计不良的模块相比,最好自己编写代码。
4.1. 预编译
模块在安装时进行编译。 当程序使用模块时,将加载预编译的版本-这样可以加快程序的编译速度。
无法预编译程序,但是你可以将代码(或至少其中的大部分)移至一个或多个模块。 你应该对较大的应用程序执行此操作,但不要因为启动速度有所提高(或多或少是虚构的)。 |
4.2. 用 zef 管理模块
模块,管理)你需要一个模块管理器来安装,列出,更新和删除模块。 zef
是应该使用的唯一模块管理器。
不会维护旧的模块管理器 panda ,因此不应使用它。
|
4.2.1. zef list
使用 zef list --installed
命令获取已安装模块的列表。
这是一个非常简短的列表:
$ zef list --installed
===> Found via /usr/local/share/perl6/site ①
App::Mi6:ver<0.2.2>:auth<cpan:SKAJI> ②
Bailador:ver<0.0.15>:auth<github:Bailador> ③
Linenoise:ver<0.1.1>:auth<Rob Hoelz> ④
Shell::Command ⑤
p6doc:ver<1.002001> ⑥
zef:ver<0.5.3>:auth<github:ugexe>:api<0> ⑦
① zef告诉我们它在哪里找到模块。 ② 具有版本(«ver»)和作者(«auth»)且带有单个冒号前缀的模块。 作者是 CPAN 用户名。 ③ 如上所述,但作者是 github 项目名称。 ④ 作者为文本字符串。 ⑤ 此作者暂无作者或版本。 ⑥ 此作者暂无作者。 ⑦ 作者是 GitHub 用户名。 注意新标签 «api»。 我们将在"高级 Raku"课程中进行讨论。
模块可能具有版本(:ver )和作者(:auth )部分。 这样就可以安装一个模块的多个版本,你可以选择使用哪个(或更多)版本。 (有关详细信息,请参见第12.3节"使用模块(use)"。)
|
4.2.2. CPAN vs GitHub
Raku 模块最初仅在 GitHub 上托管,但在 2018 年添加了对 CPAN(《综合 Perl 存档网络》)的支持。
GitHub 是一台服务器,Raku 社区在脱机时遭受了损失。 CPAN 是站点的分布式网络,因此单个服务器上的问题不会影响模块存储库。
zef
支持 GitHub 和 CPAN。 请注意,模块名称中的 "auth" 字段只是一个文本字段,因此即使 "auth" 字段使用 «github:»,也可以将模块托管在 CPAN 上。
4.2.3. zef search
使用 zef search
可搜索名称或描述中具有给定字符串的模块。
例如。 zef search www
:
输出相当宽,但这是最后一个:
字段 |
描述 |
值 |
ID |
只是一个内部计数器 |
4 |
From |
来自于哪个仓库 |
Zef::Repository::Ecosystems<p6c> |
Package |
包(模块)的名字 |
WWW:ver<1.005003> |
Description |
短的描述 |
带有 JSON 解码器的简单的 HTTPS 客户端 |
请注意,可用模块的本地列表将首先更新。 也可以使用 zef update
手动完成此步骤。
如果你以 root 用户身份安装了 Raku 和 zef,则可能必须在 zef 命令前加上 sudo 前缀:sudo zef …
|
4.2.4. zef install
使用 zef install
来安装模块。 如果测试通过,它将下载指定的模块,运行测试并安装它。 如果模块具有未安装的依赖项(其他模块),则将首先安装它们。
例如。 zef install WWW
:
$ zef install WWW
===> Searching for: WWW
它从更新可用模块的本地列表开始:
===> Updating cpan mirror: https://raw.githubusercontent.com/ugexe/Perl6- ecosystems/master/cpan1.json
===> Updating p6c mirror: http://ecosystem-api.p6c.org/projects1.json
===> Updated cpan mirror: https://raw.githubusercontent.com/ugexe/Perl6- ecosystems/master/cpan1.json
===> Updated p6c mirror: http://ecosystem-api.p6c.org/projects1.json
然后,它以递归方式检查依赖关系:
===> Searching for missing dependencies: HTTP::UserAgent, IO::Socket::SSL
===> Searching for missing dependencies: DateTime::Parse, Encode, IO::Capture::Simple, Test::Util::ServerPort, OpenSSL
然后,它为两个不遵循规则的模块发出警告:
===> Extraction: Failed to find a META6.json file for Encode:ver<0.0.2>:auth<github:sergot> -- failure is likely
===> Extraction: Failed to find a META6.json file for IO::Capture::Simple -- failure is likely
然后测试所有模块:
===> Testing: DateTime::Parse:ver<0.9.1>
===> Testing [OK] for DateTime::Parse:ver<0.9.1>
===> Testing: Encode:ver<0.0.2>:auth<github:sergot>
===> Testing [OK] for Encode:ver<0.0.2>:auth<github:sergot>
...
最后安装它们:
===> Installing: DateTime::Parse:ver<0.9.1>
===> Installing: Encode:ver<0.0.2>:auth<github:sergot>
===> Installing: OpenSSL:ver<0.1.21>:auth<github:sergot>
===> Installing: IO::Socket::SSL:ver<0.0.1>:auth<github:sergot>
===> Installing: IO::Capture::Simple
===> Installing: Test::Util::ServerPort:ver<0.0.1>:auth<github:jonathanstowe>
===> Installing: HTTP::UserAgent:ver<1.1.46>:auth<github:sergot>
===> Installing: WWW:ver<1.005003>
请注意,某些模块具有许多依赖关系,而这些依赖关系可能具有自己的依赖关系。 如果这些依赖项之一具有不通过的测试,则不会安装任何内容。
测试失败并不一定意味着所涉及的模块已损坏,因为Raku一直在不断发展,并且模块作者可能无法跟上所有更改。 但这也可能恰恰意味着该模块已损坏。 |
即使测试失败,也可以强制安装:
zef install --force WWW
但是,不知道测试为什么失败,这不是一个好主意。
4.2.5. zef depends
使用 zef depends
可获取给定模块的依赖项列表。 该列表是递归的,即它遵循依赖关系的依赖关系,依此类推。
4.2.6. zef upgrade
使用 zef upgrade
可以升级到指定模块的最新版本。 请注意,此功能是 beta。
如果不带参数使用,它将尝试升级所有已安装的模块。
4.2.7. zef uninstall
使用 zef uninstall
来删除已安装的模块。
请注意,它只会删除你要的内容。 最初由于以下原因安装的所有模块, 依赖关系不会受到影响。
4.2.8. Web 搜索
zef search
不是浏览模块的最佳方法。 但是我们可以使用 Raku Modules 网站 https://modules.raku.org/
练习 12.1 单击某些标签以获得结构感。
做一些搜索。
4.3. 使用模块(use)
我们告诉程序我们将使用带有 use
关键字的模块:
use DBIish; # Top level namespace
use My::Module; # Two levels of namespaces
如果我们未指定版本,则 Raku 将加载最新版本-如果安装了多个版本。 (“最新”的概念仅基于版本号,因此,如果你有两个具有相同名称(由 «auth» 字段区分)的合法模块,则将使用版本号最高的模块。 模块(使用 zef upgrade
)可能会更改最新版本。)
如果要确保使用模块的特定版本,请按以下方式指定它:
use DBIish:ver<0.5.17>;
坚持使用未安装的特定版本将失败:
use DBIish:ver<0.5.18>;
Could not find DBIish:ver<0.5.18> at line 1 in:
/home/arne/.perl6
/usr/local/share/perl6/site
/usr/local/share/perl6/vendor
/usr/local/share/perl6
CompUnit::Repository::AbsolutePath«94631019616016»
CompUnit::Repository::NQP«94631041192904»
CompUnit::Repository::Perl5«94631041192864»
in any statement_control at /usr/local/share/nqp/lib/Perl6/Grammar.moarvm line 1
这对于显示已安装模块的位置列表很有帮助。
«CompUnit::Repository» 主要处理预编译。 有关更多信息,请参见《高级 Raku》课程。
你可以检查是否使用 REPL 安装了模块: |
未安装:
> use Data::TextOrBinary
Could not find Data::TextOrBinary at line 1 in: ...
安装了:
> use WWW
Nil
或在命令行上:
$ raku -MData::TextOrBinary -e "say 'ok';"
===SORRY!===
Could not find Data::TextOrBinary at line 1 in: ...
$ raku -MWWW -e "say 'ok';"
ok
你可以删除 -e
部分,但是如果安装了模块,它将为你提供REPL模式。 你可以使用以下模块:
$ raku -MWWW
>
练习 12.2 从 CPAN 安装模块 «Math::Trig», 并使用其中的内容编写一个简短的程序。
4.4. 编写模块
在第 15 章,编写模块中,我们将展示如何在本地放置模块,而无需使用 zef
安装模块。
在"高级 Raku" 课程中,我们将展示如何遵循规则编写模块,以便可以将其上传到 CPAN 以供 zef
使用并进行公共安装。
== 文件和目录
4.5. 输入输出 - IO
在类似 Unix 的系统上,我们具有以下预定义的文件句柄:
名称 |
文件句柄 |
描述 |
STDIN |
$*IN |
标准输入 |
STDOUT |
$*OUT |
标准输出 |
STDERR |
$*ERR |
标准错误 |
4.5.1. note
note
打印到 STDERR(与 $*ERR.say
本质上相同)。 不要在文件句柄上使用它!
Without \n |
With \n |
To |
Stringification |
note |
$*ERR |
.gist |
在 REPL 中 note
可能会让你吃惊:
> note False
False
True
False
输出已发送到 STDERR。 由于该代码未在 STDOUT 上显示任何内容,因此 REPL 显示了最后一条语句的结果。 note
成功(因为我们尚未关闭 STDERR),并返回 True
。
4.6. 读取文件
读取文件的正常方法是先打开文件,读取内容,然后关闭文件。 我们当然可以这样做,但是我们不必这样做。
4.6.1. IO.lines
使用文件名上的 IO.lines
来获取内容(作为懒惰的行列表)。
读取指定的文件,并显示其中带有 «a» 的行:
sub MAIN ($file-name) {
for $file-name.IO.lines -> $line {
say $line if $line.contains("a");
}
}
读取文件的全部内容后,隐式文件句柄将关闭。 因此,请提防循环过早退出的情况(例如,程序在文件中查找某个字符串,并在找到该字符串时执行 last 操作)。
|
当文件句柄超出范围时,它们将自动关闭,但是在大型程序中,范围的结束可能还有很长的路要走。
我们可以使用 grep
(并把 conntains
移动到那里)来避免循环:
sub MAIN ($file-name) {
.say for $file-name.IO.lines.grep( *.contains("a") );
}
我已经使用循环在显示的每一行之后获取换行符,但是我们也可以通过使用列表上的 join
来避免这种情况:
sub MAIN ($file-name) {
$file-name.IO.lines.grep( *.contains("a") ).join("\n").say;
}
请记住,如果第一个参数是 "Whatever Star",我们可以(实际上必须)忽略 grep 参数中的花括号。
|
我们可以这样写:grep({.contains("a")})
。
花一点时间看一下这三个版本。 其中哪一个最容易理解? 如果你更喜欢另一个,为什么?
4.6.2. limit
如果只对文件的特定部分感兴趣,请指定最大行数,如下所示:
> $file-name.IO.lines(10);
4.6.3. IO.words
这与 IO.lines
相同,但是内容一次返回一个单词(而不是一次返回一行)。
请注意,如第7.4节"words"中所述,单词可能无法满足你的要求。
4.6.4. lines
在上一节中,我们已经使用 lines
作为 IO
对象上的方法,但是我们也可以将 lines
用作没有参数的过程。 这将读取在命令行上指定的文件的内容:
.say for lines;
不需要 MAIN
,它将处理尽可能多的文件:
$ raku echo-all /etc/*
如果一个或多个文件是目录,我们将收到警告:
'NPW18/' is a directory, cannot do '.open' on a directory in block <unit> at echo-all line 3
请注意,像这样使用时,我们无法获取文件名,也无法获取一个文件的结尾而下一个文件的开头。 (但请参阅第13.3.3.1节"$*ARGFILES"以获取解决方法。)
如果我们在不带参数的情况下调用该程序,它将等待输入,并逐字复制回该程序。 使用 <Control-c>
退出。
我们可以根据需要将输入传递给它,这些行是相等的:
$ raku echo-all file1.txt file2.txt
$ cat file.txt file2.txt | raku echo-all
我们可以使 echo-file-contains3
程序更短:
lines.grep( *.contains("a") }).say;
我们可以使用一个 Slurpy 数组来获取文件名:
sub MAIN (*@files) {
lines.grep({ .contains("a") }).say;
}
我们仅为使用情况消息添加了 MAIN
。 参数(在 @files
中)将被忽略。
请注意,slurpy 参数允许使用零参数,因此该程序的行为与 «echo-grep» 相同。 这意味着将永远不会触发使用消息(因此,使用MAIN是无用的)。
有关如何修复它的信息,请参见第10.14节"*(Slurpy运算符)"。
4.6.5. slurp
使用 slurp
一次性读取整个文件:
> my $content = slurp "/home/raku/bin/echo-file";
Raku 中的所有字符串都使用 Unicode,但是可以使用其他编码读取(和转换)文件:
> my $contents = slurp "/home/arne/echo.c", enc => "latin1";
有关支持的编码的更多信息:https://docs.raku.org/routine/encoding。
4.6.6. open/close
我们可以打开文件(open
),对其进行处理,然后再关闭(close
):
该程序将读取一个指定为参数的文件,并打印所有包含字母"a"的行:
sub MAIN ($file-name) {
my $fh = open $file-name;
for $fh.lines -> $line {
say $line if $line.contains("a");
}
$fh.close;
}
lines
我在打开的文件句柄上使用过 lines
来读取内容,一次只能读取一行。
练习13.1
编写文件转换程序。 输入采用latin1(iso-latin-1),输出采用 Unicode(utf-8)。
提示:不要尝试写入文件(因为我们尚未显示如何执行该操作)。 写到屏幕(STDOUT)没问题,我们可以使用shell像这样为我们保存它:
$ raku isolatin2unicode isolatinfile > unicodefile
4.7. 写文件
如果给定的话,我们可以扩展文件转换程序以写入文件。 与以前一样,程序的第一个参数是要读取的文件名,第二个参数(如果有)是要写入的文件名。
如果我们不指定第二个参数,它将像以前一样显示在屏幕上:
$ raku isolatin2unicode4 isolatinfile unicodefile
$ raku isolatin2unicode4 isolatinfile > unicodefile
我们可以使用文件句柄,以写模式打开文件,然后在文件句柄上使用 say
:
> my $fh = open :w, '/tmp/some-file.txt';
> $fh.say("Hello");
> $fh.close;
请注意,$fh.say 需要使用括号或冒号语法(例如 $fh.say: "Hello" )。
|
记住副词语法(请参见第10.13.6节"副词"); :w
与 w ⇒ True
相同。
但是,我们将改为使用 spurt
命令。 它与 lines
相反,因为它会将我们提供的所有文本写入指定的文件。
当我们创建文件时,文件许可权是从 Unix 之类的系统上的系统 umask
中复制的。 Windows 不支持 umask
值或文件权限。
无法在打开或突然通话中更改模式,但请参阅《高级 Raku》课程中 chmod
的说明部分。
4.7.1. spurt
使用 spurt
写入文件,并自动打开和关闭文件句柄。
sub MAIN ($file-in, $file-out = "") {
$file-out
?? spurt $file-out, slurp $file-in, enc => "latin1"
!! say slurp $file-in, enc => "latin1";
}
我们可以写 $file-out = Nil
,但是可以使用空字符串。
没有任何类型的错误检查,那么可能会出错吗?
spurt overwrite
请注意,spurt
会愉快地覆盖现有文件,而不会发出警告。
如果文件存在,我们可以指示它失败:
spurt $out, :createonly, slurp $in, enc => "latin1";
如果你感到困惑,可以添加括号:
spurt($out, :createonly, slurp($in, enc => "latin1"));
spurt append
spurt
还具有追加模式,其中文本被添加到现有文件的末尾:
spurt $file-out, :append, slurp $file-in, enc => "latin1";
在写入日志文件时,这很有用。
4.7.2. 复习 prompt
prompt
,如6.6.1所示,"prompt" 与 $*IN.get
相同,带有可选的文本输出。
sub prompt-reimplemented ($message = "") {
$*OUT.say $message if $message;
return $*IN.get;
}
my $name = prompt "What's your name? ";
say "Hi, $name! Nice to meet you!";
4.7.3. get
get
从指定的文件句柄读取一行。 如果没有更多输入可用,则返回 Nil
。
从标准输入读取一行:
my $line = $*IN.get;
从文件中读取一行:
my $fh = open 'filename';
my $line = $fh.get;
$fh.close;
$*ARGFILES
一个单独的 get (在文件句柄上不使用它)的行为就像 lines 。
|
它将从命令行中给出的文件中读取,如果没有给出 $*IN
代替。
魔术是由 $*ARGFILES
在幕后执行的。
我们可以在 $*ARGFILES
上使用 handles
来获取每个参数的文件句柄:
say $_ for $*ARGFILES.handles;
运行它:
$ raku argfile-handle person args ack6
person -> IO::Handle<"person".IO>(opened)
args -> IO::Handle<"args".IO>(opened)
ack6 -> IO::Handle<"ack6".IO>(opened)
指定不存在的文件会导致程序崩溃:
$ raku argfile-handle person sjsjsjsjsjs
person -> IO::Handle<"person".IO>(opened)
Failed to open file /home/raku/sjsjsjsjsjs: No such file or directory
in block <unit> at ./argfile-handle line 3
请注意,即使未打开这些句柄,它们也被报告为打开状态(如果我们尝试从中读取)。 如果尝试读取,则会收到错误消息“无法在二进制模式下处理”,这是错误的。 |
实际发生的情况是,在调用 handles
方法时,这些句柄是打开的,但之后它们是关闭的:
my @handles = $*ARGFILES.handles;
for @handles { say $_ }
运行它:
$ raku argfile-handle2 person args ack6
IO::Handle<"person".IO>(closed)
IO::Handle<"args".IO>(closed)
IO::Handle<"ack6".IO>(closed)
我们可以使用 handles
来编写一个 zip 程序,该程序从作为参数给出的每个文件中取一行,然后继续进行直到写完所有内容:
my @handles = $*ARGFILES.handles;
.open for @handles;
while @handles {
my $handle = @handles.shift;
say $handle.get;
@handles.push($handle) unless $handle.eof;
}
请注意,我们必须手动打开文件才能正常工作。
$ raku zip-merge person-say3 repeat repeat2
该程序的命名不太精细,可能会帮助你记住 zip
运算符(请参阅《高级 Raku》课程。)
我们不能使用它,因为它将在行数最少的文件结束时停止。 但我们可以使用 roundrobin
运算符(我们将在"高级 Raku"课程中介绍)。
很难正确地做到这一点,但是这个单行程序有效:
$*ARGFILES.handles.eager».open».lines.&roundrobin.flat.map: *.put
>>
是超级运算符(我们将在"高级 Raku"课程中介绍)。 它可以并行工作要素。 我会把它留给读者练习,以弄清楚它为什么起作用。
(此代码段由 Brad Gilbert 编写。请参见 https://stackoverflow.com/questions/53639771/perl-6-argfiles-binds-binary-mode)。
如果在特殊的 MAIN 函数中使用 $*ARGFILES ,它将从 $*IN 读取。 (这适用于版本6.d(及更高版本)。
|
4.7.4. getc
使用 getc
从指定的文件句柄读取单个字符。 如果不带句柄使用,子例程格式默认为 $*ARGFILES
,如果在命令行上未指定文件,则该子例程格式再次默认为 STDIN。
如果没有更多可用输入,则返回 Nil
;如果以二进制模式在文件句柄上使用,则抛出异常。
my $char;
repeat {
print "> "; $char = getc;
say "Character: $char";
} while $char ne "Q";
它一直循环,直到我们输入"Q"字符为止。
运行它:
$ raku getc
> asdQw
Character: a
> Character: s
> Character: d
> Character: Q
由于终端设置为"已缓冲",因此在按回车键之前它不会执行任何操作。 然后它将获取所有字符,在每个字符之后都显示 «>» 提示符,如我们所见。
在 Linux 系统上,我们应该能够使用 stdbuf
命令关闭程序的缓冲,如下所示:
$ stdbuf --input=0 raku getc
但这至少在我的计算机上不起作用。
Unicode 和组合字符(请参见第7.1.2节"组合字符")也是一个问题,因为它们位于基础字符之后。 这意味着 getc
将至少等待两个字符,然后再返回第一个字符。 如果第二个不是组合字符,则返回第一个。 如果它是一个组合字符,只要它们都在组合字符中(因为我们可以有很多),getc
将继续读取字符-否则我们将遇到文件结尾。
4.7.5. readchars
使用 readchars
方法从文件句柄中读取最多指定数量的字符(字素)。 如果已在二进制模式下打开文件句柄,它将引发异常。
my $file = $*TMPDIR.add('foo.txt');
$file.IO.spurt: "This is a test...\n" x 25;
given $file.IO.open {
say "A:" ~ .readchars: 5; # OUTPUT:
say "B:" ~ .readchars: 90;
say "C:" ~ .readchars;
.close;
$file.unlink;
}
如果我们不指定字符数,则使用特定于实现的数字。 Rakudo 使用 $*DEFAULT-READ-ELEMS
,即 65536
。
4.8. 移除文件
到目前为止,我们一直在读写文件。 编写文件通常会创建它们,当然我们也可以删除它们。
使用取消链接删除文件,链接或符号链接。 目录只能使用rmdir删除(请参见第13.8.7节"rmdir")。
> unlink(<A B C D>);
> unlink "A".IO, "B".IO, "C".IO, "D".IO;
由于文件系统的限制,返回列表包括所有文件,但那些文件会引起问题(缺少权限或目录)。
作为一种方法,成功时返回 True
,否则返回 X::IO::Unlink
失败:
> "A".IO.unlink;
删除不存在的文件为 True
。
我们可以通过这种紧凑的方式获取错误消息:
> say .exception.message without 'bar'.IO.unlink;
Failed to remove the file [...] illegal operation on a directory
4.9. 临时文件
临时文件应放置在适当的位置,以免混乱具有正常内容的目录。 用于临时文件的目录也可能会不时进行自动清理。
使用常规文件操作(创建,写入,读取,删除)。
4.9.1. tmpdir
使用 $*SPEC.tmpdir
或 $*TMPDIR
动态变量来查找系统临时目录。 如果系统找不到当前目录,它们将默认为当前目录。
良好的做法是在以后删除临时文件,但是在清理程序之前可能会发生程序崩溃或过早终止的情况。
4.9.2. File::Temp 模块
最好使用 «File::Temp» 模块,因为它可以隐藏细节并为你做好簿记和清理工作。 特别是在程序过早终止的情况下删除临时文件。
4.10. 文件测试
我们确实应该在程序中添加错误检测(和恢复)。 尝试从不存在的文件中读取将失败,并终止程序。
我们可以通过 IO
对象("file-name".IO.d
),文件句柄($fh.d
)或使用智能匹配("file-name".IO ~~ :d
):
方法 |
描述 |
返回值 |
不存在 |
d |
Is it a directory? |
True/False |
fail |
e |
Does it exists? |
True/False |
False |
f |
Is it a file? |
True/False |
fail |
l |
Is it a symlink? |
True/False |
fail |
r |
Is it readable? |
True/False |
fail |
rw |
Is it readable and writeable? |
True/False |
fail |
rwx |
Is it readable, writeable and executable? |
True/False |
fail |
s |
File size (in bytes) |
Integer |
fail |
w |
Is it writeable? |
True/False |
fail |
x |
Is it executable? |
True/False |
fail |
z |
File size zero? |
True/False |
fail |
如果文件不存在,大多数文件将失败(带有 X::IO::DoesNotExist
)。
请注意,如果在目录中使用 s 和 z ,则结果可能非零,但这取决于操作系统。
|
示例(假设文件存在,并且我们先完成了 my $fh = "/tmp/A".IO.open
):
IO 方法 |
文件句柄方法 |
智能匹配 |
结果 |
"/tmp/A".IO.d |
$fh.d (see note) |
"/tmp/A".IO ~~ :d |
False |
"/tmp/A".IO.e |
$fh.e |
"/tmp/A".IO ~~ :e |
True |
"/tmp/A".IO.f |
$fh.f |
"/tmp/A".IO ~~ :f |
True |
"/tmp/A".IO.s |
$fh.s |
"/tmp/A".IO ~~ :s |
126976 |
请注意,我们无法打开目录,因此检查文件句柄是否为目录永远无法返回 True
。
4.10.1. 签名中的文件测试
让我们回顾第13.2.1节"IO.lines"中的"echo-file-contains3"。 如果我们指定一个不存在的文件,则会出现运行时错误:
$raku echo-file-contains3 SSSS
Failed to open file /home/raku/SSSS: No such file or directory ...
我们可以添加类型检查和类似的 multi
检查:
multi sub MAIN ($file-name where $file-name.IO.f) {
$file-name.IO.lines.grep( *.contains("a") ).join("\n").say;
}
multi sub MAIN (*@args) {
say "Oh, no! Please specify one file.";
}
第二个 multi MAIN
捕获了这样的情况:除了一个参数即现有文件以外,我们还指定了其他任何内容。
«echo-file-contains3» 给出的错误信息非常好,终止程序可能是正确的选择。
4.11. 二进制文件
换行(请参阅第6.1节,"换行")和 Unicode 归一化(请参阅第7.1节,"Unicode")会破坏二进制文件的读取(和写入)。
当我们以 Unicode 模式阅读时,该程序将阻塞非法序列。 我们可以通过使用 utf8-c8
编码来避免这种情况,因为它会以不变的方式传递字节。
但是二进制文件应该以二进制模式读取:
my $buffer = slurp $filename, :bin;
4.11.1. Buf
当我们以二进制模式读取文件时,我们得到一个 Buf
(缓冲区)作为回报。
我们可以使用 slurp
:
my $buffer = slurp $filename, :bin;
for @$buffer { ... }
或者手动打开(open
)并读取(read
)。
read
read
读取指定的字节数:
if my $fh = open $path, :bin {
my Buf $buffer = $fh.read( $count );
my $third_byte = $buffer[2];
}
请注意,read
也适用于非二进制文件,但是这样做会导致 Unicode 字符分解。
十六进制转储文件:
constant NL = 9252.chr; # This is the Unicode "N/L" symbol
constant BOX = 9617.chr; # This is a Unicode gray box
sub MAIN ($file where $file.IO.r) {
my $fh = open $file, :bin;
while my Buf $buf = $fh.read(10) {
my $ascii = "";
my $elems = @$buf.elems;
for @$buf -> $byte {
print $byte.fmt("%02X ");
if $byte eq any(10,13) {
$ascii ~= NL;
} else {
$ascii ~= 31 < $byte < 127 ?? $byte.chr !! BOX;
}
}
print " " x 10 - $elems; # Fill the last line
say "| $ascii";
}
$fh.close;
}
4.11.2. Blob
二进制数据也可以存储在 Blob
中(《二进制大对象》)。
> my $blob = Blob.new([1, 2, 3]);
Blob:0x<01 02 03>
> my $blob = Blob.new([255, 2, 3]);
Blob:0x<ff 02 03>
值的范围是 0 ..255
。超出该范围的值将被截断(对它们应用 % 256
):
> my $blob = Blob.new([256, 2, -1]);
Blob:0x<00 02 ff>
我们可以重写«file-show»以使用 Blob
:
更改此行:
while my Buf $buf = $fh.read(10)
为这个:
while my $buf = Blob.new($fh.read(10))
(它可以作为 «file-show-blob» 使用。)
Blob 是 Buf 的不变版本。
|
练习 13.2
编写文件比较程序。 它使用两个文件名,并进行二进制比较。
用法:
./file-equal enum-red enum-redX
No such file enum-redX
$ raku file-equal enum-red enum-red
The files are equal
$ raku file-equal enum-red enum-fixed
Files differ (different sizes)
$ raku file-equal hello-usage hello-usage2
Files differ
4.11.3. 写二进制文件
使用 Buf
和 write
写二进制文件:
unit sub MAIN ($file-name);
if my $fh = open $file-name, :w, :bin {
my $buf = Buf.new: 82, 97, 107, 117, 100, 111, 10;
$fh.write: $buf;
}
作为一个 blob
:
unit sub MAIN ($file-name);
if my $fh = open $file-name, :w, :bin {
my $blob = Blob.new: 82, 97, 107, 117, 100, 111, 10;
$fh.write: $blob;
}
一些测试:
$ raku write-buf X1
$ raku write-blob X2
$ raku file-equal X1 X2
The files are equal
可以在非二进制文件(文本文件)上使用 write
,但是这可能会导致非法的 Unicode
序列。 或是你要使用的任何文本编码中的非法序列。 (请注意,可以使用 Raku 不支持的编码来编写文本文件,但是最好是实现该支持。)
4.11.4. 检测二进制文件
没有内置的方法来检测文件是否为二进制文件,但是我们可以使用模块 Data::TextOrBinary
。
安装 Data::TextOrBinary
模块(如果尚未安装)。 (可能是 «sudo zef» 代替,具体取决于你的设置):
zef install Data::TextOrBinary
另请查看 12.2, "使用 zef 管理模块"。
use Data::TextOrBinary;
sub MAIN ($file) {
if $file.IO.d {
say "Directory.";
} elsif $file.IO.e {
is-text($file.IO)
?? say "Text file."
!! say "Binary file.";
} else {
say "File doesn't exist.";
}
}
该模块的工作方式是从文件中读取前 4096 个字节,然后查找文本文件中未出现的字符。
我们可以使用 «test-bytes» 参数指定要读取的字节数:
my $text = is-text($filename.IO, test-bytes => 8192);
有关详细信息,请参见 https://github.com/jnthn/p6-data-textorbinary。
使用它:
$ raku binary axxxx
File doesn't exist.
$ raku binary num-add-err
Text file.
$ raku binary _old/
Directory.
$ raku binary /bin/false
Binary file.
请注意,如果二进制内容未放在前面,该程序可能会将一些 pdf 文件报告为文本(本书就是其中的一个例子)。 增加测试字节值可以解决此问题。 |
4.12. 目录
目录是我们存储文件的地方。 和目录。
4.12.1. %*ENV<PATH>
最重要的目录是路径,即PATH环境变量,可作为 %*ENV<PATH>
使用。 它是一个字符串,其中包含用冒号分隔的目录列表,shell在其中按指定顺序查找要执行的程序。
> say %*ENV<PATH>;
/home/arne/bin:/home/arne/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin :/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
更好的格式:
> say %*ENV<PATH>.split(":").join("\n");
/home/arne/bin
/home/arne/.local/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/games
/usr/local/games
/snap/bin
4.12.2. dir
使用不带参数的 dir
为我们提供当前目录的全部内容(不带特殊目录 «.» 和 «..»),作为 IO
对象的列表:
> dir
("src".IO "RakuExplained.html".IO)
我们可以指定一个目录:
> chdir "/"
> dir "home"
("home/arne".IO)
> dir "/home"
("/home/arne".IO)
如果给出一个相对目录(不是以 «/» 开头),则会得到一个相对于当前目录的列表。
4.12.3. indir
使用 indir
命令可以在指定目录中执行代码,并将当前目录设置为该目录。
我们可以使用 indir
列出所有可用的程序(在路径中)。 该路径不是递归的,因此将忽略该路径中指定目录内的目录。
我们从路径开始,将其(在冒号分隔符上)拆分为每个部分,并在其上进行迭代:
for %*ENV<PATH>.split(":") -> $directory {...}
在循环内部,我们从跳过不存在的目录开始,因为我们允许路径中有垃圾(实际上通常是):
next unless $directory.IO.d; # Is this a directory?
然后我们有一个新循环。 我们在目录($directory
)上使用 indir
并在其中运行 dir
命令(指定为 &dir
,一个引用)。 dir
命令为我们提供了该目录中的文件(和目录)列表。 我们添加排序以给出排序列表。 这是区分大小写的类型,但是可以:
for indir($directory, &dir).sort -> $file {
我们跳过目录,因为我们只在寻找文件:
next if $file.d;
最后,如果当前用户可执行文件,则显示文件(具有完整路径):
say "$directory/$file" if $file.x;
}
}
请注意,say
为我们将 IO.Path
对象转换为字符串。
整个程序:
for %*ENV<PATH>.split(":") -> $directory {
next unless $directory.IO.d; # Is this a directory?
for indir($directory, &dir).sort -> $file {
next if $file.d;
say "$directory/$file" if $file.x;
}
}
运行它以查看适合你的程序。
4.12.4. $*CWD
Raku 将当前目录保留在特殊的 $*CWD
变量中。 不建议手动更改它。
indir
确实确实会自动更改 $*CWD
的值,但是在指定目录中运行命令后,它也会将其更改回原值。
练习 13.3
Unix 程序 which
为提供了给定程序的完整路径。
例如:
$ which less
/usr/bin/less
$ which pwd
/bin/pwd
用 Raku 编写该程序(并称其为"which6")。 使用 «list-path» 作为起点。
它应该仅报告第一个匹配。
练习 13.4
你的路径中有重复的程序(具有相同名称的程序)可能表明存在问题,因为将执行第一个程序(在路径中),而这可能不是你想要的。
编写一个遍历路径的程序,报告重复项。
$ raku check-path fido
- /usr/bin/fido
- /opt/bin/gecco/fido false
- /bin/false
- /home/raku/fakebin/false
使用 «which6» 作为起始点。
请注意,大多数 shell 程序都有几个内置命令,而不是路径中类似的程序。
练习 13.5
扩展 «check-path»,比较文件以查看它们是否相同。 符号链接,硬链接或精确副本。
提示:重用练习13.2中的 «file-equal» 。
可以假设我们只有同一程序的两个版本。 但是,如果要允许两个以上的版本,可以只将第二个和第三个与第一个进行比较。
样本输出(节略):
$ raku check-path-duplicates
print
- /usr/local/bin/print
- /usr/bin/print - Not equal
touch
- /usr/bin/touch
- /bin/touch - Equal
Found 19 duplicates and 12 different programs.
4.12.5. 使用 indir 递归
可以谨慎使用 indir
。
这是一个程序,列出了从当前目录开始的每个可读目录中的每个可读文件,递归地:
unit sub MAIN; my @dirs = ".";
while (@dirs) {
check-dir(@dirs.shift);
}
sub check-dir ($dir) {
say "Reading dir: $dir";
for indir($dir, &dir).sort -> $current {
next unless $current.IO.r; # Skip files/directories we cannot read
$current.IO.d
?? @dirs.push("$dir/$current")
!! say "File: $dir/$current";
}
}
它将对文件(和目录)进行排序,并首先进行宽度处理(在遍历目录之前显示目录中的所有文件)。 我们使用要检查的目录列表来执行此操作,并在遇到新目录时将新目录添加到末尾。
我选择将其编程为循环,而不是递归编程,因为我想在目录之前列出文件。
这里我们有一个递归版本:
unit sub MAIN; check-dir(".");
sub check-dir ($dir) {
say "Reading dir: $dir";
for indir($dir, &dir).sort -> $current {
next unless $current.IO.r; # Skip files/directories we cannot read
$current.IO.d
?? check-dir("$dir/$current")
!! say "File: $dir/$current";
}
}
排序顺序不是很好,但是我们可以通过在目录之前对文件进行排序来解决此问题:
for indir($dir, &dir).sort({ +$^a.IO.d ~ $^a cmp +$^b.IO.d ~ $^b }) -> $current
排序将 IO.d
应用于文件,并将结果转换为带有 +
前缀的数字(0或1)。 然后,它附加文件名。 结果是所有文件的前缀都为"0",目录的前缀为"1"。 因此,我们先获取文件(按排序顺序),再获取目录(也按排序顺序)。 输出应与 «indir-loop» 完全相同。
整个程序可以作为"indir-recursive2"使用。
练习 13.6
Unix 程 «grep» 可以用来搜索文件中的字符串,但是命令行参数的语法不是用户友好的。
编写"ack"程序是为了使此任务更容易,并且具有适合程序员的功能。 (查找它,因为它确实有用。)
编写一个程序"ack6",从当前目录中递归搜索所有非二进制文件,查找指定的字符串。
提示:从 «indir-recursive2» 开始。
4.12.6. mkdir
使用 mkdir
创建一个目录:
> mkdir "misc";
> mkdir "misc".IO;
> "misc".IO.mkdir;
它可以采用可选的权限参数(或模式),最好以八进制形式指定,例如 mkdir "misc", 0o777 。 在 Windows 上将忽略此值。
|
> mkdir "misc", 0o777;
[a]
请注意,模式值将与系统"umask值"或"mkdir"程序进行"或"运算。 没有办法覆盖它。
它也可以创建路径(类似于Unix «mkdir -p» 命令):
> mkdir "a/b/c/d/e";
4.12.7. rmdir
使用 rmdir
函数删除一个或多个目录。 它只会删除空目录,并返回实际删除的目录列表:
> rmdir(<a b c d e>);
[a b]
当用作方法时,如果能够删除目录,它将返回 True
;如果无法删除目录,则将抛出 X::IO::Rmdir
异常。
> "a".IO.rmdir; # -> True
> "c".IO.rmdir; # -> Failed to remove the directory ...
5. 日期和时间
Raku 对于日期和时间有非常好的内置支持。
5.1. time
使用 time
来获取自从 1970.1.1 以来的秒数(Unix 中的开始时间, 也是为人所知的纪元):
> time; # -> 1542530698
返回的值是一个 Int。
这是传统的 Unix 方式,并且已由 POSIX 标准化。
与 CPU 周期相比,一秒是很长的时间,因此在计时代码时几乎没有用。
5.2. now
使用 now
来获取自从 1970.1.1 开始的当前时间的秒数(带小数部分)
> now; # -> Instant:1532015558.371171
返回值是 Instant
类型的对象。
5.3. 润秒
如果你关心润秒,请注意 now
可以处理润秒,而 time
则不然。
润秒是不时插入的“额外”的秒数,以纠正偏差。
结果是 time
和 now
使用的值之间略有差异。 我们实际上可以显示它:
> say "{ time } - { now }";
1542532489 - Instant:1542532526.274499
> say "{ time } - { now.Int }"; 1542532500 - 1542532537
5.4. Instant
请注意,如果我们想从日期对象中推断出日期等,则必须有一个Instant对象(而不是POSIX时间值)。
5.4.1. Date
使用 Date
方法可从 Instant
获取一个 Date
对象。
Date
对象将字符串字符串化为具有年,月和日的日期字符串,如下所示:
> say now.Date; # -> 2018-11-18
> say Date.today; # -> 2018-11-18
年份使用4位数字,月份和日期分别使用2位数字。 我们可以通过将其指定为字符串,Int数组或命名参数列表来为任何日期创建Date对象:
> my $date = Date.new("2018-10-01");
> my $date = Date.new(2018, 10, 1);
> my $date = Date.new(year => 2018, month => 12, day => 10);
我们可以对 Date
对象使用很多方法。 这是最有用的(我们从 $d = Date.new(2018,10,1)
开始):
方法 |
结果 |
描述 |
year |
2018 |
年份 |
month |
10 |
月份(1..12) |
day |
1 |
月中的某天 |
is-leap-year |
False |
2018 不是闰年 |
day-of-month |
1 |
和 |
day-of-week |
1 |
周几(1=周一 .. 7=周日) |
day-of-year |
274 |
一年中的第 274 天 |
days-in-month |
31 |
月中的天数 |
week-number |
40 |
星期数(1..53) |
week-year |
2018 |
周号所属的年份(请参阅下面的注释) |
week |
(2018 40) |
|
weekday-of-month |
1 |
本月发生这一天的次数(包括这一天) |
yyyy-mm-dd |
2019-10-01 |
与 |
TIPS: 跨度为两年的一周属于大部分(或星期四)的一年。 该星期可能属于上一年(一月)或下一年(十二月):
> Date.new(2017,1,1).week; # -> (2016 52)
> Date.new(2018,12,31).week; # -> (2019 1)
以下方法返回一个新的 Date
对象(我们从 $d = Date.new(2018,10,10)
开始):
方法 |
结果 |
描述 |
earlier(days ⇒ 2) |
2018-10-08 |
减去给定的天数 |
earlier(week ⇒ 1) |
2018-10-01 |
减去给定的星期数 |
earlier(month ⇒ 2) |
2018-08-10 |
减去给定的月数 |
earlier(year ⇒ 2) |
2016-10-11 |
减去给定的年数 |
later(days ⇒ 2) |
2018-10-12 |
加上给定的天数 |
later(week ⇒ 1) |
2018-10-17 |
加上给定的星期数 |
later(month ⇒ 2) |
2018-12-10 |
加上给定的月数 |
later(year ⇒ 2) |
2020-10-10 |
加上给定的年数 |
truncated-to('year') |
2018-01-01 |
截取到某年的第一天 |
truncated-to('month') |
2018-10-01 |
截取到某月的第一天 |
truncated-to('week') |
2018-01-08 |
截取到某周的第一天 |
succ |
2018-10-11 |
下一天 |
pred |
2018-10-09 |
前一天 |
earlier 和 later 的参数可以以单数或复数形式给出:(day 或 days,week 或 weeks,month 或 months,year 或 years)。
|
带有参数的方法仅需一个,但是可以将它们堆叠起来:
> my $date = Date.new(2018,10,10).later(years => 10).later(days => 4);
练习
如果不带参数调用,Unix命令“ cal”显示当前月份。 当前日期突出显示。 将其实现为“ cal6”。 不支持参数,请跳过突出显示。 周日应该是一周的第七天,因此请同时进行修复。
练习
扩展«cal6»,使其采用月份和年份的可选值。 例如。
$ raku cal6-param --month=2 --year=2020
5.4.2. DateTime
DateTime
对象具有日期(作为 Date
对象),还具有秒,分钟和小时的字段。 时间在内部以 UTC 格式(以前称为 GMT 或英国标准时间)保存。
我们可以使用 DateTime.now
-或 Instant
对象(使用 now
函数)获取一个新的 DateTime
对象:
> say DateTime.now; # -> 2018-11-18T23:46:22.609982+01:00
> say now.DateTime; # -> 2018-11-18T22:46:22.609982Z
如果我们打印这些值,则会得到不同的结果,如上所示。 时间相同,但是显示方式不同。 我们将回到那个(时区)。
首先,我们来看一下 DateTime
构造函数:
my $date-time = DateTime.new(year => 2018,
month => 10,
day => 20,
hour => 16,
minute => 1,
second => 10);
my Date $date = Date.now;
my $date-time2 = DateTime.new($date, hour => 16,
minute => 1,
second => 10);
my $date-time3 = DateTime.new(now); # An Instant object
my $date-time4 = DateTime.new(time); # An integer
my $date-time5 = DateTime.new("2018-11-18T23:46:22.609982+01:00");
my $date-time6 = DateTime.new("2018-11-18T22:46:22.609982Z");
请注意,秒是唯一可以包含小数部分的字段。
日期部分的格式与 Date
对象的格式相同:«year-month-date»(具有4、2和2位数字)。 然后是时间部分之前的 «T»。 时间部分:«hour:minute:seconds»(带有2、2和2位数字)。 秒也可以是小数部分。 最后一部分是时区。 它可以是表示 UTC 的字母 «Z»(如 «Zulu» 中的字母),也可以是时区的描述,以偏移 «hours:minutes»(2位和2位数字)表示。 时间以当地时区显示,我们可以通过减去此时差来获得 UTC 时间。
DateTime
对象支持与 Date
对象(第14.4.1节“日期”中的两个表)相同的方法,以及(我们以 DateTime.new("2018-11-18T23:46:22.609982+01:00")
):
方法 |
结果 |
描述 |
hour |
22 |
小时 |
minute |
46 |
分钟 |
second |
22.609982 |
秒数, 如果有小数就带小数 |
whole-second |
22 |
秒数, 截断为整数 |
timezone |
3600 |
时区, 以秒为单位的 UTC 偏移量 |
offset |
3600 |
与 |
offset-in-minutes |
60 |
时区, 以分钟为单位的 UTC 偏移量 |
offset-in-hours |
1 |
时区, 以小时为单位的 UTC 偏移量 |
Str |
2018-11- 18T23:46:22.609982 +01:00 |
作为日期和时间的字符串 |
Instant |
Instant |
代表时间和日期的 |
posix |
Int |
POSIX 值(自 1970.1.1 以来的秒数) |
Date |
Date |
对象的日期部分。 当心时区 |
utc |
DateTime |
一个新的 |
in-timezone |
DateTime |
一个新的 |
local |
DateTime |
一个新的 |
除了对这些的扩展参数支持之外:
方法 |
结果 |
描述 |
earlier(second ⇒ 2) |
2018-11-18T23:46:20.609982+01:00 |
减去给定的秒数 |
earlier(minute ⇒ 2) |
2018-11-18T23:44:22.609982+01:00 |
减去给定的分钟数 |
earlier(hour ⇒ 2) |
2018-11-18T21:46:22.609982+01:00 |
减去给定的小时数 |
later(second ⇒ 2) |
2018-11-18T23:46:24.609982+01:00 |
加上给定的秒数 |
later(minute ⇒ 2) |
2018-11-18T23:48:22.609982+01:00 |
加上给定的分钟数 |
later(hour ⇒ 2) |
2018-11-19T01:46:22.609982+01:00 |
加上给定的小时数 |
truncated-to('second') |
2018-11-18T23:46:22+01:00 |
截断为整秒数 |
truncated-to('minute') |
2018-11-18T23:46:00+01:00 |
截断为整分钟数 |
truncated-to('hour') |
2018-11-18T23:00:00+01:00 |
截断为整小时数 |
truncated-to('day') |
2018-11-18T00:00:00+01:00 |
截断为整天数 |
值2只是一个例子。
TIPS: 可以以单数形式或复数形式给出 earlier
和 later
的参数:(second 或 seconds,minute 或 minutes,hour 或 hours,day 或 days,week 或 weeks,month 或 months,year 或 years)。
我们还有一个 clone
,它提供 DateTime
对象的副本,其中包含指定字段的新值(如果有)。 这些字段与 new
调用的字段相同:
> my $much-later = DateTime.now.clone(year => 2045);
5.4.3. 时区
新的构造函数采用可选参数 timezone ⇒ <seconds>
,在此我们将时区指定为相对于 UTC(或 GMT)的偏移量(以秒为单位)。 如果未指定,则使用动态变量 $*TZ
(«时区»)的值。
> say $*TZ; # -> 3600 (in Oslo, Norway)
请注意,DateTime.now
为我们添加了时区,但 now.DateTime
没有。
5.4.4. 自定义格式化
当我们使用 DateTime.new
或 DateTime.now
时,我们也可以指定自定义格式器。 它的工作是格式化字符串化的日期和时间:
sub custom-formatter (DateTime $dt) {
sprintf '%02d.%02d.%04d %02d:%02d:%02d', $dt.day, $dt.month, $dt.year, $dt.hour, $dt.minute, $dt.whole-second;
}
say DateTime.now;
say DateTime.now(formatter => &custom-formatter);
say DateTime.now(formatter => &custom-formatter).later(year => 1);
运行它表明自定义格式化程序被新对象继承:
$ raku datetime
2018-11-20T12:18:36.963003+01:00
20.11.2018 12:18:36
20.11.2019 12:18:36
5.4.5. 从 POSIX 到 Instant
我们可以从 POSIX 值转换为 Instant:
> my $intant = Instant.from-posix: $posix-time;
我们本可以在 «time-leap» 中使用这一事实:
my $time = time;
my $now = Instant.from-posix: $time;
say "Number of leap seconds added after 1.1.1970: " ~ $now.Int - $time;
我们得到与本章前面相同的结果:
$ raku text/code/time-leap2
Number of leap seconds added after 1.1.1970: 37
5.5. 计时器
我们可以计算执行一个程序或其一部分所花费的时间。
5.5.1. 给程序计时
在类似 Unix 的系统上,我们可以使用 «time» 程序来查看运行一个程序需要多长时间:
$ time raku random-prime 10 7
real 0m0,138s
user 0m0,164s
sys 0m0,031s
$ time raku random-prime 10000 8089
real 0m1,419s
user 0m1,458s
sys 0m0,024s
(«real»值是实际时间,«user» 是编译器使用的时间的一部分,«sys» 是系统调用(在操作系统中)使用的时间。)
TIPS: 第10.14.2节“重访了随机素数”中介绍了 «random-prime» 程序。 重要的一点是,你提供的数字越大,完成所需的时间就越长。
注意,我们为所有时间计时。 raku 的启动,读取源文件(和任何模块),编译程序并最终执行它。
每次运行该程序时,实际值都会有所不同,这取决于同时进行的操作。 另一台计算机可以提供完全不同的值,因此进行比较时要小心。
当我们要比较事物时,此方法不是很有用。
5.5.2. 给代码计时
我们可以在编译器自身内部给执行代码计时:
my $start = now;
# do-something;
say "Time used: { $start - now }";
如果我们只想给单个块计时, 我们可以将其写为:
# do-something;
say "Time used: { now - INIT now }";
INIT
是程序执行相位器(或块),在程序编译之后和执行之前执行。
我们可以使用相位器在不同时间自动执行代码。 有很多。 有关详细信息,请参见 https://docs.raku.org/language/phasers。
与使用 «time» 时一样,由于运行时间会有所不同,因此代码应定时计时几次。 实际值不是很有用,但是可以将它们与其他值进行比较-并用于比较不同的实现。 我们可以实现它的功能:
sub time-me (&code, $iterations = 100) {
my @time;
for ^$iterations {
my $start = now;
&code();
my $stop = now; @time.push($stop - $start);
}
return @time.sum / @time.elems;
}
然后进行一些测试,在其中增加一个匿名状态变量(我们将在第16.6.2节“ $ / @ /%(匿名状态变量)中进行描述)。它们在调用之间保留其值,在此代码中基本上用作计数器:
sub a {
$++ for 10000;
}
sub b {
++$ for 10000;
}
say "a: " ~ time-me(&a, 10000);
say "b: " ~ time-me(&b, 10000);
运行它:
> $raku time-me
a:4.481019762283224e-05
b:4.395750332005312e-05
科学记法很难阅读,尤其是难以比较。 因此,我们添加了一个可选的乘数:
sub time-me (&code, $iterations = 100, $multiplier = 1)
return $multiplier * @time.sum / @time.elems;
say "a:" ~ time-me(&a, 10000, 1000);
say "b:" ~ time-me(&b, 10000, 1000);
运行它会提供更好的(人类可读的)值:
$ raku time-me
a:0.044137168141592915
b:0.04315050652893645
现在,我们有了一个非常基本的计时框架。 我们可以将其变成一个模块,而且确实可以。 在第15章中,编写模块。
6. 编写模块
我们在第14.5.2节"计时代码"中建立了一个非常基本的计时框架。 我们可以将其转换为一个模块,但是由于已经有两个模块可以执行此操作,因此可以重新设计轮子:
-
Test::Performance
-
Benchmark
但是我们还是会做…
6.1. unit 模块
指定模块的通常方法是使用 module
关键字后跟一个块:
module xxxx {
# Code here;
}
但是我们可以通过使用 unit module
来节省一个块级别:
unit module xxxx;
# Code here;
就像我们指定一个 procedure
(请参见10.9.1,"unit procedure")或一个类(请参见17.12.2,"unit class")一样。
6.2. is exportt
我们必须在签名后标记要导出的可用于外部使用的过程:
sub time-me (&code, :$iterations = 100, :$multiplier = 1) is export { ... }
6.3. pm6
Raku 模块的文件名扩展名为 "pm6"(如 «Perl Module 6» 中一样。重命名为 Raku 的语言将影响此扩展名和其他文件扩展名。)请注意,过去也使用了 «pm»。 较旧的模块可能仍会这样做。 建议仅使用《 pm6》,因为如果有人试图使用 Perl 5 中的模块,则会给出更好的错误消息。
use v6.c;
unit module Time-Code;
sub time-me (&code, :$iterations = 100, :$multiplier = 1) is export {
my @time;
for ^$iterations {
my $start = now;
&code();
my $stop = now; @time.push($stop - $start);
}
return $multiplier * @time.sum / @time.elems;
}
6.4. use lib
使用 use lib
可以指定编译器应在其中查找模块的其他位置。
正常(建议)使用例如 在开发模块时 use lib "lib"
,因为这样可以更轻松地进行动态测试。 (我们稍后将讨论的测试框架将使用此技术,因为 zef
在安装模块之前会运行测试(否则不会找到模块)。
你必须从 «lib» 所在的目录中运行该程序,此程序才能起作用。
请注意,如果从另一个位置也有 «lib» 目录的位置运行程序,则可能是一个安全问题:
$ pwd # -> /home/raku
$ raku code/chapter12/check-path
如果 «check-path» 程序具有 use lib "lib"
语句,它将告诉编译器查看 «/home/raku/lib» 目录(而不是 «/home/raku/code/chapter12»)。
以及使用它的程序(同样来自第14.5.2节"计时代码")。
use lib "lib";
use Time-Code;
sub a {
$++ for 10000;
}
sub b {
++$ for 10000;
}
my $iterations = 1000;
my $multiplier = 1000;
say "a: " ~ time-me(&a, :$iterations, :$multiplier);
say "b: " ~ time-me(&b, :$iterations, :$multiplier);
运行它,以及没有模块的旧版本:
$ raku time-me-module
a: 0.04731108696221918
b: 0.04730277517929529
$raku time-me
a: 0.04462623946451714
b: 0.0447062423500612
计时框架的模块版本稍慢一些(约5%)。
6.5. 斐波纳契记时
我们可以对斐波那契数列程序(请参见10.12.2,“斐波那契数列”)和序列(请参见16.3.1,“斐波那契数列”)进行计时。
use lib "lib";
use Time-Code;
my $fibonacci := (1, 1, { $^a + $^b } ... Inf);
sub MAIN (Int $n, :$iterations = 100, :$multiplier = 1) {
say "Fib $n: " ~ time-me({ &fibonacci($n) }, :$iterations, :$multiplier);
say "Fib Rec $n: " ~ time-me({ &fibonacci-recursive($n) }, :$iterations, :$multiplier);
say "Fib Mul $n: " ~ time-me({ &fibonacci-multi($n) }, :$iterations, :$multiplier);
say "Fib Seq $n: " ~ time-me({ $fibonacci[$n] }, :$iterations, :$multiplier);
}
sub fibonacci (Int $n) {
return 1 if $n == 1 or $n == 2;
my @fib = (1, 1);
for 2 .. $n -1 -> $i {
@fib[$i] = @fib[$i -1] + @fib[$i -2]
}
return @fib.tail;
}
sub fibonacci-recursive (Int $n) {
return 1 if $n == 1 or $n == 2;
return fibonacci-recursive($n-1) + fibonacci-recursive($n-2)
}
multi fibonacci-multi (1) { 1 }
multi fibonacci-multi (2) { 1 }
multi fibonacci-multi (Int $n where $n > 2) {
fibonacci-multi($n - 2) + fibonacci-multi($n - 1)
}
$ raku fibonacci-time --mul=1000 12
Fib 12: 0.17109426818938775
Fib Rec 12: 0.28062656376560663
Fib Mul 12: 4.763127095776541
Fib Seq 12: 0.05405135328930629
计算第十二个斐波那契数与循环和递归几乎需要相同的时间。 multi
版本速度较慢。
而且,相比起来 Sequence 很快。(但问题是,我们将计时运行了 100 次,最后 99 次是我们从序列中检索了一个缓存的值。)
我们可以运行一次代码:
$ raku fibonacci-time --mul=1000 --iter=1 12
Fib 12: 1.686145939074689
Fib Rec 12: 0.8542231594091491
Fib Mul 12: 8.122685764523933
Fib Seq 12: 1.4559518704264895
不要相信这些数字(递归版本突然比循环版本快),因为我们只运行了一次代码。 但是 Sequence 版本的计时更为现实。
$ raku fibonacci-time --mul=1000 20
Fib 20: 0.19579672840493245
Fib Rec 20: 7.5705065387054296
Fib Mul 20: 220.8083508932559
Fib Seq 20: 0.06156730344760013
当我们计算第 20 个数字时,递归版本的速度会大大降低,而 multi
版本的速度甚至会更慢。
6.5.1. 练习
为什么递归版本比循环版本慢?
TIPS: 请注意,计时模块不会返回或显示我们正在计时的过程中返回的值。 因此,也要正常运行代码,以确保返回的值正确(或者至少我们对所有它们都得到相同的错误-因此它们同样是错误的)。
6.6. 字典
在本节中,你将需要一个合法单词词典。 Ubuntu Linux 具有以下词典:
-
/usr/share/dict/american-english (the «wamerican» package)
-
/usr/share/dict/british-english (the «wenglish» package)
-
/usr/share/dict/ngerman (the «wngerman» package)
英文字典中有诸如“安倍”之类的条目,我们会忽略它们,因为它们包含非单词字符。
如果你没有安装词典文件,请找到并下载。 它必须是一个文本文件,每行一个单词。 只要你熟悉所选的语言,选择哪种语言都没关系。
6.6.1. 练习
编写一个模块 «Dictionary»,该模块将加载指定的词典文件(具有完整路径),并返回所有单词的哈希值。
编写简短的测试程序。
«回文词是单词,数字,短语或其他字符序列,它们向前或向后读取相同,例如,madam,racecar 或数字 10801。”(来源:https://en.wikipedia.org/wiki/Palindrome)
6.6.2. 练习
使用 «Dictionary» 模块编写一个程序,该程序可以在字典中打印回文。
6.6.3. 练习
使用 «Dictionary» 模块编写程序,该程序检查字典中每个单词的反向版本是否也是有效单词。
«字谜是通过重新排列不同单词或短语的字母而形成的单词或短语,通常只使用所有原始字母一次。 例如,可以将 anagram 单词重新排列为 "nag a ram",或者将 "binary" 单词重新排列为 "brainy"。»(来源:https://en.wikipedia.org/wiki/Anagram)
6.6.4. 练习
使用 «Dictionary» 模块编写程序,该程序检查指定为程序参数的单词的字谜。
6.6.5. 练习
很容易重写字谜检查程序来检查字典中的所有单词,而不是单个单词作为参数。
这是一个好主意吗?
7. 范围和序列
如果尚未阅读 Ranges 的介绍,请参见第4.2节"Ranges(简短介绍)"。
范围是相当有限的,但是序列(几乎)没有任何限制。
7.1. 范围
范围运算符 ..
给出了一系列连续递增的整数:
> say (1 .. 5).WHAT; # -> (Range)
> say (1 .. 5); # -> 1..5
7.2. lazy
值通常在定义它们时计算。 Raku 将其称为"急迫的",并添加了第二种类型,称为"懒惰的"。
惰性数据结构由某种类型的值组成,但是只有在实际需要它们时(如在访问中一样),才计算各个值。
范围(以及稍后将介绍的序列)是惰性的,因此只有在实际需要它们时才计算值。
这样就可以拥有无限(或无限)的范围:
> say (1 .. Inf).WHAT; # -> (Range)
7.2.1. is-lazy
如果不确定某个值,变量或数据结构是惰性的还是急切的,请使用 is-lazy
:
> say (1 .. Inf).is-lazy; # -> True
> say "A String".is-lazy; # -> False
如果我们将范围分配给数组,则将对其进行计算:
> my @range = 1 .. 10; # -> [1 2 3 4 5 6 7 8 9 10]
> say @range.is-lazy; # -> False
> say @range.WHAT; # > (Array)
我们可以尝试无限范围:
> my @range = 1 .. Inf; # -> [...]
> say @range.WHAT; # -> (Array)
> say @range.is-lazy; # -> True
这给出了一个惰性列表。 仅当它不急切时才是默认值。
也可以使用 lazy
关键字强制表达式是惰性的。
7.2.2. lazy
使用 lazy
关键字强制表达式为惰性的。
它几乎可以用于任何事物,但是循环是最有用的构造。
my $numbers := lazy for ^Inf { $_ };
say $numbers[0];
say $numbers[10];
.say for $numbers;
这给了我们无限循环。 通常,计数器变量(从 0
到 Inf
)在 $_
中可用。 块中的最后一个表达式是惰性列表中的值。
如果我们在块内执行某些操作,例如 例如:{ "dummy$" } 或 { pi * $
- e }
。
TIPS: 查看 lazy vs gather/take 获取更复杂的例子。
7.2.3. infinite
我们可以使用 infinite
来检查 Range
是否是无限的,或者将开始和/或结束声明为无限(使用 Inf
,*
或 ∞
):
> say (1 .. Inf).infinite; # True
请注意,我们不能在 @array
上使用 infinite
,因为在赋值时它会被强制转换为列表。
7.2.4. 列表强制转换
我们可以把范围强制转换为列表:
> say (1 .. Inf).WHAT; # -> (Range)
> say (1 .. Inf).List.WHAT; # -> (List)
> say (1 .. Inf).List.is-lazy; # -> True
> say (1 .. 100).is-lazy; # -> False
> say (1 .. 100).List.is-lazy; # -> False
7.2.5. eager
使用 eager
强制计算范围(或序列或惰性列表)中的值。 它将返回值作为列表。
> (1 .. Inf).eager
这有效(或挂起,具体取决于你的观点)。 该表达式将永远运行,没有任何可见的结果。 练习16.1如果我们坐下来等待无限个列表崩溃,首先发生的事情是: - 内存不足(数组中的元素过多)? - 值对于整数来说太大吗?
7.2.6. is-int
使用 is-int
(仅适用于 Range
!)告诉我们 Range
是否仅包含整数:
> say (1 .. 10).is-int; # -> True
> say ('A' .. 'Z').is-int; # -> False
7.2.7. 字符串中的范围
这有效:
> .print for ("a" .. "z"); say ""; # Add a newline at the end.
abcdefghijklmnopqrstuvwxyz
或字符:
> say ("aa" .. "bb").WHAT; # -> (Range)
> say ("aa" .. "bb").eager; # -> (aa ab ba bb)
你期望的是 "aa" .. "az", "ba", "bb" 吗?
范围运算符现在不再需要字母。 一切都是 Unicode 字符,因此它将完全满足你的要求。 (它迭代第一个字符直到到达目标,然后在第二个字符中执行相同的操作,依此类推。)
请注意,Raku 关于如何从 "aa" 到 "bb" 进行计数的想法可能不适合你的需求。
7.2.8. minmax
也可以使用 «minmax» 运算符构造一个范围。
它采用两个值,并返回从值的最低到最高的范围,而不管给定的顺序如何:
# numeric comparison
10 minmax 3; # 3..10
# string comparison
'10' minmax '3'; # "10".."3"
'z' minmax 'k'; # "k".."z"
顺序由 «cmp» 运算符确定。
如果最小值和最大值一致,则运算符将返回由相同值构成的范围:
1 minmax 1; # 1..1
当应用于列表时,minmax 运算符将计算所有可用值中的最低和最高值:
(10,20,30) minmax (0,11,22,33); # 0..33
('a','b','z') minmax ('c','d','w'); # "a".."z"
同样,当应用于哈希时,它会按照 cmp
方式比较:
my %winner = points => 30, misses => 10;
my %loser = points => 20, misses => 10;
%winner cmp %loser; # More
%winner minmax %loser;
# ${:misses(10), :points(20)}..${:misses(10), :points(30)}
7.3. 序列
序列是用 …
生成的,而不是用 ..
生成范围的。
> say (1 .. Inf).WHAT; # -> (Range)
> say (1 .. Inf)[^10]; # -> (1 2 3 4 5 6 7 8 9 10)
> say (1 ... Inf).WHAT; # -> (Seq)
> say (1 ... Inf)[^10]; # -> (1 2 3 4 5 6 7 8 9 10)
你可以在 Raku 中构造的每个范围也可以按顺序排列,但不能相反。
序列(和范围)是惰性的。 仅在需要时才计算这些值。
> say (1..10).WHAT; # -> (Range)
> say (1..10).reverse; # -> (10 9 8 7 6 5 4 3 2 1)
> say (1..10).reverse.WHAT; # -> (Seq)
Raku 可以为我们生成序列:
> (1, {$_ * 2} ... *)[^10]; # -> (1 2 4 8 16 32 64 128 256 512)
> (2, {$_ - 2} ... *)[^10]; # -> (2 0 -2 -4 -6 -8 -10 -12 -14 -16)
或者我们可以给它足够的值来理解模式:
> (1, 2, 4 ... Inf)[^10]; # -> (1 2 4 8 16 32 64 128 256 512)
> (2, 4 ... Inf)[^10]; # -> (2 4 6 8 10 12 14 16 18)
> (2, 0 ... -Inf)[^10]; # -> (2 0 -2 -4 -6 -8 -10 -12 -14 -16)
> (1 ... -10)[^10]; # -> (1 0 -1 -2 -3 -4 -5 -6 -7 -8)
7.3.1. 斐波那契数列
我们可以使用相当高级的规则来生成值。
还记得斐波那契数字(来自第10.12.2节“斐波那契数字”)吗? 在这里,它们是一个序列:
> say (1, 1, * + * ... *)[^10]; # -> (1 1 2 3 5 8 13 21 34 55)
> say (1, 1, { $^a + $^b } ... Inf)[^10]; # -> (1 1 2 3 5 8 13 21 34 55)
* + *
部分表示使用两个占位符值计算第三个值并将它们相加。 占位符位于当前值的左侧,因此在这种情况下,第一个和第二个值(这就是为什么必须明确指定它们的原因)。 … *
部分表示这将永远持续下去(这与Inf相同)。
第二个示例使用显式的占位符变量,在这里我们几乎可以对值进行任何处理。
7.3.2. 绑定 vs 赋值
在序列上推荐使用绑定 :=
而不是通常的赋值(=
)。
> my $fibonacci := 0, 1, * + * ... *;
> say $fibonacci.is-lazy; # -> True
> say $fibonacci[10]; # -> 55
斐波那契数列可以从 1 开始(直到现在为止一样),也可以从 0 开始。数学家不同意正确的答案。 1 是最常用的起始值。
绑定仅适用于标量。 如果希望变量看起来像数组,请使用赋值:
> my @fibonacci = 0, 1, * + * ... *;
> @fibonacci.is-lazy
True
> @fibonacci[10]
55
绑定在惰性数据结构(作为序列)上效果更好,因为赋值可能会导致对数据结构进行计算(或者可以说是“非惰性化”)。
如果你想知道。 这是合法的(但不是个好主意):
> my $a = (1..10)
1..10
> $a[8]
9
该列表(在这种情况下为范围)将是只读的。
7.3.3. lazy
使用 lazy
强制范围(或序列)尽可能地保持惰性:
> my @range = (1 .. 10);
(1 2 3 4 5 6 7 8 9 10)
> my @range = (1 .. 10).lazy;
[...]
请注意,赋值给数组会导致对范围进行计算。 将其分配给标量可以避免这种情况:
> my $range = (1 .. 10);
1..10
> $range.WHAT;
(Range)
总结:
Eager 数据类型 |
Lazy 数据类型 |
my @x = (1 .. 10) |
|
my @x = (1 .. 10) |
|
my $x = (1 .. 10) |
|
my $x = (1 .. 10) |
|
my $x := (1 .. 10) |
|
my $x := (1 … 10) |
无限范围和顺序总是很懒。 因此,如果我们将 Inf
更改为 10,则所有内容最终都会出现在“惰性数据类型”列中。
7.3.4. 内存友好
无限范围和序列的内存占用量很低,因为它只会根据需要生成值:
> say (1..Inf)[10] # Parens required, as «Inf[10]» isn't a thing.
9
请注意,在惰性范围(或列表或序列)上进行 pick
会强制对其进行计算。 如果它是无限的,则将不起作用:
> (1 .. Inf).pick; # -> Nil
运气不好!
7.3.5. Flip-Flop 序列
是否可以制作触发器序列? 每次我们问某件事时,会改变主意的事情吗?
Raku 有一个名为 ff 的触发器运算符和一个名为 fff 的变体。 它们将在"高级 Raku"课程中介绍。 它们的含义与我们在此描述的含义不同。
|
> my $flip-flop := (True, False, !* ... *);
> (True, False, !* ... *)[^10]
(True False True False True False True False True False)
或者这个:
> my $flip-flop := (True, {! $_ } ... *);
这个序列永远不会达到无限(或者说:“永不尝试达到无限”),但是生成器甚至可以这样工作:
> my $i = 0;
> say "I like potatos: " ~ $flip-flop[$i++] for ^10;
我们不能对序列使用 shift,而只能迭代:
my $flip-flop := (True, False, !* ... *);
say "I like potatos: $_." for $flip-flop
> raku flip-flop-sequence I like potatos: True.
I like potatos: False.
I like potatos: True.
I like potatos: False.
...
该程序将永远运行。 使用<Control-c>停止它。
7.3.6. 列表重复运算符和序列
像这样使用时,我们可以使用列表重复运算符 xx
获得相同的序列(请参见第8.19节"`xx`(列表重复运算符)"):
> my $flip-flop = |(True, False) xx *
> $flip-flop.WHAT
(Seq)
> $flip-flop.is-lazy
True
|
在将列表弄平之前,它是必不可少的,因为否则我们将获得具有 (True, False)
的无限子列表。
7.4. state
用 state
声明状态变量(而不是普通的 my
)。
仅在程序第一次进入 state
行时才进行初始化,并且此行将被忽略之后,使其保持旧值。
我们可以使用状态变量实现 Flip.Flop,并将所有内容包装在过程中。
我们可以通过跟踪索引的过程来访问 Flip.Flop 序列。 状态变量的初始化仅完成一次,它将在两次调用之间保留该值:
my $flip-flop := (True, False, !* ... *);
sub flip-flop {
state $index = 0; # Only executed once!
return $flip-flop[$index++];
}
say flip-flop for ^10;
我们完全不需要序列:
sub flip-flop {
state $state = False; # Only executed once!
$state = ! $state;
return $state;
}
say flip-flop for ^10;
7.5. Truly 随机 Flip-Flop
«flip-flop» 的第一个版本是可以预见的,因为它总是以 True
开头。
很容易解决这个问题,因此,如果第一个值为 True
或 False
,则它是完全随机的:
sub flip-flop {
state $state = (True, False).pick; # Only executed once!
$state = ! $state;
return $state;
}
say flip-flop for ^10;
7.6. flip-flop 问题
如果在一个上下文中使用,«flip-flop» 的行为将符合预期。 相反的例子:
sub flip-flop {
state $state = (True, False).pick; # Only executed once!
$state = ! $state;
return $state;
}
sub free-lunch {
say "I'm { flip-flop() ?? "for" !! "against" } free lunches";
}
sub free-dinner {
say "I'm { flip-flop() ?? "for" !! "against" } free dinners";
}
for ^5 {
free-dinner;
free-lunch;
}
I'm against free dinners
I'm for free lunches
I'm against free dinners
I'm for free lunches
I'm against free dinners
I'm for free lunches
I'm against free dinners
I'm for free lunches
I'm against free dinners
I'm for free lunches
该计划根本不会改变位置,但是反对免费晚餐,并且始终免费午餐。
有什么建议吗?
7.6.1. Flip-Flop 重新设计
需要进行彻底的重新设计。
显而易见的方法是创建一个类,并使用该类的实例(对象)。 我们将在下一章中说明。
(我们尚未完成范围。)
7.6.2. $/@/% (匿名状态变量)
可以使用匿名状态变量 $
代替显式 state $xxxx
(请参见第16.4节“状态”),但是(显然)只能有一个。
sub something {
my $ = 0; # Only executed once!
return $++;
}
say something for ^10;
程序将在各自的行上打印数字 0 至 9。
如果我们对零作为初始值感到满意,则实际上可以跳过声明。 使用时,匿名状态变量将神奇地弹出。
sub something {
return $++;
}
say something for ^10;
数组 @
和哈希 %
版本也可用。 因此,在某种程度上,你可以根据需要拥有多个变量:
> %<name> = 12;
> %<city> = "Oslo";
这看起来类似于匹配项(请参见第11.9.1节"()(捕获)"),在这里我们可以使用 $[0] 获得第一个匹配项。 但是匿名状态数组中的第一个值是 @[0] 。
|
7.7. gather/take
让我们看看制作序列的另一种方法 gather
与 take
。
gather
以一个块作为参数,并收集(或“gather”)使用 take
指定的值。
.文件: gather1
my @a = gather {
take 1; take 5; take 42;
}
say @a;
$ raku gather1
[1 5 42]
请注意,所有值都是一次计算的。 我们可以这样做,但结果相同:
my @a = 1, 5, 43;
我们需要一个惰性序列,可以通过绑定到标量来完成: .文件: gather2
my $a := gather {
take 1 while 1;
}
say "1: $a[1]";
say "40: $a[40]";
say "4: $a[4]";
my $count = 0;
for $a -> $item {
last if $count++ >= 10;
say $item;
}
$ raku gather2
1: 1
40: 1
4: 1
1111111111
循环之前的三个 say
语句将其用作惰性列表,并根据需要将其扩展。
另一方面,循环产生一个序列,一旦消耗掉这些值就将其忘记(循环进入下一个迭代)。 因此,循环之后,$a
用尽,如果尝试访问它,它将失败。
我们可以删除花括号(如果需要),因为在该块中只有一个表达式:
my $a := gather take 1 while 1;
将序列赋值给数组不是一个好主意,因为它将尝试计算无限序列。 该代码将永远运行:
my @a = gather take 1 while 1;
my $flip-flop := (True, False, !* ... *);
say "I like potatos: $_." for $flip-flop
这里使用 gather/take .文件 flip-flop-gather
my $flip-flop := gather loop { take True; take False; }
say "I like potatos: $_." for $flip-flop;
我们可以改用状态变量:
my $flip-flop := gather loop { state $state = False;
$state = ! $state;
take $state;
}
say "I like potatos: $_." for $flip-flop
循环仍然存在,因此此版本可提供更多代码,而没有任何明显的好处。 但是我们也可以在此处添加随机起始值:
my $flip-flop := gather loop { state $state = (True, False).pick;
$state = ! $state;
take $state;
}
say "I like potatos: $_." for $flip-flop
这是我们之前没有显示的序列。 因此,让我们尝试按顺序进行操作。
我们可以缩短定义(在 «flip-flop-sequence» 中):
my $flip-flop := (True, False, !* ... *); # Old
my $flip-flop := (True, !* ... *); # New
接下来,我们将第一个值随机化:
my $flip-flop := ( (True, False).pick, !* ... *);
正如这个无限循环所示:
my $flip-flop := ( (True, False).pick, !* ... *);
say "I like potatos: $_." for $flip-flop
7.7.1. lazy vs gather/take
注意,如果代码只有一个 take
,则可以使用 lazy
(请参见16.2,“惰性”)代替 gather/take
。
lazy
版的 flop-gather2 看起来像这样
my $flip-flop := lazy loop {
state $state = False;
$state = ! $state;
}
say "I like potatos: $_." for $flip-flop;
这还是原始的,以方便比较:
my $flip-flop := gather loop { state $state = False;
$state = ! $state;
take $state;
}
say "I like potatos: $_." for $flip-flop
7.7.2. 一副牌
一副纸牌由 52 张纸牌组成,分别是黑桃,梅花,红桃和方块四种类型的各 13 张(值 1 至 13)。
Unicode 具有以下类型的字符:
for <♠ ♣ ♠ ♦> -> $type # spade, club, heart, diamond
但是它们很难打印,因此我们会坚持使用名称的首字母。
my @deck;
for <S C H D> -> $type # Spade, Club, Heart, Diamond {
@deck.push("$type$_") for 1 .. 13;
}
say @deck.join(",");
$ raku deck
S1,S2,S3,S4,S5,S6,S7,S8,S9,S10,S11,S12,S13,C1,C2,C3,C4,C5,C6,C7,C8,C9,C10,C11,C12,C13,
H1,H2,H3,H4,H5,H6,H7,H8,H9,H10,H11,H12,H13,D1,D2,D3,D4,D5,D6,D7,D8,D9,D10,D11,D12,D13
练习 16.2
使用 map
而不是两个循环重写 «deck» 程序。
练习 16.3
使用 gather/take
重写 «deck» 程序。
一副纸牌如果排序就没什么用了。 我们可以使用 pick(*)
来洗牌(以随机顺序返回值):
.文件: deck-random
my @deck;
for <S C H D> -> $type # Spade, Club, Heart, Diamond {
@deck.push("$type$_") for 1 .. 13;
}
say @deck.pick(*).join(",");
$ raku deck-random C6,S4,S6,H6,S1,C12,D8,C7,C8,S2,S5,C5,D13,H3,S7,S3,S11,H8,C4,H11,D9,D4,C10,D10,H9,D5,D3 ,H12,C11,D11,H7,D12,D6,S13,H4,C9,C2,S8,D2,D7,H10,C3,D1,S10,S9,H1,H2,H13,H5,C1,S12,C13
$ raku deck-random S6,H7,S5,D8,S1,H12,C1,H13,C13,C11,S10,C5,H8,D5,H10,D3,H2,S4,H11,C6,C3,D1,D12,D11,D2,S7 ,H5,D13,D9,S11,C12,C7,H4,S8,H1,D6,H3,C9,D7,D10,H9,S3,S9,H6,C10,S13,D4,S2,C4,S12,C2,C8
请注意,使用 pick (请参见8.22.2,"pick")从我们的牌组中选择一张牌不是一个好主意,因为它会将牌留在牌组中。 (以便我们稍后可以再次绘制它们。)
|
使用循环(for @deck ..
或在其上使用 shift
)。
另一种方法是使用 grab
,这将在《高级 Raku》课程中进行介绍。
7.7.3. take-rw
使用 take-rw
将给定项目返回到封闭的 gather
块,而无需引入新的容器。 它绑定到原始数组中的位置,并且可以更改:
my @flip-flop = |(True, False) xx 8; say @flip-flop;
sub flipflop (@list) {
gather for @list
{
take-rw $_;
}
}
for flipflop(@flip-flop) {
$_ = True if $_ == False;
}
say @flip-flop;
运行它:
$raku flip-flip
[True False True False True False True False True False True False True False True False]
[True True True True True True True True True True True True True True True True]
小心使用。 如果我们给它一个列表,它将失败(带有“不能为一个不可变的值分配”错误消息),因为我们不能更改不可变的值:
for flipflop((True, False, True, False)) {
$_ = True if $_ == False;
}
7.8. 闭包
我们在一个块中定义一个词法变量(一个my变量)。 该变量通常在块结束时会消失,但是窍门是我们在引用的过程中使用它(也超出了范围)。 此引用存储在此块外部。
在本书的前面,我们使用术语“关闭”来表示字符串中大括号内的文本。 那是一个 Raku 的名字,有点不幸。 但是另一方面,它是一种闭包,因为它会评估其中的表达式并将其返回到闭包死亡后继续存在的外部。 |
7.8.1. Flip-Flop
在讨论类之前,我们将讨论闭包(在下一章中)。 .文件: flip-flop-closure
sub gen-flip-flop {
my $state = Bool.pick;
sub flip-flop {
$state = ! $state; return ! $state;
}
return &flip-flop; 4
}
my $lunch = gen-flip-flop;
my $dinner = gen-flip-flop;
say "Lunch: " ~ $lunch();
say "Dinner: " ~ $dinner();
say "Dinner: " ~ $dinner();
say "Lunch: " ~ $lunch();
say "Lunch: " ~ $lunch();
我们建立了一个外部程序。
我们放置状态变量(现在是通常的 my
-variable)
及其内部的触发器程序。
此外部过程返回指向内部过程的指针(使用 &
前缀)。
然后,我们设置了生成器的两个实例。 变量包含指向过程的指针。
使用parens语法调用过程。…
练习 16.4
是否可以和/或明智地使用 take-rw
(请参见第16.7.3节"take-rw")来生成闭包?
7.8.2. 可选的开始值
初始值是完全随机的。 我们可以对其进行修改以采用可选的起始值: .文件: flip-flop-closure2 (diff)
sub gen-flip-flop (Bool $state is copy = Bool.pick)
my $dinner = gen-flip-flop(True);
7.8.3. 闭包 vs 类
每个闭包都可以由一个类替换,因此可以随意使用类。
但是事实却并非如此。 类比闭包可以做的更多。
8. 类
我们使用 class
关键字定义类:
class Person {
# 把内容放到这里
}
8.1. has
类变量是使用 has
关键字来声明的:
class Person {
has Bool $.is-taxpayer;
has Bool $!is-happy;
}
8.1.1. !(私有属性)
符号($
)和名称(在本例中为 is-taxpayer
)之间的感叹号(!
)表示属性(变量)是私有的,不能在类外部访问。
(实际上,我们可以为其他类提供显式访问;请参见第 17.13.1 节"trusts"。)
8.1.2. .(公共属性)
使用点(.
)而不是感叹号(!
)使属性(变量)公开。
Flip-Flop 作为一个类实现:
class Flip-Flop {
has Bool $!state = Bool.pick;
}
8.2. new
我们需要两个 flip-flop 实例,并使用每个类支持的内置 new
方法来获取它们。 我们忽略了给类变量赋值,它将默认为 True
或 False
的随机值:
class Flip-Flop {
has Bool $!state = Bool.pick;
}
my $lunch = Flip-Flop.new();
my $dinner = Flip-Flop.new();
8.2.1. 存根类
我们必须声明一个类,然后才能使用(或引用)它。 如果我们希望类之间互相引用,那将是一个问题。
一种解决方案是前向类声明(也称为«存根类», 因为我们使用的是 10.12.1 "存根运算符")。 看起来像这样:
class Parent { ... }
只要我们在使用类之前指定了类的内容,就可以使用存根类。
另一个解决方案是确保我们以正确的顺序指定它们,以便在我们声明它们之前不引用它们。
当我们具有相互依赖性(或循环数据结构)时,必须使用存根类:
class Parent { ... }
class Child { has Parent $.parent; }
class Parent { has Child $.child; }
8.3. method
我们需要一种提供状态并翻转存储值的方法。 我称它为 «flip-flop»:
class Flip-Flop {
has Bool $!state = Bool.pick;
method flip-flop {
$!state = ! $!state;
return $!state;
}
}
8.3.1. .(方法调用)
然后,我们将不得不重写 «free-lunch» 和 «free-dinner» 程序。
我们使用 .
以及在对象($lunch
)上的方法名称(.flip-flop
):
sub free-lunch {
say "I'm { $lunch.flip-flop ?? "for" !! "against" } free lunches";
}
sub free-dinner {
say "I'm { $dinner.flip-flop ?? "for" !! "against" } free dinners";
}
free-lunch; # True
free-dinner; # True
free-dinner; # False
free-dinner; # True
free-lunch; # False
free-lunch; # True
free-dinner; # False
free-lunch; # False
我已将结果添加为注释,以表明程序可以按预期工作。
完整程序如下:
class Flip-Flop {
has Bool $!state = Bool.pick;
method flip-flop {
$!state = ! $!state;
return $!state;
}
}
my $lunch = Flip-Flop.new();
my $dinner = Flip-Flop.new();
sub free-lunch {
say "I'm { $lunch.flip-flop ?? "for" !! "against" } free lunches";
}
sub free-dinner {
say "I'm { $dinner.flip-flop ?? "for" !! "against" } free dinners";
}
free-lunch; # True
free-dinner; # True
free-dinner; # False
free-dinner; # True
free-lunch; # False
free-lunch; # True
free-dinner; # False
free-lunch; # False
8.3.2. 冒号语法
还有一种用于调用方法的冒号语法,使它们看起来像过程调用:
$lunch.flip-flop;
flip-flop($lunch:); # The same
如果方法接收参数,则只需添加它们即可:
$obj.method(1);
method($obj:, 1); # The same
8.4. 命名参数
默认构造函数(new
方法)仅支持命名参数。 因此,如果要覆盖随机初始值,则必须这样做:
my $lunch = Flip-Flop.new(state => False);
my $dinner = Flip-Flop.new;
但这是行不通的。 初始值仍然是随机的。
8.5. 公开的类变量
内置的默认构造函数仅适用于公共变量,因此我们必须将变量从私有更改为公开:
has Bool $.state = Bool.pick; # public
请注意,公共变量对公众可见(类外部的程序代码):
say $lunch.state;
因此,这通常不是一个好主意。
我们可以读取公共对象变量,但是不能更改它们。
8.5.1. is rw
我们可以允许使用 is rw
(读写)trait 来更改公共变量,如下所示:
has Bool $.state is rw = Bool.pick;
对于我们而言,这将是一个坏主意。
8.6. self
如果你需要访问方法中的当前对象,请使用 self
。
有关示例,请参见下一部分。
8.7. 自定义 «new»
如果我们可以从外部访问对象变量是一个问题,请将它们设为私有并编写一个自定义的 new
构造函数:
class Flip-Flop {
has Bool $!state;
method new (:$start = Bool.pick) {
self.bless(state => :$start);
}
}
我已将变量更改为私有,并将默认值移至 new
方法,以便它可以处理和不使用参数。
我还将构造函数中使用的名称从 «state» 更改为 «start»,因为从用户角度来看,这是一个更好的名称。 在类内部,«state» 很有意义。
my $lunch = Flip-Flop.new(start => False);
my $dinner = Flip-Flop.new;
8.7.1. bless
bless
是低级对象构造函数。 它创建一个与请求者相同类型的新对象(给与 self
),并填充命名的参数。
这对于自定义构造函数很有用(作为上一节中的 new
方法)。 但是自定义 Build
方法(请参阅下一节)通常更好。
8.8. 自定义 BUILD
我们可以尝试使用 BUILD
代替,默认情况下调用 new
:
class Flip-Flop {
has Bool $!state;
submethod BUILD (:$!state = Bool.pick) { }
}
BUILD
也必须处理缺少的值,因此我们将默认初始化移到那里。 BUILD
没有正文,因为具有相同名称的变量会自动映射。 因为我一直保持名字状态。
submethod
是一种 method
,但子类不会继承它。 有关更多信息,请参见 17.14.3,"submethod" 和 17.14, "继承"部分。
8.9. 错误的起始值
«flip-flop» 方法翻转值,然后返回。 结果是我们获得的第一个值与我们指定为起始值的相反。
正常的解决方案是在更改值之前保存该值,但是由于我们有一个布尔值,所以我们可以避免反转(翻转)返回的值。
method flip-flop {
$!state = ! $!state;
return ! $!state;
}
很多感叹号!
完整程序:
class Flip-Flop {
has Bool $!state;
method flip-flop {
$!state = ! $!state;
return ! $!state;
}
method new (:$start = Bool.pick) {
self.bless(state => $start);
}
}
my $lunch = Flip-Flop.new(start => False);
my $dinner = Flip-Flop.new;
sub free-lunch {
say "I'm { $lunch.flip-flop ?? "for" !! "against" } free lunches";
}
sub free-dinner {
say "I'm { $dinner.flip-flop ?? "for" !! "against" } free dinners";
}
free-lunch;
free-dinner;
free-dinner;
free-dinner;
free-lunch;
free-lunch;
free-lunch;
free-dinner;
8.10. 对象比较
如果我们比较两个对象,我们在比较什么? .文件: flip-flop-test (partial)
my $lunch = Flip-Flop.new(start => False);
my $dinner = Flip-Flop.new(start => False);
say $dinner eq $lunch; # -> False
答案:我们检查它们是否是同一对象。 在这种情况下,显然不是。
8.10.1. eqv(等价)
我们可以将它们与等价运算符 eqv
进行比较。
如果两个参数的类型,结构和值相同,则此运算符返回 True
:
say $dinner eqv $lunch; # -> True
这些对象可能具有相同的值,但它们不是同一对象。 想像一下,在同一所幼儿园有两个名为 «Fred» 的孩子。 他们不是同一个孩子,无论同一个名字。
my $a = Person.new(name => "Arne");
my $b = Person.new(name => "Bente");
say $a eq $b; # -> False
my $c = $a
say $a eq $c # -> True
$c.name = "Charlie";
say $c.name # -> Charlie
say $a.name # -> Charlie
在下一节中,我们将回到本类。
在对象列表上使用 |
8.10.2. ===
使用值标识运算符 ===
来检查两个参数是否是同一对象,而无需考虑容器化:
my class A { };
my $a = A.new;
say $a === $a; # -> True
say A.new === A.new; # -> False
say A === A; # -> True
my $b := $a;
say $a === ba; # -> True
有关在值上使用 ===
的说明,请参见第 3.7.7节 "==="。
8.11. Person 类
上一节中提示的 «Person» 类如下所示:
class Person {
has Str $.name; # ①
has Str $.birtdate; # ②
has Person $.father; # ③
has Person $.mother; # ④
has Person $.spouse; # ⑤
has Person @.child; # ⑥
method new (:$name, :$birthdate) {
self.bless(:$name, :$birthdate); 11
}
}
my $tom = Person.new(name => "Tom", birthdate => "12 Jan 1970"); # ⑦ ⑧
my $lisa = Person.new(name => "Lisa", birthdate => "21 Mar 1969");
my $john = Person.new(name => "John", birthdate => "5 Apr 1998");
my $peter = Person.new(name => "Peter", birthdate => "23 Oct 2001");
my $mary = Person.new(name => "Mary", birthdate => "12 Mar 2000");
say $tom.birthdate; # 12 Jan 1970 # ⑨
say $mary.name; # Mary # ⑨
① 一个公共变量,因此默认的 "new" 将为我们处理。 ② 如上所述。 也是一个字符串,因为我们还没有讨论日期(日期对象)。 ③ 一个人有一个(或没有)父亲。 ④ 一个人有一个(或没有)母亲。
⑤ 一个人有一个(或没有)配偶。 ⑥ 一个人没有一个或多个孩子。 ⑦ 最后,我们设置了5个 Person 对象。 ⑧ 我们使用默认的 «new» 构造函数,并忽略关系字段。 ⑨ 只是为了证明它确实有效。 10我已经编写了一个构造函数,因此我们无法指定关系字段。 11当字段与变量具有相同的名称时,我们可以使用这种缩写形式。
我本可以将关系字段设为私有(例如,"$!father" 而不是 "$.father"),而不是编写新的构造函数,以使其无法在 "new" 调用中进行设置。
但这会限制我们以后的工作,因此我们会坚持与公众保持联系。
练习 17.1
编写以下方法 «set-father», «set-mother», 和 «set-spouse» 以设置各个字段。 忽略 @.child
字段。
添加以下代码以检查其是否正常运行(无错误执行):
$tom.set-spouse($lisa);
$lisa.set-spouse($tom);
$john.set-father($tom);
$john.set-mother($lisa);
$peter.set-father($tom);
$peter.set-mother($lisa);
$peter.set-spouse($mary);
$mary.set-spouse($peter);
say $tom.spouse.name;
say $john.mother.name;
数据结构/依赖关系应如下所示:
如果丽莎有一个父亲,我们可以使用 «$john.mother.father.name» 来获得他的名字。 她没有(在我们的数据结构中),因此该程序将终止。
$tom.spouse.name; # -> lisa
$tom.father.name; # -> program termination
不是很健壮。 这表明我们确实不应该直接访问字段(而是使用方法)。 我们稍后会再讲。
在我们的班级中,配偶是一对一的关系,我们通过分别指定两个方向在数据结构中将其设置为双向关系。 可能会搞砸这样的事情:
$peter.set-spouse($mary);
$mary.set-spouse($tom); # Tom vs Peter
$peter.spouse.name; # -> Mary
$peter.spouse.spouse.name; # -> Tom
$peter.spouse.spouse.spouse.name; # -> Lisa
练习 17.2 简化 «set-spouse» 以建立两个关系,以便我们可以这样做:
$tom.set-spouse($lisa);
# $lisa.set-spouse($tom);
$peter.set-spouse($mary);
# $mary.set-spouse($peter);
添加此行以检查其是否有效:
say $lisa.spouse.name; # Tom
练习 17.3 编写方法 "add-child" 以添加到 "child" 字段。
添加以下代码以检查其是否正常运行(无错误执行):
$tom.add-child($john);
$tom.add-child($peter);
$lisa.add-child($john);
$lisa.add-child($peter);
编写显示它们的方法 «show-children» ,并像这样使用它:
$tom.show-children;
$mary.show-children;
这应该给出以下输出:
Tom has a child named John.
Tom has a child named Peter.
Mary has no children.
孩子们的清单就是一个列表。
$tom.add-child($peter);
$tom.add-child($peter);
$tom.add-child($peter);
$tom.add-child($peter);
防止重复的最简单方法是在将新子项添加到列表之后,在列表中应用唯一性:
method add-child (Person $child) {
@!child.push($child);
@!child.=unique;
}
这样只会删除重复的对象(请参见 17.10,"对象比较"),因此可以让多个人同名(尽管众所周知,在实践中会造成混淆)。
练习 17.4 就像我们对配偶所做的那样,简化了父子关系的设置。
重写 «set-father» 和 «set-mother» 以为我们调用 «add-child» ,以便我们可以删除程序中的 «add-child» 调用。
8.12. 输出
如果我们尝试打印对象会怎样?
我们可以试试。 我写了一个小程序来做那个(作为 «person-say» 用)。 它所做的只是定义 «Person» 类,设置 «Tom»,然后这些行:
say $tom;
say $tom.raku;
say $tom.gist;
say $tom.Str;
8.13. 私有方法
方法可以是私有的, 只要在方法名前面加上一个感叹号 !
:
method !explode { ... }
在类本身之外不能调用它们。 在类内部这样调用他们:
self!explode;
8.13.1. trusts
我们可以允许其他类访问类中的私有方法和属性。
我们要信任的类必须已经声明,并且可以使用 trusts
关键字为其授予访问权限:
class Owner { ... }; ①
class Car { ①
trusts Owner;
has Str $.type = "no name";
method !sell { say "The $.type is sold." } ②
}
class Owner { ①
has Str $.name = "no name";
has Car $!car = Car.new(type => "Volvo X1"); ④
method sell-car { $!car!Car::sell; } ⑥
method get-car { $!car; }
}
my Owner $o = Owner.new(name => "Tom Jones"); ③
$o.sell-car; ⑤
say $o.get-car.type; ⑦
# $o.get-car!sell;
# $o.get-car!Car::sell;
① Owner
类引用 Car
类,因此必须首先声明 Car
。 这带来了一个问题,因为 trusts Owner
要求必须首先声明 Owner。 但是,我们可以摆脱存根类。
② `Car
类具有允许 Owner
调用的私有方法 sell
。
③ 我们建立了一个新的 Owner
对象,
④ 这也设置了一个新 Car
,由 Owner
链接。
⑤ 然后,我们从 Owner
对象出售汽车。
⑥ 我们必须在方法(sell
)之前加上完整的类名(Car::sell
)。
⑦ 我们可以访问汽车类型,因为这是公共属性。
# $o.get-car!sell;
在程序中取消注释此行会产生错误:
===SORRY!=== Private method call to sell must be fully qualified with the package
containing the method
我们可以尝试解决问题(使用类名),然后取消注释此行:
# $o.get-car!Car::sell;
它也失败了:
===SORRY!=== Cannot call private method 'sell' on package Car because it does not
trust GLOBAL
语法正确,但是主程序不受信任。 错误消息给出了提示,因此我们可以尝试:
class Car {
trusts Owner;
trusts GLOBAL;
...
}
通过此更改,它可以工作。 现在,我们可以从本身(Car
类),Owner
类和主程序出售汽车,但不能从其他任何类别(如果有)出售汽车。
8.13.2. trusts(方法)
使用 trusts
作为获取发起者信任的类的列表的方法。
我们可以将以下代码添加到程序中:
print "Owner trusts:"; print " " ~ .^name for Owner.^trusts; say "";
print "Car trusts:"; print " " ~ .^name for Car.^trusts; say "";
结果如下:
Owner trusts:
Car trusts: Owner
8.14. 继承
继承是类中代码重用的主要机制。 一个类(称为子类)可以从一个或多个类(称为父类或基类)继承。
父类中的所有子元素(子方法(请参见下文)和私有方法除外)均被继承。 如果子类使用相同的名称定义属性或方法,则使用此版本。
8.14.1. is
继承是通过 is
关键字指定的。
让我们回顾一下 «Person» 课程。 我们可以添加几个新类,像这样重用它:
class Adult is Person {
has Str $.employer;
}
class Child is Person {
has Str $.school;
}
class Pensioner is Person {
;
}
一个类可以从一个以上的父类中继承,或者通过从本身使用继承的类中继承(依此类推)。
或直接这样:
class SeaPlane is Plane is Boat {
has Int $.pontoons;
}
继承循环是我们已知的问题,最终我们从自身继承。 我们无法从尚未声明的类中继承(就像使用 trusts
一样;请参见第17.12.1节 "trusts"),这一事实使得很难犯此错误。
存根类将不起作用:
class Mammal { ... }; ①
class Person is Mammal { has Str $.name; } ②
class Mammal is Person { has Str $.city; } ③
① 我们必须存根,因为 «is Person» 会失败 ② 由于尝试从存根类继承而失败 ③ 我们永远不会走这么远
如果删除第2行,则由于 is Person
部分而导致错误。 我们无法在存根类上添加继承:
===SORRY!=== Redeclaration of symbol 'Mammal'
因此,不可能建立循环继承。
8.14.2. also is
我们也可以在类主体中使用 also is
,而不是在头部中使用 is
:
class SeaPlane {
also is Plane;
also is Boat;
has Int $.pontoons;
}
多重继承的问题是,如果我们继承的两个类都添加了一个具有相同名称的方法,该怎么办? 我们应该使用哪个? Raku 没有让程序员影响其选择操作的机制。 这是建议改用角色的主要原因(请参见第17.14节"角色")。 |
8.14.3. submethod
submethod
是一种方法,但子类不会继承它。 (名称是指 submethod
的作用域与过程(子)相同的事实。
它通常用于自定义 BUILD
方法(如第17.7节"自定义 BUILD" 中所述)。
如果我们要确保必须为子类指定方法,它们也可能很有用,例如父版本不合适,并且你希望将其作为公共方法使用。
class Person {
has Str $.birthdate; # On the form "yyyy-mm-dd"
submethod age { Int((now.Date - Date.new($!birthdate))/365) }
}
class Woman is Person {
;
}
向女人询问年龄的情况会引起例外。
练习17.5
我们计算一个人的年龄的方法是错误的(因为我们假设每年有365天)。 修理它。
8.14.4. rebless
使用 Metamodel::Primitives.rebless
方法更改对象的类型。 仅当新类型是对象原始类型的子类型时,此方法才有效。
让我们回顾一下 Person/Woman 的例子,将其简化为最低限度:
class Person { ; }
class Woman is Person { ; }
my $tom = Person.new;
my $lisa = Woman.new;
say $tom.^name; # -> Person
say $lisa.^name; # -> Woman
Metamodel::Primitives.rebless($tom, Woman);
# -> New type Woman for Person is not a mixin type
say $tom.^name; # -> Woman
## See https://stackoverflow.com/questions/44486985/how-can-i-rebless-an-object-in-perl-6
## https://stackoverflow.com/questions/59845201/raku-rebless-doesnt-work-with-inhertited-classes-anymore
待办事项:这曾经有用,但现在已经不行了(Raku 2019.11)。 |
$ raku rebless
Person
Woman
New type Woman for Person is not a mixin type
in block <unit> at rebless line 12
8.15. 角色
角色使我们可以将属性和方法附加到类上,而无需继承。 当角色的内容是类唯一的共同点时,这很有用。
它们表现为一种宏,将其添加到类中(或以普通的 OO 术语混入),它们成为类的一部分-我们无法检测到它们是作为角色添加而不是被添加的。 在类本身中声明。
一个例子:
role Doors {
has Int $.number-of-doors;
}
8.15.1. does(对象)
使用 does
关键字将角色添加到类中:
class Car does Doors {
has Int $.wheels;
has Bool $.has-automatic-transmission;
}
class House does Doors {
has Int $.floors;
has Int $.rooms;
}
可以将角色应用于对象(在运行时): |
role Windows {}
my $c = Car.new; # -> Car.new
$c does Doors; # -> Car+{Doors}.new
$c does Windows # -> Car+{Doors}+{Windows}.new
8.15.2. also does
我们也可以在类主体中使用 also does
,而不是在 head 中使用 does
:
class Car {
also does Doors;
has Int $.wheels;
has Bool $.has-automatic-transmission;
}
class House {
also does Doors;
has Int $.floors;
has Int $.rooms;
}
8.15.3. but(对象)
在第3.8节"but(True 和 False, but …)"中,我们展示了 but
(和 does
)如何与其他标量值上的标量值一起使用。 应用除布尔值以外的任何内容都无法真正解决。
我们可以以相同的方式应用角色。
my $a = 41 but Doors;
say $a ~ " " ~ $a.doors();
my $b = 42;
say $b ~ " " ~ ( $b.^can("doors") ?? $b.doors() !! "-" );
but 关键字与 does 相似。 区别在于 does 会将其添加到给定的变量,类或对象中,而 but 将其应用于其副本。 在处理对象时,我们在类内部使用 does ,但在对象上使用 but 。
|
8.15.4. ^can
$b
没有角色,并且在其上调用 .doors
将终止程序。 我们可以通过在调用该方法之前检查该方法是否存在(使用 .^can
方法)来避免这种情况。
运行它:
$ raku but-role
41 No doors
42 -
使用 but 将角色应用于具有 REPL 的对象或值,但在 REPL 中不起作用
|
8.16. 多重分派
我们可以使用 multi
关键字指定方法的不同版本(与过程一样;请参见第 10.12 节"多重分派"),并带有不同的参数列表(或"签名"):
multi method do-something ($file1) { ... }
multi method do-something ($file1, $file2) { ... }
除了我们从类中获得的内置分派机制之外,还可以进行多种分派,因为我们可以在不同类的对象上使用相同的方法名称,并使它们的行为有所不同。 |
class Fly {
# Attributes
method kill { say "Fly killed"; }
}
class Process {
# Attributes
method kill { say "Process killed"; };
}
my $a = Fly.new;
my $b = Process.new;
.kill for $a, $b;
运行它:
$ raku class-multi
Fly killed
Process killed
不要忘记 method 关键字,因为 multi 本身就是 multi sub 的缩写。
|
8.17. Fallback 方法
我们可以用特殊的 FALLBACK
名称定义一个方法,如果我们调用一个不存在的方法,它将被使用:
class Stupid {
method FALLBACK ($name) {
say "You invoked $name, but it doesn't exist.";
}
method hello {
say "Hi.";
}
}
my Stupid $s = Stupid.new;
$s.some-method-that-doesn't-exist;
$s.hello;
$s.hi;
运行它:
$ raku fallback
You invoked some-method-that-doesn't-exist, but it doesn't exist.
Hi.
You invoked hi, but it doesn't exist.
如果没有 FALLBACK 方法,则调用不存在的方法将产生运行时错误。
|
如果我们实际上是想调用一个现有的方法但是却键入错误,那么拥有 FALLBACK
方法将隐藏此错误消息。
我们也可以有论点。 只需在带有方法名称的参数后指定它们(通常称为 $name
)。
multi method
支持不同的签名:
class Stupid {
multi method FALLBACK ($name, Str $person) {
say "Hi, $person.";
}
multi method FALLBACK ($name) {
say "You invoked $name, but it doesn't exist.";
}
method hello {
say "Hi.";
}
}
my Stupid $s = Stupid.new;
$s.some-method-that-doesn't-exist;
$s.some-method-that-doesn't-exist("Tom");
$s.hello;
$s.hi;
运行它:
$ raku fallback-multi
You invoked some-method-that-doesn't-exist, but it doesn't exist.
Hi, Tom.
Hi.
You invoked hi, but it doesn't exist.
我们可以使用 slurpy 参数来捕获所有内容(请参见10.14.1节"Slurpy MAIN"):
class Stupid {
multi method FALLBACK ($name, *@arguments) {
say "Method: $name";
say "- Arg: $_" for @arguments;
}
}
my Stupid $s = Stupid.new;
$s.some-method-that-doesn't-exist(<1 2 3>);
$s.hello;
$s.hi(706);
运行它:
$ raku fallback-catchall
Method: some-method-that-doesn't-exist
- Arg: 1
- Arg: 2
- Arg: 3
Method: hello
Method: hi
- Arg: 706
8.18. .?
我们可以使用特殊的 .?
如果不确定方法是否可用于对象,则调用方法。 如果是,则执行;如果不是,则返回 Nil
。
class A {};
my $a = A.new;
say $a.foo-bar; # -> No such method 'foo-bar' for invocant of type 'A'
say $a.?foo-bar; # -> Nil
8.19. .+
如果我们有一个子类在基类中重新定义了一个方法,则在子类的对象上调用该方法将使用子类版本。 我们可以使用 .+
语法来调用它们:
class A {
method hi { say "Hi!"; }
}
class B is A {
method hi { say "Hello!"; }
}
my $x = B.new;
$x.hi;
say "....";
$x.+hi;
运行它:
> raku all-methods
Hello!
....
Hello!
Hi!
如果该方法不存在,则会引发异常。
我们忽略了返回值,但是它们是可用的。 最后一个给出了一个列表,在这里值是(True
True
)。
调用的顺序是从当前类开始,然后嵌套。 我们可以使用 ^mro
方法(请参见第3.2节"`^mro`(方法解析顺序)”)以查看“方法解析顺序”:
my $x = B.new;
say $x.^mro; # -> ((B) (A) (Any) (Mu))
8.20. .*
与 .+
一样,不同之处在于如果该方法不存在(而不是引发异常),它将返回一个空列表。
8.21. handles(代理)
代理是在类之间建立关系的另一种方法。 我们使当前类中的另一个类的方法可用(一种导入)。
class Baby {
has $.name;
method cry($times) { say "Waah! " x $times; }
}
my $tim = Baby.new(name => 'Tim');
say $tim.name; # -> Tim
$tim.cry(5); # -> Waah! Waah! Waah! Waah! Waah!
class BabySitter {
has $.name;
has Baby $.baby handles (baby_name => 'name'); ① ②
}
my $teenager = BabySitter.new(name => 'Lisa', baby => $tim); ③
say $teenager.name; # -> Lisa
say $teenager.baby_name; # -> Tim ③
① «BabySitter» 类具有 «Baby» 属性,
②,我们提供了一种方法 «baby_name» 变量。 它被(通过 handles
)委托给 «Baby» 类中的 name
方法。
③ 调用它。
请注意,我们必须重命名方法(为 "«baby_name»"),因为它们都使用 "name"。 如果方法名称不冲突,我们可以改为:
has Baby $.baby handles 'name';
我们也可以继承几种方法:
as Baby $.baby handles <name bedtime diaper-type>';
代理的替代方法是直接访问对象。 例如。 |
say $teenager.baby.name; # -> Tim
8.22. 调用在变量中指定的方法
我们可以通过将变量放在双引号中(进行插值),然后添加 ()
来调用指定为字符串(在变量中)的方法:
say "{pi}.{$_}: " ~ pi."$_"() for <Int Real Str>;
# 3.141592653589793.Int: 3
# 3.141592653589793.Real: 3.141592653589793
# 3.141592653589793.Str: 3.141592653589793
8.22.1. 存根类
我们必须声明一个类,然后才能使用(或引用)它。 如果我们希望类之间互相引用,那将是一个问题。
一种解决方案是前向类声明(也称为"存根类",因为我们使用的是 10.12.1 "存根运算符")。 看起来像这样:
class Parent { ... }
只要我们在使用类之前指定了类的内容,就可以使用存根类。
另一个解决方案是确保我们以正确的顺序指定它们,以便在我们声明它们之前不对其进行引用。
当我们具有相互依赖性(或循环数据结构)时,必须使用存根类:
class Parent { ... }
class Child { has Parent $.parent; }
class Parent { has Child $.child; }
9. Docker
Docker(一种轻量级的容器技术)是获取和运行Raku(Windows除外)的最简单方法,至少仅用于测试。 (当然,如果你不想在系统上安装Raku。)
由于Raku的开发非常活跃,每月发布一次,因此在升级本地安装的Raku版本之前,最好先通过Docker检查具有较新版本的程序。
如果你不想安装Rakudo Star,那么使用Docker是一个不错的选择。
我不建议在 Windows 上使用 Docker,因为它需要 Win 10 Pro。 使用 VirtualBox 是一种解决方法。 |
如果你仍然想去,请参阅 https://docs.docker.com/docker-for-windows/install/。
9.1. 使用 Docker 安装 Rakudo Star
有一些 Rakudo 可公开获得的 Docker 镜像:
镜像名 |
操作系统 |
URRL(获取更多信息) |
rakudo-star |
Ubuntu (Linux) |
|
jjmerelo/alpine-perl6 |
Alpine (Linux) |
|
moritzlenz/perl6-regex-alpine |
Alpine (Linux) |
|
jjmerelo/rakudo-nostar |
Debian (Linux) |
|
jjmerelo/perl6-doc |
Debian (Linux) |
Ubuntu 版本是一个相当大的 Ubuntu 系统,而 Alpine 版本则是一个更加紧凑的发行版。 Alpine 应该更快,并使用更少的内存。 但是它有一些限制,我们稍后会再讲。
我们可以通过一个操作下载并运行 Docker 映像(假设首先安装了 Docker):
$ docker run -it rakudo-star
Unable to find image 'rakudo-star:latest' locally
latest: Pulling from library/rakudo-star
693502eb7dfb: Already exists
081cd4bfd521: Pull complete
c3439586dbe8: Pull complete
Digest: sha256:eac1ce2634c62857ee7e5e3f23b215 ...
Status: Downloaded newer image for rakudo-star:latest
To exit type 'exit' or '^D'
首先,它会检查镜像是否已下载,如果尚未下载,则进行下载。 然后在运行容器之前进行校验和测试。
最后一行是 REPL 提示。
如果收到错误消息(权限被拒绝),请以超级用户身份运行程序: |
$ sudo docker run -it rakudo-star
或修复权限:
$ sudo usermod -a -G docker $USER
你必须注销然后再次登录才能使此更改生效。 如果不起作用,请重新启动。
或者,如果你想使用 Alpine:
$ docker run -it jjmerelo/alpine-perl6
To exit type 'exit' or '^D'
Docker 首次运行此命令时将下载指定的 Docker 镜像,然后将使用本地副本。 |
使用 docker pull
检查是否有可用的较新版本,然后下载该版本:
$ docker pull rakudo-star
$ docker pull jjmerelo/alpine-perl6
9.1.1. docker shell
我们已经展示了如何使用 Docker 在 REPL 模式下运行 Raku。
但是可以登录到容器,以便你可以运行程序。 使用此命令:
$ docker run -it -v $(pwd):/opt rakudo-star bash
这将在 Docker 文件系统内为你提供 bash(shell) 提示,并且你运行命令的目录为 /opt。
只需转到该目录并运行它们,即可将其用于测试本地程序。
请注意,这不适用于 Alpine(因为"bash"在容器中不可用)。 因此,如果你想运行 «bash»,请使用 Ubuntu。 |
也可以直接运行本地程序。 例如。 当前目录中的 "hello-world" 程序:
$ docker run -it -v $(pwd):/opt rakudo-star /opt/hello-world
Hello, World!
请注意,当前目录(在 Docker 内部)不会设置为 /opt
,如果程序假定它是从其所在的目录运行的,则可能会导致问题。
10. 答案
10.1. 第一章
10.2. 第二章
10.3. 第三章
10.4. 第四章
10.5. 第五章
10.6. 第六章
10.7. 第七章
10.8. 第八章
10.9. 第九章
10.10. 第十章
10.11. 第十一章
10.12. 第十二章
10.13. 第十三章
10.14. 第十四章
10.15. 第十五章
10.16. 第十六章
10.17. 第十七章
11. 小心
有许多重要的注意事项。
11.1. length
没有 «length» 方法。
在字符串上使用 chars
(查看 7.1.1 "chars")方法, 在列表上使用 elems
(查看 8.7.1, "elems(列表大小))方法。
11.2. 对象不是字符串
匹配对象在执行任何操作之前应转换为字符串。
IO 对象也应如此。
$*TMPDIR; # "/tmp".IO
$*TMPDIR.say; # "/tmp".IO
$*TMPDIR ~ "/myfile"; # /tmp/myfile
它们可能会自动转换为字符串,也可能不会自动转换为字符串。
11.3. 另请参考
11.4. 语法汇总
程序使用的其他特殊字符的摘要; 在头部,身体或调用(调用)中。
With variables
语法 |
位置 |
描述 |
查看所在章节 |
:$a |
Head |
命名参数 |
10.13.4, "具名参数" |
:$a! |
Head |
强制命名参数 |
10.13.5, "强制命名参数" |
:$a |
Invocation |
命名参数 |
10.13.4, "命名参数" |
$:a |
Body |
命名占位符变量 |
10.4.1, "命名占位符变量" |
$a: |
Invocation |
用过程语法调用的方法 |
17.2.2, "冒号语法" |
With Names Arguments
语法 |
位置 |
描述 |
查看所在章节 |
:a |
Invocation |
命名参数为 |
10.13.6, "副词" |
:!a |
Invocation |
命名参数为 |
10.13.6, "副词" |
短形式的 $, @ 和 %
语法 |
描述 |
查看所在章节 |
$<aa> |
查找命名匹配 |
查看 «高级 Raku» 课程 |
%<aa> |
在状态散列 |
16.6.2, "$/@/%(匿名状态变量)" |
其它
语法 |
所在位置 |
描述 |
查看所在章节 |
FOOBAR: |
在循环里面或前面 |
一个标签 |
4.17.5, "标签" |
:12("12345") |
任何地方 |
12 进制数 |
5.1, "八进制,十六进制,二进制…" |
:13<12345> |
任何地方 |
13 进制数 |
5.1, "八进制,十六进制,二进制…" |
FOO.BAR: 12, 13; |
方法调用 |
传递参数 |
12.2.2, "冒号语法" |
12. Raku 背景和历史
Perl 1 至 4 版本从 1987 年至 1993 年发布。
Perl 5 版于 1994 年发布。它是一个完全重写,并且所有新功能都终止了 Perl 4 的使用。 今天,Perl 5 正在积极开发中。
Perl 版本 6 是 2000 年构想的。它首先邀请 Perl 社区提出更改建议。 这个过程非常耗时,因为一切都是(现在仍然是)由志愿者完成的。
它原本是 Perl 的新版本,但是多年来,很明显 Perl 5 将继续存在-并且正在积极维护中。
首席开发人员于 2019 年 10 月做出将语言重命名为 Raku 的决定,但要完全实施更改尚需时日。
12.1. 6.a 和 6.b
Alpha 和 Beta 版本分别称为 "6.a" 和 "6.b"。
请勿使用它们,因此,如果你有其中之一,请升级。
12.2. 6.c
第一个稳定版本(版本 6.c)于 2015 年 12 月 25 日发布。
12.3. 6.d
下一版本(6.d)于 2018 年 11 月发布。
12.4. 6.e
该版本尚未发布,但是正在开发中。
12.5. 关于版本
对于 Perl 5(和大多数其他解释语言),你只有一个版本。 如果你升级到较新的版本,那么你已经拥有了。 对于已安装的模块也是如此。
在 Raku 中,你可以安装一个模块的多个版本。 默认情况下,程序将使用最新版本(版本号最高的版本),但是你可以选择显式使用特定版本。
这也适用于语言本身。 6d 具有一些破坏与 6c 兼容性的新功能。 你可以通过显式执行旧的 6c 语义:
use v6.c;
如果这样做,代码将永远锁定在该版本上。 如果你使用模块,请注意,较新的版本(因为你对其进行了升级)可能会使用 6d 中的功能,并且可能会 use v6d;
语句。
这样可以解决问题,因为程序的不同部分最终可能使用同一模块的不同版本。 但是,这可能是开发人员的噩梦。
Rakudo 实现在主要版本之间获得了新功能,但是重大更改隐藏在 «PREVIEW» 标签之后。 因此,如果你想使用将成为 «6.e» 版本一部分的内容,请使用以下命令:
use v6.e.PREVIEW;
请勿在生产代码中使用该代码。 发布 «6.e» 后,你应该更新 use
语句:
use v6.e;
请注意,如果指定版本,则代码将在该版本下运行。 |
如果你未指定版本,则将获得最新版本(«PREVIEW» 版本除外)。