Raku 入门

on

本文档旨在为你提供 Raku 编程语言的快速概述。 它可以让 Raku 初学者快速上手。

本文档的一部分章节涉及 Raku 文档的其他(更完整和准确)部分。 如果你需要有关特定主题的更多信息,你应该阅读它们。

在本文档中,你会找到大部分所讨论主题的示例。 为了更好地理解它们,请花时间再现所有的例子。

许可证

本工作根据知识共享署名 - 授权4.0国际许可协议进行许可。 要查看此许可证的副本,请访问

贡献

如果你想对此文档做贡献,请访问:

意见反馈

欢迎所有的反馈: naoum@hankache.com

如果你喜欢这个工作, 欢迎在 Github 给这个仓库点赞。

1. 序言

1.1. 什么是 Raku

Raku 是一种高级的,通用的,渐进类型的语言。 Raku 是多范式的。它支持过程式编程,面向对象编程和函数式编程。

Raku motto:
  • TMTOWTDI (发音是 Tim Toady): 每个问题都有许多解决方式。

  • 简单的事情应该保持简单,困难的事情应该变得简单,不可能的事情应该成为可能。

1.2. 术语

  • Raku: 带有测试套件的语言规范。 Raku 是通过该规范测试套件的实现。

  • Rakudo: Raku 的编译器。

  • Rakudobrew: Rakudo 的安装管理器。

  • Zef: Raku 的模块安装程序。

  • Rakudo Star: 是一个包含 Rakudo, Zef, 和经遴选的 Raku 模块与文档的分发包。

1.3. 安装 Raku

Linux
  1. 安装 Rakudo Star: 在终端中运行:

wget https://rakudo.perl6.org/downloads/star/rakudo-star-2019.03.tar.gz
tar xfz rakudo-star-2019.03.tar.gz
cd rakudo-star-2019.03
perl Configure.pl --gen-moar --make-install --prefix ~/rakudo
OSX

可用的选择有四种:

  • 按照在 Linux 上安装步骤进行同样的操作

  • 使用 homebrew 安装: brew install rakudo-star

  • 使用 MacPorts 安装: sudo port install rakudo

  • https://rakudo.org/latest/star/macos 下载最新的安装器(.dmg 后缀的文件)

Windows
  1. 从 或 下载最新的安装器(.msi 后缀的文件)

  2. 如果你的系统架构是 64-bit, 可以从 https://rakudo.org/latest/star/win64 下载最新的安装器(.msi 后缀的文件); 如果是 32-bit, 则从 https://rakudo.org/latest/star/win32 下载最新的安装器(.msi 后缀的文件)。

  3. 安装完成后,确保 C:\rakudo\bin 在 PATH 中。

Docker
  1. 获取官方的 Docker 镜像 docker pull rakudo-star

  2. 然后运行一个带有该镜像的容器 docker run -it rakudo-star

1.4. 运行 Raku 代码

运行 Raku 代码可以通过使用 REPL(Read-Eval-Print 循环)来实现。 实现方法是,打开命令行终端,输入 perl6 后回车。这会导致命令提示符 > 的出现。接着, 输入一行代码后回车。 REPL 将会打印出该行的返回值。然后你可以输入另外的行, 或输入 exit 并回车以离开 REPL。

或者,你可以在文件中编写你的代码,保存后再运行。建议 Raku 脚本文件的扩展名设定为 .raku。 运行该文件的代码时只需将 perl6 filename.raku 输入到命令行终端后回车即可。 但不像 REPL, 这不会自动打印出每一行的结果:这里要求文件中的代码必须包含 say 那样的语句以打印输出。

REPL 大多用于测试特殊代码片段,通常只有有一行。对于超过一行的程序,建议先把它们保存到文件中而后再运行。

单行代码也通过在命令行中键入 perl6 -e 'your code here' 并回车来以非交互的方式来测试。

Rakudo Star 搭载了一个行编辑器来帮你最大程度地利用 REPL。

如果你安装了普通的 Rakudo 而不是 Rakudo Star,那么你的行编辑功能可能没有开启(利用上下方向键查询历史;左右方向键以编辑输入;TAB 键以完成当前行输入)功能。 此时可以考虑运行以下命令来设置好上述功能:

  • zef install Linenoise 能在 Windows, Linux 和 OSX 上工作

  • zef install Readline 如果你使用 Linux 并且偏好 Readline

1.5. 编辑器

因为大多数情况下我们会在文件中编写并存储 Raku 程序,因此我们需要一个优雅的而且能识别 Raku 语法的文本编辑器。

从个人角度来说,我推荐正在使用的 Atom。 这是一个时尚的文本编辑器,带有开箱即用的 Raku 语法高亮功能。 Raku FE 是相对于 Atom 默认 Raku 语法高亮插件的另一种选择,也可以高亮 Raku 的语法,该软件来源于原始的包, 但存在很多 bug 需要修补和编辑。

社区里的其他同伴也有使用 VimEmacsPadre 的。

最新版本的 Vim 自带 Raku 语法高亮的功能,Emacs 和 Padre 则需要另行安装额外的包。

1.6. Hello World!

我们会从 hello world 惯例程序开始。

say 'hello world';

它也可以被写为:

'hello world'.say;

1.7. 语法概要

Raku 是 形式自由的, (大多数时候)你可以使用任意数量的空格。

语句 通常是一个逻辑代码行, 它们需要以一个分号结尾: say "Hello" if True;

表达式 是一种能够返回值的特殊类型的语句: 1+2 会返回 3

表达式由 运算符 组成。

是:

  • 变量: 一个可以被操作和改变的值。

  • 字面值: 一个像数字或字符串那样的常量值。

运算符 的分类:

类型

解释

示例

前缀

用在项前

++1

中缀

用在项与项之间

1+2

后缀

用在项后

1++

环缀

包围项

(1)

后环缀

在一个项之后, 包围另一个项

Array[1]

1.7.1. 标识符

标识符是你定义项时给它们起的名字。

命名规则:
  • 它们必须以字母字符或下划线开头。

  • 它们可以包含数字 (除了第一个字符)。

  • 它们可以包含破折号或撇号(除了第一个和最后一个字符), 前提是每个破折号或撇号的右侧有一个字母字符。

有效标识符

无效标识符

var1

1var

var-one

var-1

var’one

var'1

var1_

var1'

_var

-var

命名约定:
  • 驼峰式: variableNo1

  • 串联式: variable-no1

  • 蛇行式: variable_no1

你可以随意命名标识符,但是最好采用一个命名约定。

使用有意义的名称将减轻你(和其他人)的编程工作负担。

  • var1 = var2 * var3 在语法上是正确的,但它的意图不明显。

  • monthly-salary = daily-rate * working-days 是更好的命名变量的方法。

1.7.2. 注释

注释是用于注解的文本片段, 编译器会忽略注释。

注释被分为 3 种类型:

  • 单行注释:

    # This is a single line comment
  • 嵌套的注释:

    say #`(This is an embedded comment) "Hello World."
  • 多行注释:

    =begin comment
    This is a multi line comment.
    Comment 1
    Comment 2
    =end comment

1.7.3. 引号

字符串被双引号或单引号包围。

必须使用双引号, 如果:

  • 你的字符串包含单引号。

  • 你的字符串包含需要被替换的变量。

say 'Hello World';   # Hello World
say "Hello World";   # Hello World
say "Don't";         # Don't
my $name = 'John Doe';
say 'Hello $name';   # Hello $name
say "Hello $name";   # Hello John Doe

2. 运算符

2.1. 一般运算符

下表列出了最常用的运算符。

运算符 类型 描述 例子 结果

+

中缀

1 + 2

3

-

中缀

3 - 1

2

*

中缀

3 * 2

6

**

中缀

指数

3 ** 2

9

/

中缀

3 / 2

1.5

div

中缀

整除 (向下取整)

3 div 2

1

(-3) div 2

-2

%

中缀

取模

7 % 4

3

%%

中缀

是否能整除

6 %% 4

False

6 %% 3

True

gcd

中缀

最大公约数

6 gcd 9

3

lcm

中缀

最小公倍数

6 lcm 9

18

==

中缀

数值相等

9 == 7

False

!=

中缀

数值不等

9 != 7

True

<

中缀

数值小于

9 < 7

False

>

中缀

数值大于

9 > 7

True

<=

中缀

数值小于等于

7 <= 7

True

>=

中缀

数值大于等于

9 >= 7

True

<=>

中缀

数值比较

1 <=> 1.0

Same

1 <=> 2

Less

3 <=> 2

More

eq

中缀

字符串相等

"John" eq "John"

True

ne

中缀

字符串不等

"John" ne "Jane"

True

lt

中缀

字符串小于

"a" lt "b"

True

gt

中缀

字符串大于

"a" gt "b"

False

le

中缀

字符串小于等于

"a" le "a"

True

ge

中缀

字符串大于等于

"a" ge "b"

False

leg

中缀

字符串比较

"a" leg "a"

Same

"a" leg "b"

Less

"c" leg "b"

More

cmp

中缀

智能比较

"a" cmp "b"

Less

3.5 cmp 2.6

More

=

中缀

赋值

my $var = 7

将字面值 7 赋值给变量 $var

~

中缀

字符串连接

9 ~ 7

97

"Hi " ~ "there"

Hi there

x

中缀

字符串重复

13 x 3

131313

"Hello " x 3

Hello Hello Hello

~~

中缀

智能匹配

2 ~~ 2

True

2 ~~ Int

True

"Raku" ~~ "Raku"

True

"Raku" ~~ Str

True

"enlightenment" ~~ /light/

「light」

++

前缀

递增

my $var = 2; ++$var;

递增变量然后返回变量值 3

后缀

递增

my $var = 2; $var++;

返回变量值 2 然后递增变量

--

前缀

递减

my $var = 2; --$var;

递减变量然后返回变量值 1

后缀

递减

my $var = 2; $var--;

返回变量值 2 然后递减变量

+

前缀

强制转变为数值

+"3"

3

+True

1

+False

0

-

前缀

强制转换为数值并返回相反数

-"3"

-3

-True

-1

-False

0

?

前缀

强制转换为布尔值

?0

False

?9.8

True

?"Hello"

True

?""

False

my $var; ?$var;

False

my $var = 7; ?$var;

True

!

前缀

强制转换为布尔值并返回相反数

!4

False

..

中缀

Range 构造函数

0..5

创建区间 [0,5] [1]

..^

中缀

Range 构造函数

0..^5

创建区间 [0,5) [1]

^..

中缀

Range 构造函数

0^..5

创建区间 (0,5] [1]

^..^

中缀

Range 构造函数

0^..^5

创建区间 (0,5) [1]

^

前缀

Range 构造函数

^5

等价于0..^5,创建区间 [0,5) [1]

...

中缀

惰性列表构造函数

0...9999

只有接到请求时才返回元素

|

前缀

扁平化

|(0..5)

(0 1 2 3 4 5)

|(0^..^5)

(1 2 3 4)

2.2. 反转运算符

在任何运算符前加 R 就可以得到调换运算符两边的参数后运算的结果。

正常运算符 结果 反转运算符 结果

2 / 3

0.666667

2 R/ 3

1.5

2 - 1

1

2 R- 1

-1

2.3. 归约运算符

归约运算符应用于一个值的列表。 归约运算符由被方括号 [] 包围的运算符来表示。

正常运算符 结果 归约运算符 结果

1 + 2 + 3 + 4 + 5

15

[+] 1,2,3,4,5

15

1 * 2 * 3 * 4 * 5

120

[*] 1,2,3,4,5

120

可以到 https://docs.raku.org/language/operators 查看完整的运算符列表和它们的优先级。

3. 变量

Raku 中的变量分为 3 类:标量、数组和哈希。

变量名以 魔符 (sigil) 开头以区分不同类型。

  • $ 用于标量 (scalar)

  • @ 用于数组 (array)

  • % 用于哈希 (hash)

3.1. 标量

标量用于存储一项数据或引用。

#字符串
my $name = 'John Doe';
say $name;

#整数
my $age = 99;
say $age;

特定数据类型的标量对应一组作用于它的操作。

字符串
my $name = 'John Doe'; # 对于字符串类型
say $name.uc;          # .uc 将字母全部大写
say $name.chars;       # .chars 获得字符串的字符数, 即字符串长度
say $name.flip;        # .flip 返回翻转字符串的结果
JOHN DOE
8
eoD nhoJ
所有可应用于字符串的方法列表,请参照 https://docs.raku.org/type/Str
整数
my $age = 17;
say $age.is-prime; # .is-prime 判断整数值是否为质数
True
所有可应用于整数的方法列表,请参照 https://docs.raku.org/type/Int
有理数
my $age = 2.3;
say $age.numerator;   # .numerator 获得有理数值对应最简分数的分子
say $age.denominator; # .denominator 获得有理数值对应最简分数的分母
say $age.nude;        # .nude 同时获得分子分母
23
10
(23 10)
所有可应用于有理数的方法列表,请参照 https://docs.raku.org/type/Rat

3.2. 数组

数组是储存有多个值的列表。

my @animals = 'camel','llama','owl';
say @animals;

下面例子中的操作可以应用于列表:

波浪符 ~ 可用于列表中字符串的连接。
脚本
my @animals = 'camel','vicuña','llama';
say "动物园容纳了 " ~ @animals.elems ~ " 只动物";
say "这些动物是: " ~ @animals;
say "动物园收养了一只 owl";
@animals.push("owl");
say "现在动物园有: " ~ @animals;
say "动物园收养的第一只动物是 " ~ @animals[0];
@animals.pop;
say "不幸的是, owl 走了,还剩下: " ~ @animals;
say "我们要关闭动物园,只留下一只动物";
say "我们要放走: " ~ @animals.splice(1,2) ~ " ,只剩下 " ~ @animals;
输出
动物园容纳了 3 只动物
这些动物是: camel vicuña llama
动物园收养了一只 owl
现在动物园有: camel vicuña llama owl
动物园收养的第一只动物是 camel
不幸的是, owl 走了,还剩下: camel vicuña llama
我们要关闭动物园,只留下一只动物
我们要放走: vicuña llama ,只剩下 camel
说明

.elems 返回列表中的元素个数。
.push() 向数组追加一个或多个元素。
我们可以通过提供元素在数组中的位置来访问数组中特定元素 @animals[0]
.pop 删除并返回数组中最后一个元素。
.splice(a,b) 会删除从位置 a 开始的 b 个元素。

3.2.1. 定长数组

一般数组通过以下方式声明:

my @array;

一般的数组的长度可以无限,因此被叫做自动扩展。
它可以接受任意个数的元素而不受限制。

对应地,我们可以创建一个定长数组。
在这种数组中不能访问下标超过定义大小的元素。

可以通过在变量名后的方括号中指定最大元素个数来定义定长数组的长度:

my @array[3];

这个数组最多可以储存 3 个变量,索引从 0 开始, 到 2 结束。

my @array[3];
@array[0] = "first value";
@array[1] = "second value";
@array[2] = "third value";

我们不能在这个数组中添加第四个元素:

my @array[3];
@array[0] = "first value";
@array[1] = "second value";
@array[2] = "third value";
@array[3] = "fourth value";
Index 3 for dimension 1 out of range (must be 0..2) 第一维的索引值 3 超出了范围(必须是0..2)

3.2.2. 多维数组

到现在为止我们看到的数组还都是一维的。
幸运的是,我们在 Raku 中能定义多维数组。

my @tbl[3;2];

这是一个二维数组。 它的第一个维度最多可以有 3 个水平,第二个维度最多可以有 2 个水平。

可以把它看成 3x2 的矩阵。

my @tbl[3;2];
@tbl[0;0] = 1;
@tbl[0;1] = "x";
@tbl[1;0] = 2;
@tbl[1;1] = "y";
@tbl[2;0] = 3;
@tbl[2;1] = "z";
say @tbl
[[1 x] [2 y] [3 z]]
数组的可视化表示:
[1 x]
[2 y]
[3 z]
对于完整的数组参考资料,请参照 https://docs.raku.org/type/Array

3.3. Hashes 哈希

哈希是一组键值对。
my %capitals = ('UK','London','Germany','Berlin');
say %capitals;
另一种填充哈希的方法:
my %capitals = (UK => 'London', Germany => 'Berlin');
say %capitals;

一些能应用于哈希的方法:

脚本
my %capitals = (UK => 'London', Germany => 'Berlin');
%capitals.push: (France => 'Paris');
say %capitals.kv;
say %capitals.keys;
say %capitals.values;
say "The capital of France is: " ~ %capitals<France>;
输出
(France Paris Germany Berlin UK London)
(France Germany UK)
(Paris Berlin London)
The capital of France is: Paris
说明

.push:(key => "value") 添加一个新键值对。
.kv 返回一个包含所有键值对的列表。
.keys 返回一个包含所有键的列表。
.values 返回一个包含所有值的列表。
我们可以通过哈希中特定值所对应的键来访问这个值。 %hash<key>

完整的哈希参考资料,请参照 https://docs.raku.org/type/Hash

3.4. 类型

前面的例子中,我们并没有指定变量中值的类型。

.WHAT 会返回变量中值的类型。
my $var = 'Text';
say $var;
say $var.WHAT;

$var = 123;
say $var;
say $var.WHAT;

在上面的例子中,你能看到变量 $var 中的值先是(Str)后是(Int)。

这种编程风格被称作动态类型。动态在这里是指变量可以储存任何类型的值。

接下来试着运行下面的例子:
注意在变量名前的 Int

my Int $var = 'Text';
say $var;
say $var.WHAT;

运行会失败并返回报错信息: Type check failed in assignment to $var; expected Int but got Str 赋值给$var时类型检查失败,预期Int但是得到Str

这是因为我们预先指定变量类型为(Int),当将(Str)赋值给它的时候就导致了运行失败。

这种编程风格被称为静态类型编程。静态在这里是指变量类型在赋值前定义并且不能在更改。

Raku 属于 渐进类型 ;它同时支持 静态动态 类型。

数组和哈希同样可以为静态类型:
my Int @array = 1,2,3;
say @array;
say @array.WHAT;

my Str @multilingual = "Hello","Salut","Hallo","你好","안녕하세요","こんにちは";
say @multilingual;
say @multilingual.WHAT;

my Str %capitals = (UK => 'London', Germany => 'Berlin');
say %capitals;
say %capitals.WHAT;

my Int %country-codes = (UK => 44, Germany => 49);
say %country-codes;
say %country-codes.WHAT;

my Str %student-name{Int} = (1 => 'Alex', 2 => 'Mary');
say %student-name;
say %student-name.WHAT;
下面是最常用的类型:

你可能永远不会用到前两种类型,他们被列出来只是为了让你知道。

类型

描述

例子

结果

Mu

Raku 的基础类型

Any

新类和大部分内部类的默认基类

Cool

既可以被当做数值也可以作为字符串

my Cool $var = 31; say $var.flip; say $var * 2;

13 62

Str

字符串

my Str $var = "NEON"; say $var.flip;

NOEN

Int

整数(任意精度)

7 + 7

14

Rat

有理数(有限精度)

0.1 + 0.2

0.3

Bool

布尔类型

!True

False

3.5. 自省

自省是获取对象属性信息的过程,比如获取对象的类型。
在前面的一个例子中我们使用 .WHAT 来获取变量的类型。

my Int $var;
say $var.WHAT;    # (Int)
my $var2;
say $var2.WHAT;   # (Any)
$var2 = 1;
say $var2.WHAT;   # (Int)
$var2 = "Hello";
say $var2.WHAT;   # (Str)
$var2 = True;
say $var2.WHAT;   # (Bool)
$var2 = Nil;
say $var2.WHAT;   # (Any)

变量的类型和它储存的值有关。
使用强声明定义的空变量,它的类型就是声明的类型
一个不是强声明定义的空变量,它的类型是 (Any)
可以通过赋 Nil 给变量,来清除变量的值。

3.6. 作用域

在第一次使用变量之前,变量需要被声明。

在 Raku 中可以使用使用多种声明方式。其中 my 已经在上述例子中使用到。

my $var=1;

my 声明指定了变量上下文作用域。 换句话说,这个变量只能在它被定义的块中被访问。

Raku 中块有 {} 界定。 如果没有块存在,那么变量就在整个 Raku 脚本中可用。

{
  my Str $var = 'Text';
  say $var; # 可以使用
}
say $var;   # 不能使用, 会报错

因为一个变量只有在定义它的块中有效,所以同样的变量名可以在另一个块中使用。

{
  my Str $var = 'Text';
  say $var;
}
my Int $var = 123;
say $var;

3.7. 赋值 vs. 绑定

我们已经在前面的例子中看到如何将值 给变量。
赋值 通过 = 操作符完成。

my Int $var = 123;
say $var;

我们可以改变赋给变量的值:

赋值
my Int $var = 123;
say $var;
$var = 999;
say $var;
输出
123
999

我们不能改变 绑定 到变量上的值
绑定 通过 := 操作符实现。

绑定
my Int $var := 123;
say $var;
$var = 999;
say $var;
输出
123
Cannot assign to an immutable value 不允许赋值到一个不可变值
变量也可以绑定到其他变量:
my $a;
my $b;
$b := $a;
$a = 7;
say $b;
$b = 8;
say $a;
输出
7
8

就像你已经注意到的那样,绑定变量是双向的。
$a := $b$b := $a 拥有同样的效果.

关于变量的更多信息,请参见 https://docs.raku.org/language/variables

4. 函数和修改器

把函数和修改器区分开来很重要。
函数在调用的时候不改变对象的初始状态。
修改器改变对象的状态。

脚本
my @numbers = [7,2,4,9,11,3];

@numbers.push(99);
say @numbers;      #1

say @numbers.sort; #2
say @numbers;      #3

@numbers.=sort;
say @numbers;      #4
输出
[7 2 4 9 11 3 99] #1
(2 3 4 7 9 11 99) #2
[7 2 4 9 11 3 99] #3
[2 3 4 7 9 11 99] #4
说明

.push 是一个修改器,它会改变数组的状态。(#1)

.sort 是一个函数,它返回排序后的数组,但是不改变原始数组。

  • (#2) 展示了它返回排序后的数组。

  • (#3) 展示了原始数组没有被修改。

为了强制一个函数作为修改器那样对原始数据进行修改,而我们可以使用 .= 来代替 . (#4) (脚本的第9行)

5. 循环和条件语句

Raku 有多种条件和循环结构。

5.1. if

仅在条件满足(条件表达式结果为 True)时运行代码。

my $age = 19;

if $age > 18 {
  say 'Welcome'
}

在 Raku 中我们能倒装条件语句和待运行的代码。
将待运行的代码和条件语句倒装后,条件语句总是优先被执行。

my $age = 19;

say 'Welcome' if $age > 18;

如果条件不满足,我们可以指定执行另一个代码块:

  • else

  • elsif

# 为变量不同的值运行相同的代码
my $座位数 = 9;

if $座位数 <= 5 {
  say '我是小轿车'
} elsif $座位数 <= 7 {
  say '我是七座车'
} else {
  say '我是面包车'
}

5.2. unless

unless 是否定形式的 if。

下面这些代码:

my $clean-shoes = False;

if not $clean-shoes {
  say 'Clean your shoes'
}

可以改写成:

my $clean-shoes = False;

unless $clean-shoes {
  say 'Clean your shoes'
}

运算在 Raku 中用 not 来实现。

unless (条件) 可以用来代替 if not (条件)

需要注意 unless 不能配合使用 else

5.3. with

with 的用法和 if 相同, 不过 with 会检查变量是否已经定义。
如果变量没有定义,那么就不会执行块中的代码。

my Int $var=1;

with $var {
  say 'Hello'
}

以下代码中,如果你没有给 $var 赋值,那么代码块就不会被执行。

my Int $var;

with $var {
  say 'Hello'
}

without 是否定版的 with。你可以拿 ifunless 的关系做类比。

如果第一个 with 条件不能满足,替代执行的代码块可以使用 orwith 来指定。
withorwith 的关系可以同 ifelsif 的关系相比较。

5.4. for

for 循环可以迭代多个值。

my @array = [1,2,3];

for @array -> $array-item {
  say $array-item * 100
}

需要注意到我们创建了一个循环变量 $array-item 用来对每个数组元素进行 *100 的操作。

5.5. given

在 Raku 中的 given 类似其他语言中的 switch,但是要更加地强大。

my $var = 42;

given $var {
    when 0..50 { say 'Less than or equal to 50'}
    when Int { say "is an Int" }
    when 42  { say 42 }
    default  { say "huh?" }
}

在成功匹配后,匹配就会停止。

如果在匹配成功后运行的代码块中加了 proceed,那么就要在下一次成功匹配后才停止匹配。

my $var = 42;

given $var {
    when 0..50 { say 'Less than or equal to 50';proceed}
    when Int { say "is an Int";proceed}
    when 42  { say 42 }
    default  { say "huh?" }
}

5.6. loop

loop 是进行 for 循环的另一种方式。
Raku 中 C 语言家族的 for 循环使用 loop 来表示。
Raku 是 C 语言家族中的一员。

loop (my $i = 0; $i < 5; $i++) {
  say "The current number is $i"
}
更多关于循环和条件语句的信息,参见 https://docs.raku.org/language/control

6. I/O

在 Raku 中,两个最常用的输入/输出的端口是终端和文件。

6.1. 在终端中基本的 I/O 操作

6.1.1. say

say 能输出到标准输出,而且它还会在输出的最后添加一个换行符,请见下面的示例代码:

say 'Hello Mam.';
say 'Hello Sir.';

会在两行中分别打印出来。

6.1.2. print

print 的功能和 say 相似,但是它不会在输出内容后添加换行符。

试试将 say 换成 print 比较一下它们两者的特点。

6.1.3. get

get 用来获取终端中的输入。

my $name;

say "Hi, what's your name?";
$name = get;

say "Dear $name welcome to Raku";

当上面的代码运行时,终端会等你输入名字和回车键。 然后就会向你打招呼。

6.1.4. prompt

prompt 整合了 printget 的功能。

以上的例子可以改写成以下方式:

my $name = prompt "Hi, what's your name? ";

say "Dear $name welcome to Raku";

6.2. 运行 Shell 命令

有两种子例程可以用来运行 shell 命令:

  • run 不通过 shell 运行外部命令。

  • shell 通过系统 shell 来运行命令。它依赖系统平台和它的 shell。 所有的保留字(meta chracters) 会被 shell 解释,包括管道(pipes)、重定向、环境变量替换等等。

如果你用的是 Linux 或 OSX 请运行以下代码:
my $name = 'Neo';
run 'echo', "hello $name";
shell "ls";
如果你用的是 Windows 请运行下面这段代码:
shell "dir";

echols 是常用的 Linux shell 命令
echo 打印文本到终端(和 Raku 中的 print 相同
ls 列出当前目录下所有的文件和文件夹。

dir 在 Windows 中和 ls 的功能相同。

6.3. File I/O

6.3.1. slurp

slurp 用来从文件中读入数据。

建立一个文本文件包含以下内容:

datafile.txt
John 9
Johnnie 7
Jane 8
Joanna 7
my $data = slurp "datafile.txt";
say $data;

6.3.2. spurt

spurt 用来向文件写入数据。

my $newdata = "New scores:
Paul 10
Paulie 9
Paulo 11";

spurt "newdatafile.txt", $newdata;

在运行上述代码后,一个存储有 New score 的文件 newdatafile.txt 会被创建。

6.4. 文件和文件目录操作

在先前的例子中,Raku 能不使用 shell 命令的情况下列出一个目录下所有的内容。

say dir;              # 列出当前目录下的文件和文件夹
say dir "/Documents"; # 列出特定目录下的文件和文件夹

另外你还能创建和删除目录。

mkdir "newfolder";
rmdir "newfolder";

mkdir 创建一个新的目录。
rmdir 删除一个空目录,如果目录不为空则返回 error。

你还可以检查特定的路径是否存在:

在你要运行以下脚本的目录中,建立一个叫 folder123 的空文件夹和一个文件 script123.raku

say "script123.raku".IO.e;
say "folder123".IO.e;

say "script123.raku".IO.d;
say "folder123".IO.d;

say "script123.raku".IO.f;
say "folder123".IO.f;

IO.e 检查文件夹或文件是否存在。
IO.f 检查路径所指的是否为文件。
IO.d 检查路径所指的是否为文件夹。

Windows 下使用 /\\ 来定义目录
C:\\rakudo\\bin
C:/rakudo/bin
更多关于 I/O,详见 https://docs.raku.org/type/IO

7. 子例程

7.1. 定义

子例程 (也叫 subsfunctions ) 是一种功能集的打包。

定义子例程时以关键字 sub 起始。在定义之后你能通过子例程名来进行调用。
让我们来看下面的例子:

sub alien-greeting {
  say "Hello earthlings";
}

alien-greeting;

上面的例子给我们展示了一个不需要输入参数的子例程。

7.2. Signature 函数签名

很多子例程需要多个输入才能运行。这些输入由 参数 提供。 一个子例程可以不定义或定义多个 参数。 子例程所定义的参数之个数与类型称为它的 函数签名

下面的子例程接收一个字符串参数。

sub say-hello (Str $name) {
    say "Hello " ~ $name ~ "!!!!"
}
say-hello "Paul";
say-hello "Paula";

7.3. 多重分派

我们可以定义多个有相同命名但有不同函数签名的的子例程。 当这样的子例程被调用的时候,运行环境会根据提供的参数之数量和类型判断那个版本的同名子例程来运行。 这种子例程和普通的子例程的定义方法是一样的,不过我们需要使用 multi 来代替原先的 sub

multi greet($name) {
    say "Good morning $name";
}
multi greet($name, $title) {
    say "Good morning $title $name";
}

greet "Johnnie";
greet "Laura","Mrs.";

7.4. 默认参数和可选参数

如果一个子例程被定义需要一个参数,但是我们调用它的时候没有提供所需的参数,那么这个子例程就不能运行。

不过 Raku 提供了:

  • 可选参数

  • 默认参数

可选参数在定义的时候需要在参数名后加 ?

sub say-hello($name?) {
  with $name { say "Hello " ~ $name }
  else { say "Hello Human" }
}
say-hello;
say-hello("Laura");

如果使用者没有提供参数,那么它就默认使用预先给定的值。
上面是通过在子例程中给参数赋值的方式实现的。

sub say-hello($name="Matt") {
  say "Hello " ~ $name;
}
say-hello;
say-hello("Laura");

7.5. 返回值

我们现在为止看到的子例程都在终端中打印一些文字。

一些时候我们需要让子例程 返回 值让我们能在我们的程序中能重复使用。

在一般的情景下,子例程代码的最后一行被默认为返回值。

隐式返回
sub squared ($x) {
  $x ** 2;
}
say "7 squared is equal to " ~ squared(7);

一旦我们的代码变多,清楚地指明我们需要返回的变量是很有用的。 可以使用 return 关键字来指明返回变量。

显式返回
sub squared ($x) {
  return $x ** 2;
}
say "7 squared is equal to " ~ squared(7);

7.5.1. 限制返回值

在之前的例子中,我们对将子例程的参数限定为特定类型。 我们可以同样地限定返回值的类型。

为了将返回值限定为特定类型,我们可以使用 returns 特征 (trait) 或在函数签名中使用箭标。

使用 rerurns 特征
sub squared ($x) returns Int {
  return $x ** 2;
}
say "1.2 squared is equal to " ~ squared(1.2);
使用箭头
sub squared ($x --> Int) {
  return $x ** 2;
}
say "1.2 squared is equal to " ~ squared(1.2);

如果子例程不能提供符合类型要求的返回值,程序就会报错。

Type check failed for return value; expected Int but got Rat (1.44) 返回值类型检查失败,预期Int但是得到Rat(1.44)

类型限制不仅可以控制返回值的类型,还可以控制返回值的定义状态。

之前的例子中,我们指定了返回值必须为 Int
还可以进一步严格指定返回值 Int 是已定义(defined)或未定义(undefined)的:
--> Int:D--> Int:U

使用类型限制是一个好的习惯。
以下是将前面的例子修改后的结果,其中使用 :D 强制返回已定义的 Int

sub squared ($x --> Int:D) {
  return $x ** 2;
}
say "1.2 squared is equal to " ~ squared(1.2);
关于更多子例程和函数的资料,详见 https://docs.raku.org/language/functions

8. 函数式编程

在本章中,我们将看看一些有利于函数式编程的功能。

8.1. 函数是一等公民

函数/子例程是一等公民:

  • 它们能作为参数传递

  • 它们能从另外一个函数中返回

  • 它们能被赋值给变量

map 函数是用来说明这个概念的极好例子。
map高阶函数, 它接收另外一个函数作为参数。

脚本
my @array = <1 2 3 4 5>;
sub squared($x) {
    $x ** 2
}
say map(&squared, @array);
输出
(1 4 9 16 25)
解释

我们定义了一个叫做 squared 的子例程, 它接收一个数字并返回该数字的二次幂。
下一步, 我们使用 map 这个高阶函数并传递给它两个参数, 一个子例程和一个数组。
结果是所有数组元素的平方组成的列表。

注意当传递子例程作为参数时, 我们需要在子例程的名字前添加一个 & 符号。

8.2. 匿名函数

匿名函数 也叫做 拉姆达(lambda)。
匿名函数没有绑定到标识符(匿名函数没有名字)。

让我们使用匿名函数重写 map 那个例子。

my @array = <1 2 3 4 5>;
say map(-> $x {$x ** 2}, @array);

注意我们没有声明子例程并把它作为参数传递给 map, 而是在里面直接定义了匿名函数。
匿名函数 -> $x {$x ** 2} 没有句柄并且不能被调用。

按照 Raku 的说法我们把这个标记叫做 pointy block

pointy block 也能用于把函数赋值给变量:
my $squared = -> $x {
    $x ** 2
}
say $squared(9);

8.3. 链式调用

在 Raku中, 方法可以链接起来, 你不再需要把一个方法的结果作为参数传递给另外一个方法了。

我们假设你有一个数组。你被要求返回该数组的唯一值, 并且按从大到小的顺序排序。

下面是没有使用链式调用的代码:

my @array       = <7 8 9 0 1 2 4 3 5 6 7 8 9 >;
my @final-array = reverse(sort(unique(@array)));
say @final-array;

首先我们在 @array 上调用 unique 函数, 然后我们把它的结果作为参数传递给 sort 函数, 再然后我们把结果传递给 reverse 函数。

和上面的例子相比, Raku 允许链式方法。
上面的例子可以像下面这样写, 利用 方法链 的优点:

my @array       = <7 8 9 0 1 2 4 3 5 6 7 8 9 >;
my @final-array = @array.unique.sort.reverse;
say @final-array;

你已经看到链式方法看起来有多 清爽 啦。

8.4. Feed 操作符

feed 操作符, 在有些函数式编程语言中也叫 管道, 然而它是链式方法的一个更好的可视化产出。

正向流(Forward Feed)
my @array = <7 8 9 0 1 2 4 3 5 6>;
@array ==> unique()
       ==> sort()
       ==> reverse()
       ==> my @final-array;
say @final-array;
解释
从 `@array` 开始 然后 返回一个唯一元素的列表
                然后 排序它
                然后 反转它
                然后 把结果保存到 @final-array 中

就像你看到的那样, 方法的流向是自上而下的。

反向流(Backward Feed)
my @array = <7 8 9 0 1 2 4 3 5 6>;
my @final-array-v2 <== reverse()
                   <== sort()
                   <== unique()
                   <== @array;
say @final-array-v2;
解释

正向流就像反向流一样, 但是是以反转的顺序写的。

方法的流动方向是自下而上。

8.5. Hyper 操作符

hyper 操作符 >>. 对列表的所有元素调用同一方法,返回其结果的列表。

my @array = <0 1 2 3 4 5 6 7 8 9 10>;
sub is-even($var) { $var %% 2 };

say @array>>.is-prime;
say @array>>.&is-even;

我们能通过使用 hyper 操作符,调用 Raku 中已经定义过的方法。例如 is-prime 告诉我们一个数字是否是质数。

此外我们能定义新的子例程并使用 hyper 操作符调用它们。但是这时我们必须在方法的名字前面加上 & 符号。例如 &is-even

这很实用,因为我们不必写 for 循环就可以迭代每个值。

Raku 会保证结果的顺序与原始值的顺序相同。
但是 不能保证 Raku 会真正地在同一个顺序或在同一个线程中调用该方法。
因此,请注意具有副作用的方法,例如 sayprint

8.6. Junction

junction 是值的逻辑叠加。

在下面的例子中 1|2|3 是一个junction。

my $var = 2;
if $var == 1|2|3 {
    say "The variable is 1 or 2 or 3"
}
junction 的使用常常触发 **自动线程化**; 每个 junction 元素都执行该操作, 并且所有的结果被组合到一个新的 junction 中并返回。

8.7. 惰性列表

惰性列表 是被惰性求值的列表。
惰性求值延迟表达式的计算直到需要时, 并把结果存储到查询表中以避免重复计算。

惰性列表的优点包括:

  • 通过避免不必要的计算带来的性能提升

  • 构建潜在的无限数据结构的能力

  • 定义控制流的能力

我们使用中缀操作符 ... 来创建惰性列表。
惰性列表具有 初始元素, 生成器结束点

. 简单惰性列表
----
my  $lazylist = (1 ... 10);
say $lazylist;
----

初始元素为 1 而结束点为 10。因为没有定义生成器所以默认的生成器为后继生成器(+1)。
换句话说, 这个惰性列表可能返回(如果需要的话)下面的元素 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)。

. 无穷惰性列表
----
my  $lazylist = (1 ... Inf);
say $lazylist;
----

该列表可能返回(如果需要的话) 1 到无穷大之间的任何整数, 换句话说, 可以返回任何整数。

使用推断生成器创建惰性列表
my  $lazylist = (0,2 ... 10);
say $lazylist;

初始的元素是 0 和 2 而结束点是 10。虽然没有定义生成器, 但是使用了初始元素, Raku 会把生成器推断为 (+2)。

这个惰性列表可能返回(如果需要的话)下面的元素 (0, 2, 4, 6, 8, 10)。

使用定义的生成器创建惰性列表
my  $lazylist = (0, { $_ + 3 } ... 12);
say $lazylist;

在这个例子中, 我们在闭合 { } 中显式地定义了一个生成器。

这个惰性列表可能返回(如果需要的话)下面的元素 (0, 3, 6, 9, 12)。

当使用显式的生成器时, 结束点必须是生成器能返回的一个值。

如果在上面的例子中我们使用的结束点是 10 而非 12, 那么生成器就不会停止。生成器会 跳过 那个结束点。

你可以使用 0 ...^ * > 10 代替 0 ... 10
你可以把它读作: 从 0 直到第一个大于 10 的值(不包含该值)。

这不会使生成器停止
my  $lazylist = (0, { $_ + 3 } ... 10);
say $lazylist;
这会使生成器停止
my  $lazylist = (0, { $_ + 3 } ...^ * > 10);
say $lazylist;

8.8. 闭包

在 Raku 中所有的代码对象都是闭包, 这意味着它们能从外部作用域(outer scope)引用词法变量(lexical variables)。

sub generate-greeting {
    my $name = "John Doe";
    sub greeting {
      say "Good Morning $name";
    };
    return &greeting;
}
my $generated = generate-greeting;
$generated();

如果你运行上面的代码,它将在终端上显示 Good Morning John Doe
虽然结果相当简单,但这个例子有趣的是,greeting 内部子程序在执行之前是从外部子例程中返回的。

$generated 已经变成了 闭包

闭包 是一种特殊类型的对象,它结合了两个东西:

  • 子例程

  • 创建该子例程的环境。

该环境由创建闭包时在作用域内的任何局部变量组成。 在这种情况下,$generated 是一个闭包,它包含在创建闭包时存在的 greeting 子例程和 John Doe 字符串。

让我们来看一个更有趣的例子。

sub greeting-generator($period) {
  return sub ($name) {
    return "Good $period $name"
  }
}
my $morning = greeting-generator("Morning");
my $evening = greeting-generator("Evening");

say $morning("John");
say $evening("Jane");

在这个例子中,我们定义了一个子例程 greeting-generator($period),它接受单个参数 $period 并返回一个新的子例程。新的子例程接受单个参数 $name 并返回构造好的问候语。

基本上,greeting-generator 是一个子例程工厂。在这个例子中,我们使用了 greeting-generator 来创建两个新的子例程,一个说 Good Morning ,一个说 Good Evening

$morning$evening 都是闭包。它们共享相同的子例程主体定义,但存储不同的环境。
$morning 的环境中 $periodMorning。在 $evening 的环境中 $periodEvening

9. 类和对象

在上一章中我们学习了 Raku 中函数式编程的便利性。
在这一章中我们将看看 Raku 中的面向对象编程。

9.1. 介绍

面向对象 编程是当今广泛使用的范式之一。
对象 是一组绑定在一起的变量和子例程。

其中的变量叫做 属性, 而子例程被叫做 方法
属性定义对象的 状态, 而方法定义对象的 行为

是创建 对象 的模板。

为了理解它们之间的关系, 考虑下面的例子:

房间里有 4 个人

对象 ⇒ 4 people

这 4 个人是人类

⇒ Human

它们有不同的名字,年纪,性别和国籍

属性 ⇒ name,age,sex,nationality

面向对象 的说法, 对象是类的 实例

考虑下面的脚本:

class Human {
  has $.name;
  has $.age;
  has $.sex;
  has $.nationality;
}

my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
say $john;

class 关键字用于定义类。
has 关键字用于定义类的属性。
.new 方法被称之为 构造函数。它创建了对象作为类的实例。

在上面的例子中, 新的变量 $john 保存了由 Human.new() 所定义的新 "Human" 实例。
传递给 .new() 方法的参数用于设置底层对象的属性。 类可以使用 my 来声明一个 本地作用域:

my class Human {

}

9.2. 封装

封装是一个面向对象的概念, 它把一组数据和方法捆绑在一块。
对象中的数据(属性)应该是 私有的, 换句话说, 只能从对象内部访问它。
为了从对象外部访问对象的属性, 我们使用叫做 存取器 的方法。

下面两个脚本拥有同样的结果。

直接访问变量:
my  $var = 7;
say $var;
封装:
my $var = 7;
sub sayvar {
    $var;
}
say sayvar;

sayvar 是一个存取器。它让我们通过不直接访问这个变量来访问这个变量。 在 Raku 中使用 twigil 使得封装很便利。
twigil 是次级的 魔符(sigil) ,用于魔符和属性名之间。
有两种 twigil 可用于类中:

  • ! 用于显式地声明属性是私有的

  • . 用于为属性自动生成存取器

默认地, 所有的属性都是私有的, 但是总是用 ! twigil 是一个好习惯。

因此, 我们应该把上面的类重写成下面这样:

class Human {
    has $!name;
    has $!age;
    has $!sex;
    has $!nationality;
}

my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
say $john;

给脚本追加这样的的语句: say $john.age;
它会返回这样的错误: Method 'age' not found for invocant of class 'Human' 在类’Human’中没有找到请求的方法’age'
原因是 $!age 是私有的并且只能用于对象内部。 尝试在对象外部访问它会返回一个错误。

现在用 has $.age 代替 $!age 并看看 say $john.age; 的结果是什么。

9.3. 具名参数 vs. 位置参数

在 Raku 中, 所有的类继承了一个默认的 .new 构造函数。
通过为他提供参数, 它能用于创建对象。
只能提供 具名参数 给默认的构造函数。

如果你考虑上面的例子, 你会看到所有提供给 .new 方法的参数都是按名字定义的:

  • name => 'John'

  • age => 23

假如我不想在每次创建新对象的时候为每个属性提供一个名字呢?
那么我需要创建另外一个接收 位置参数 的构造函数。

class Human {
    has $.name;
    has $.age;
    has $.sex;
    has $.nationality;

    # 重写默认构造函数的新构造函数
    method new ($name, $age, $sex, $nationality) {
        self.bless(:$name, :$age, :$sex, :$nationality);
    }
}

my $john = Human.new('John', 23, 'M', 'American');
say $john;

9.4. 方法

9.4.1. 介绍

方法是对象的 子例程
像子例程一样, 方法是一种打包一组功能的手段, 它们接收 参数, 拥有 签名 并可以被定义为 multi

方法是使用关键字 method 来定义的。
正常情况下, 方法被要求在对象的属性身上执行一些动作。这强制了封装的概念。对象的属性只能在对象里面使用方法来操作。在对象外面, 只能和对象的方法交互, 并且不能访问它的属性。

class Human {
  has $.name;
  has $.age;
  has $.sex;
  has $.nationality;
  has $.eligible;
  method assess-eligibility {
      if self.age < 21 {
          $!eligible = 'No'
      } else {
          $!eligible = 'Yes'
      }
  }
}

my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
$john.assess-eligibility;
say $john.eligible;

一旦方法定义在类中, 它们就能在对象身上使用 点记号 来调用:
object . method 或像上面的例子那样: $john.assess-eligibility

在方法的定义中, 如果我们需要引用对象本身以调用另一个方法, 则使用 self 关键字。

在方法的定义中, 如果我们需要引用属性, 则使用 ! , 即使属性是使用 . 定义的。
理由是 . twigil 做的就是使用 ! 声明一个属性并自动创建存取器。

在上面的例子中, if self.age < 21if $!age < 21 会有同样的效果, 尽管它们从技术上来讲是不同的:

  • self.age 调用了 .age 方法(存取器)
    二选一, 还能写成 $.age

  • $!age 是直接调用那个变量

9.4.2. 私有方法

正常的方法能从类的外面在对象身上调用。

私有方法 是只能从类的内部调用的方法。
一个可能的使用情况是一个方法调用另外一个执行特定动作的方法。 连接外部世界的方法是公共的而被引用的那个方法应该保持私有。我们不想让用户直接调用它, 所以我们把它声明为私有的。

私有方法的声明需要在方法的名字前使用 ! twigil。
私有方法是使用 ! 而非 . 调用的。

method !iamprivate {
    # code goes in here
}

method iampublic {
    self!iamprivate;
    # do additional things
}

9.5. 类属性

类属性 是属于类自身而非类的对象的属性。
它们能在定义期间初始化。
类属性是使用 my 关键字而非 has 关键字声明的。
它们是在类自己身上而非它的对象身上调用的。

class Human {
    has $.name;
    my  $.counter = 0;
    method new($name) {
      Human.counter++;
      self.bless(:$name);
    }
}
my $a = Human.new('a');
my $b = Human.new('b');

say Human.counter;

9.6. 访问类型

到现在为止我们看到的所以例子都使用存取器来从对象属性中获取信息。

假如我们需要修改属性的值呢?

我们需要使用下面的 is rw 关键字把它标记为 read/write

class Human {
    has $.name;
    has $.age is rw;
}
my $john = Human.new(name => 'John', age => 21);
say $john.age;

$john.age = 23;
say $john.age;

默认地, 所有属性都声明为 只读, 但是你可以显式地使用 is readonly 来声明。

9.7. 继承

9.7.1. 介绍

继承 是面向对象编程的另一个概念。

当定义类的时候, 很快我们会意思到很多属性/方法在很多类中是共有的。
我们应该重复代码吗?
不! 我们应该使用 继承

假设我们想定义两个类, 一个类是 Human, 一个类是 Employees。
Human 拥有两个属性: name 和 age。
Employees 拥有 4 个属性: name, age, company 和 salary。

尝试按照下面的方式定义类:

class Human {
    has $.name;
    has $.age;
}

class Employee {
    has $.name;
    has $.age;
    has $.company;
    has $.salary;
}

虽然上面的代码技术上是正确的, 但是概念上差。

更好的写法是下面这样:

class Human {
    has $.name;
    has $.age;
}
class Employee is Human {
    has $.company;
    has $.salary;
}

is 关键字定义了继承。
按面向对象的说法, Employee 是 Human 的 孩子, 而 Human 是 Employee 的 父亲

所有的子类继承了父类的属性和方法, 所以没有必要重新定义它们。

9.7.2. 重写

类从它们的父类中继承所有的属性和方法。
有些情况下, 我们需要让子类中的方法表现得和继承的方法不一样。
为了做到这, 我们在子类中重新定义方法。
这个概念就叫做 重写

在下面的例子中, introduce-yourself 方法被 Employee 类继承。

class Human {
    has $.name;
    has $.age;
    method introduce-yourself {
      say 'Hi 我是人类, 我的名字是 ' ~ self.name;
    }
}

class Employee is Human {
    has $.company;
    has $.salary;
}

my $john = Human.new(name => 'John', age => 23,);
my $jane = Employee.new(name => 'Jane', age => 25, company => 'Acme', salary => 4000);

$john.introduce-yourself;
$jane.introduce-yourself;

重写工作如下:

class Human {
    has $.name;
    has $.age;
    method introduce-yourself {
      say 'Hi 我是人类, 我的名字是 ' ~ self.name;
    }
}

class Employee is Human {
    has $.company;
    has $.salary;
    method introduce-yourself {
      say 'Hi 我是一名员工, 我的名字是 ' ~ self.name ~ ' 我工作在: ' ~ self.company;
    }
}

my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);

$john.introduce-yourself;
$jane.introduce-yourself;

根据对象所属的类, 会调用正确的方法。

9.7.3. Submethod

submethod 是一种子类继承不到的方法。
它们只能从所声明的类中访问。
它们使用 submethod 关键字定义。

9.8. 多重继承

在 Raku 中允许多重继承。一个类可以继承自多个其它的类。

class bar-chart {
  has Int @.bar-values;
  method plot {
    say @.bar-values;
  }
}

class line-chart {
  has Int @.line-values;
  method plot {
    say @.line-values;
  }
}

class combo-chart is bar-chart is line-chart {
}

my $actual-sales   = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);

my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
                                         line-values => [9,8,10,7,6,9]);
say "实际的销售: ";
$actual-sales.plot;
say "预测的销售: ";
$forecast-sales.plot;
say "实际 vs 预测:";
$actual-vs-forecast.plot;
输出
实际的销售:
[10 9 11 8 7 10]
预测的销售:
[9 8 10 7 6 9]
实际 vs 预测:
[10 9 11 8 7 10]
解释

combo-chart 类应该能持有两个序列, 一个是绘制条形图的实际值, 另一个是绘制折线图的预测值。
这就是我们为什么把它定义为 line-chartbar-chart 的孩子的原因。
你应该注意到了, 在 combo-chart 身上调用 plot 方法并没有产生所要求的结果。它只绘制了一个序列。
发生了什么事?

combo-chart 继承自 line-chartbar-chart, 它们都有一个叫做 plot 的方法。当我们在 combo-chart 身上调用那个方法时, Raku 内部会尝试通过调用其所继承的方法之一来解决冲突。

纠正

为了表现得正确, 我们应该在 combo-chart 中重写 plot 方法。

class bar-chart {
  has Int @.bar-values;
  method plot {
    say @.bar-values;
  }
}

class line-chart {
  has Int @.line-values;
  method plot {
    say @.line-values;
  }
}

class combo-chart is bar-chart is line-chart {
  method plot {
    say @.bar-values;
    say @.line-values;
  }
}

my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);

my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
                                         line-values => [9,8,10,7,6,9]);
say "实际的销售: ";
$actual-sales.plot;
say "预测的销售: ";
$forecast-sales.plot;
say "实际 vs 预测:";
$actual-vs-forecast.plot;
输出(译注,截至2019.4.30,这段代码在实现中的输出与预期不一致,参见 https://github.com/hankache/perl6intro/issues/192
实际的销售:
[10 9 11 8 7 10]
预测的销售:
[9 8 10 7 6 9]
实际 vs 预测:
[10 9 11 8 7 10]
[9 8 10 7 6 9]

9.9. Role

role 也是属性和方法的集合,在这个意义上它和类(class)有些相似。 role 使用关键字 role 声明, 而想实现该 role 的类可以使用 does 关键字。

使用 role 重写多重继承的例子
role bar-chart {
  has Int @.bar-values;
  method plot {
    say @.bar-values;
  }
}

role line-chart {
  has Int @.line-values;
  method plot {
    say @.line-values;
  }
}

class combo-chart does bar-chart does line-chart {
  method plot {
    say @.bar-values;
    say @.line-values;
  }
}

my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);

my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
                                         line-values => [9,8,10,7,6,9]);
say "实际的销售: ";
$actual-sales.plot;
say "预测的销售: ";
$forecast-sales.plot;
say "实际 vs 预测:";
$actual-vs-forecast.plot;

运行上面的脚本你会看到结果是一样的。

现在你问问自己, 如果 role 表现得像类的话那么它们的用途是什么呢?

要回答你的问题, 修改第一个用于展示多重继承的脚本, 这个脚本中我们 忘记 重写 plot 方法了。

role bar-chart {
  has Int @.bar-values;
  method plot {
    say @.bar-values;
  }
}

role line-chart {
  has Int @.line-values;
  method plot {
    say @.line-values;
  }
}

class combo-chart does bar-chart does line-chart {
}

my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);

my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
                                         line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
输出
===SORRY!===
Method 'plot' must be resolved by class combo-chart because it exists in multiple roles (line-chart, bar-chart)
类'combo-chart'的方法'plot'必须被决定,因为存在多个role(line-chart, bar-chart)
解释

如果多个 role 被应用到同一个类中, 会出现冲突并抛出一个编译时错误。
这是比多重继承更安全的方法, 其中冲突不被认为是错误并且简单地在运行时解决。

role 会提醒你有冲突。

class Human {
  has Str $.name;
  has Int $.age;
  method introduce-yourself {
    say 'Hi I am a human being, my name is ' ~ self.name;
  }
}

class Employee is Human {
  has Str $.company;
  has Int $.salary;
  method introduce-yourself {
    say 'Hi I am a employee, my name is ' ~ self.name ~ ' and I work at: ' ~ self.company;
  }
}

my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);

say $john.WHAT;
say $jane.WHAT;
say $john.^attributes;
say $jane.^attributes;
say $john.^methods;
say $jane.^methods;
say $jane.^parents;
if $jane ~~ Human {say 'Jane is a Human'};

9.10. 内省

内省 是获取诸如对象的类型、属性或方法等对象属性的信息的过程。

class Human {
  has Str $.name;
  has Int $.age;
  method introduce-yourself {
    say 'Hi i am a human being, my name is ' ~ self.name;
  }
}

class Employee is Human {
  has Str $.company;
  has Int $.salary;
  method introduce-yourself {
    say 'Hi i am a employee, my name is ' ~ self.name ~ ' and I work at: ' ~ self.company;
  }
}

my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);

say $john.WHAT;
say $jane.WHAT;
say $john.^attributes;
say $jane.^attributes;
say $john.^methods;
say $jane.^methods;
say $jane.^parents;
if $jane ~~ Human {say 'Jane is a Human'};

内省使用了:

  • .WHAT 返回已经创建的对象所属的类。

  • .^attributes 返回一个包含该对象所有属性的列表。

  • .^mtethods 返回能在该对象身上调用的所有方法。

  • .^parents 返回该对象所属类的所有父类。

  • ~~ 叫做智能匹配操作符。 如果对象是从它所进行比较的类或任何它继承的类创建的, 则计算为 True。

有关 Raku 中面向对象编程的更多信息,请参阅:

10. 异常处理

10.1. 捕获异常

异常 是当某些东西出错时发生在运行时的特殊行为。
我们说异常被 抛出

考虑下面这个运行正确的脚本:

my Str $name;
$name = "Joanna";
say "Hello " ~ $name;
say "How are you doing today?"
输出
Hello Joanna
How are you doing today?

现在考虑这个抛出异常的脚本:

my Str $name;
$name = 123;
say "Hello " ~ $name;
say "今天过得怎么样?"
输出
Type check failed in assignment to $name; expected Str but got Int 赋值给$name时类型检查失败,预期Str但是得到Int
   in block <unit> at exceptions.raku:2

你应该看到当错误出现时(在这个例子中把数组赋值给字符串变量)程序会停止并且其它行的代码不会被执行, 即使它们是正确的。

异常处理 是捕获已经抛出的异常的过程以使脚本能继续工作。

my Str $name;
try {
  $name = 123;
  say "Hello " ~ $name;
  CATCH {
    default {
      say "请再说一次你的名字,我们在记录中找不到它。";
    }
  }
}
say "今天过得怎么样?";
输出
请再说一次你的名字,我们在记录中找不到它。
今天过得怎么样?

异常处理是使用 try-catch 块完成的。

try {
  # 代码在这里运行
  # 如果有东西出错, 脚本会进入到下面的 CATCH 块中
  # 如果什么错误也没有, 那么 CATCH 块会被忽略
  CATCH {
    default {
      # 只有抛出异常时, 这儿的代码才会被求值
    }
  }
}

CATCH 块能像定义 given 块那样定义。 这意味着我们能捕获并处理各种不同类型的异常。

try {
  # 代码在这里运行
  # 如果有东西出错, 脚本会进入到下面的 CATCH 块中
  # 如果什么错误也没有, 那么 CATCH 块会被忽略
  CATCH {
    when X::AdHoc { # 当异常 X::AdHoc 被抛出时要执行的操作 }
    when X::IO { # 当异常 X::IO 被抛出时要执行的操作}
    when X::OS { # 当异常 X::OS 被抛出时要执行的操作 }
    default { # 当异常类型不属于上述任何一种时要执行的操作}
  }
}

10.2. 抛出异常

Perl 6 也允许你显式地抛出异常。
有两种类型的异常可以抛出:

  • 特设异常(ad-hoc exceptions)

  • 类型化异常(typed exceptions)

特设
my Int $age = 21;
die "Error !";
类型化
my Int $age = 21;
X::AdHoc.new(payload => 'Error !').throw;

使用 die 子例程后面跟着异常消息来抛出特设异常。

类型化异常是对象, 因此上面的例子中使用了 .new() 构造函数。
所有类型化异常都是从类 X 开始, 下面是一些例子:
X::AdHoc 是最简单的异常类型
X::IO 跟 IO 错误有关。
X::OS 跟 OS 错误有关。
X::Str::Numeric 跟把字符串强制转换为数字有关。

查看异常类型和相关方法的完整列表请到 https://docs.raku.org/type-exceptions.html

11. 正则表达式

正则表达式(regular expression), 或 regex 是一个用于模式匹配的字符序列。
(译注,regular expression和regex都是“正则表达式”,后者是前者的缩写。本文中的regex同时也是Perl6的类名,它包含“Perl6专属”的用法,与其他语言甚至Perl5中的正则语法不完全一致,故不翻译,以示区分。)

理解它最简单的一种方式是把它看作模式。

if 'enlightenment' ~~ m/ light / {
    say "enlightenment 包含单词 light";
}

在这个例子中, 智能匹配操作符 ~~ 用于检查一个字符串(enlightenment)是否包含一个单词(light)。

"Enlightenment" 与regex m/ light / 匹配。

11.1. Regex 定义

正则表达式可以按如下方式定义:

  • /light/

  • m/light/

  • rx/light/

除非显式地指定, 否则空白是无关紧要的, m/light/m/ light / 是相同的。

11.2. 匹配字符

字母数字字符和下划线 _ 在表达式中是按原样写出的。
所有其它字符必须使用反斜线或用引号围起来以转义。

反斜线
if 'Temperature: 13' ~~ m/ \: / {
    say "提供的字符串包含冒号 :";
}
单引号
if 'Age = 13' ~~ m/ '=' / {
    say "提供的字符串包含等号 = ";
}
双引号
if 'name@company.com' ~~ m/ "@" / {
    say "这是一个有效的电子邮件地址,因为它包含 @ 字符";
}

11.3. 匹配字符类

字符可以分类,我们可以用类别匹配字符。
也可以匹配该类别的反面(除此之外的所有内容):

类别

Regex

反类别

Regex

单词字符 (字母数字下划线)

\w

除了单词字符之外的任意字符

\W

数字

\d

除了数字之外的任意字符

\D

空白

\s

除了空白之外的任意字符

\S

水平空白

\h

除了水平空白之外的任意字符

\H

垂直空白

\v

除了垂直空白之外的任意字符

\V

制表符空白(Tab)

\t

除了制表符空白之外的任意字符

\T

换行符

\n

除了换行符之外的任意字符

\N

if "John123" ~~ / \d / {
  say "这不是有效名称,不允许使用数字";
} else {
  say "这是个有效名称"
}
if "John-Doe" ~~ / \s / {
  say "这个字符串包含空白";
} else {
  say "这个字符串不包含空白"
}

11.4. Unicode 属性

就像之前章节看到的, 匹配字符类很方便。
话虽这么说,更系统的方法是使用 Unicode 属性。
这样就可以让你匹配在 ASCII 标准内和标准外的字符集。 Unicode 属性闭合在 <: > 中。

if "Devanagari Numbers १२३" ~~ / <:N> / {
  say "包含数字";
} else {
  say "不包含数字"
}

if "Привет, Иван." ~~ / <:Lu> / {
  say "包含大写字母";
} else {
  say "不包含大写字母"
}

if "John-Doe" ~~ / <:Pd> / {
  say "包含破折号";
} else {
  say "不包含破折号"
}

11.5. 通配符

通配符也可以用在 regex 中。

. 意味着任何单个字符。

if 'abc' ~~ m/ a.c / {
    say "匹配";
}

if 'a2c' ~~ m/ a.c / {
    say "匹配";
}

if 'ac' ~~ m/ a.c / {
    say "匹配";
  } else {
    say "不匹配";
}

11.6. 量词

量词在字符后面用于指定我们期望匹配它前面的东西的次数。

问号 ? 意思是 0 或 1 次。

if 'ac' ~~ m/ a?c / {
    say "匹配";
  } else {
    say "不匹配";
}

if 'c' ~~ m/ a?c / {
    say "匹配";
  } else {
    say "不匹配";
}

星号 * 意思是 0 或多次。

if 'az' ~~ m/ a*z / {
    say "匹配";
  } else {
    say "不匹配";
}

if 'aaz' ~~ m/ a*z / {
    say "匹配";
  } else {
    say "不匹配";
}

if 'aaaaaaaaaaz' ~~ m/ a*z / {
    say "匹配";
  } else {
    say "不匹配";
}

if 'z' ~~ m/ a*z / {
    say "匹配";
  } else {
    say "不匹配";
}

+ 意思是至少匹配 1 次。

if 'az' ~~ m/ a+z / {
    say "匹配";
  } else {
    say "不匹配";
}

if 'aaz' ~~ m/ a+z / {
    say "匹配";
  } else {
    say "不匹配";
}

if 'aaaaaaaaaaz' ~~ m/ a+z / {
    say "匹配";
  } else {
    say "不匹配";
}

if 'z' ~~ m/ a+z / {
    say "匹配";
  } else {
    say "不匹配";
}

11.7. 匹配结果

当匹配字符串的regex成功时, 匹配结果被存储在一个特殊的变量 $/ 中。

脚本
if 'Rakudo is a Raku compiler' ~~ m/:s Raku/ {
    say "匹配的内容是:" ~ $/;
    say "匹配之前的字符串:" ~ $/.prematch;
    say "匹配之后的字符串:" ~ $/.postmatch;
    say "匹配从字符串此处开始:" ~ $/.from;
    say "匹配从字符串此处结束:" ~ $/.to;
}
输出
匹配的内容是:Raku
匹配之前的字符串:Rakudo is a
匹配之后的字符串: compiler
匹配从字符串此处开始:12
匹配从字符串此处结束:18
解释

$/ 返回一个 Match Object (匹配 regex 的字符串)。
下面的方法可以在 Match Object 身上调用:

.prematch 返回匹配前面的字符串
.postmatch 返回匹配后面的字符串
.from 返回匹配的开始位置
.to 返回匹配的结束位置

默认地,空白符在 regex 中会被忽略。
如果想在 regex 中包含空白, 我们必须显式地这样做。
regex m/:s Raku/ 中的 :s 用于强制匹配空白符。 另外, 可以把 regex 写为 m/Perl\s6/ ,使用 \s 代表空白符。 如果 regex 中包含的空白不止一个, 使用 :s 比使用 \s 更高效。

11.8. 例子

让我们检查一个邮件是否合法。
我们假设一个合法的电子邮件地址的形式如下:
名 [点] 姓 [at] 公司名 [点] (com/org/net)

这个例子中用于电子邮件检测的 regex 不是很准确。
它的核心意图是用来解释 Raku 中的 regex 的功能的。
不要在生产中原样使用它。
脚本
my $email = 'john.doe@perl6.org';
my $regex = / <:L>+\.<:L>+\@<:L+:N>+\.<:L>+ /;

if $email ~~ $regex {
  say $/ ~ " 是一个合法的Email地址";
} else {
  say "这不是合法的Email地址";
}
输出

john.doe@perl6.org 是一个合法的Email地址

解释

<:L> 匹配一个字母
<:L>+ 匹配至少一个字母
\. 匹配单个 . 符号
\@ 匹配单个 @ 符号
<:L+:N> 匹配一个字母或数字
<:L+:N>+ 匹配至少一个字母或数字

其中的 regex 可以分解成如下:

  • <:L>+

  • [点] \.

  • <:L>+

  • [at] \@

  • 公司名 <:L+:N>+

  • [点] \.

  • com/org/net <:L>+

可选的, 一个 regex 可以被分解成多个具名 regex 。
my $email = 'john.doe@perl6.org';
my regex 多个字母 { <:L>+ };
my regex 点 { \. };
my regex at { \@ };
my regex 多个字母与数字 { <:L+:N>+ };

if $email ~~ / <多个字母> <点> <多个字母> <at> <多个字母与数字> <点> <多个字母> / {
  say $/ ~ " 是一个合法的Email地址";
} else {
  say "这不是合法的Email地址";
}

具名 regex 是使用 my regex 表达式名 { regex 定义 } 定义的。
具名 regex 可以使用 <表达式名> 来调用。

更多关于 regex 的内容, 查看 https://docs.raku.org/language/regexes

12. Raku 模块

Raku是通用编程语言。 它可以用于处理众多任务,包括: 文本处理,图形,网络,数据库,网络协议等。

可重用性是一个非常重要的概念,程序员不必在每次他们想要执行新任务时重新发明轮子。

Raku 允许创建和重新分发 modules。 每个模块是一组封装的功能,可以在安装后重复使用。

Zef 是 Rakudo Star 中自带的模块管理工具。

要安装指定的模块, 在终端中键入如下命令:

zef install "module name"

Raku 的模块目录可以在 https://modules.raku.org/ 中找到。

12.1. 使用模块

MD5 是一个关于密码的散列函数,它产生一个128位的散列值。
MD5 有多种加密存储在数据库中的口令的应用程序。 当新用户注册时,其证书并不存储为纯文本,而是 哈希。 这样做的理由是,如果该数据库被破解,攻击者将不能够知道口令是什么。

比方说,你需要一个生成密码的MD5哈希以存储在数据库中备用的脚本。

幸运的是, Raku 已经有一个能实现 MD5 算法的模块。我们来安装它:
zef install Digest::MD5

现在运行下面的脚本:

use Digest::MD5;
my $password = "password123";
my $hashed-password = Digest::MD5.new.md5_hex($password);

say $hashed-password;

为了运行创建哈希的 md5_hex() 函数, 我们需要加载需要的模块。
use 关键字用于在脚本中加载模块。

实际上,MD5 哈希是不够的,因为它容易被字典攻击。
它应该加盐。维基百科:盐 (密码学)

13. Unicode

Unicode 是编码并表现文本的标准, 它涵盖了世界上大多数书写系统。
UTF-8 是能够以Unicode编码所有可能的字符或代码点的字符编码。

字符的定义是通过:

字素: 视觉表示
代码点: 赋值给字符的数字
代码点名称: 字符的名称

13.1. 使用 Unicode

让我们看一下使用 Unicode 能输出什么
say "a";
say "\x0061";
say "\c[LATIN SMALL LETTER A]";

上面 3 行展示了构建字符的不同方法:

  1. 直接写出字符(字素)

  2. 使用 \x 和代码点

  3. 使用 \c 和代码点名字

现在我们来输出笑脸
say "☺";
say "\x263a";
say "\c[WHITE SMILING FACE]";
组合两个代码点的另外一个例子
say "á";
say "\x00e1";
say "\x0061\x0301";
say "\c[LATIN SMALL LETTER A WITH ACUTE]";

字母 á 可以被写为:

  • 使用它的唯一代码点 \x00e1

  • 或作为 a 和 重音符号 \x0061\x0301 代码点的组合

有些方法可以使用
say "á".NFC;
say "á".NFD;
say "á".uniname;
输出
NFC:0x<00e1>
NFD:0x<0061 0301>
LATIN SMALL LETTER A WITH ACUTE

NFC 返回唯一的代码点。
NFD 分解(decompose)那个字符并返回每部分的代码点。
uniname 返回代码点的名字。

Unicode 字符可以用作标识符:
my $Δ = 1;
$Δ++;
say $Δ;
Unicode 可以用作做算术:
my $var = 2 + ⅒;
say $var;

13.2. Unicode 相关的操作符

13.2.1. 数字

10 个阿拉伯数字 "0, 1, 2, 3, 4, 5, 6, 7, 8, 9" 是今天全世界使用最广泛的数字字符集。

不过在全世界各地其他数字字符集也少量地被使用。

在使用不同数字字符集的时候,不需要特别的留意。所有方法、操作符都会像在阿拉伯数字上那样工作。

say (٤,٥,٦,1,2,3).sort;  # (1 2 3 4 5 6)
say 1 + ٩;               # 10

13.2.2. 字符串

我们在进行字符串操作的时候,结果可能不会像我们期待的那样,特别是在进行比较或排序的时候。

比较
say 'a' cmp 'B'; # More

上面的例子显示 a 要比 B 更大。这是因为小写 a 的代码点比大写 B 的代码点大。

虽然这在技术上是对的,不过它可能不是我们所期待的。

幸运的是 Raku 拥有一套应用了 Unicode 排序算法的操作符。
unicmp 是其中一个,它的功能和上面的 cmp 相似,不过是 unicode 相关的。

say 'a' unicmp 'B'; # Less

你看到了吗,使用 unicmp 操作符的时候得到 a 小于 B 的结果。

排序

Raku 提供了 collate 方法来替代代码点排序方法 sort 应用于 unicode,collate 应用了 Unicode 排序算法

say ('a','b','c','D','E','F').sort;    # (D E F a b c)
say ('a','b','c','D','E','F').collate; # (a b c D E F)

14. 并行、并发和异步

14.1. 并行

在正常情况下, 程序中的所有任务都是相继地运行的。
这可能不是个事儿除非你正尝试去做的东西需要耗费很多时间。

幸亏Raku 拥有能让你并行地运行的功能。
此时, 需要注意到的是存在两类并行方式:

  • 任务并行化: 两个(或更多)独立的表达式并行地运行。

  • 数据并行化: 单个表达式并行地迭代列表中的元素。

让我们从后者开始。

14.1.1. 数据并行化

my @array = (0..50000);                     # 数组总体
my @result = @array.map({ is-prime $_ });   # 为每个数组元素调用 is-prime(判断是否为质数)
say now - INIT now;                         # 输出脚本完成花费的时间
考虑上面的例子:

我们只做一个操作 @array.map({is-prime $_})
is-prime 子例程相继被每个数组元素所调用:
is-prime @array[0] 然后是 is-prime @array[1] 然后是 is-prime @array[2] 等等。

幸运的是, 我们能同时在多个数组元素身上调用 is-prime 函数:
my @array = (0..50000);                         # 数组总体
my @result = @array.race.map({ is-prime $_ });  # 为每个数组元素调用 is-prime(判断是否为质数)
say now - INIT now;                             # 输出完成所花费的时间

注意表达式中使用的 race。这个方法会使数组元素能够并行地迭代。

运行两个例子(使用和不使用 race)运行之后, 比较两个脚本运行结束所花费的时间。

race 不会保存元素的顺序。如果你想那样做, 使用 hyper 代替。

race
my @array = (1..1000);
my @result = @array.race.map( {$_ + 1} );
@result».say;
hyper
my @array = (1..1000);
my @result = @array.hyper.map( {$_ + 1} );
@result».say;

如果你俩个脚本都运行了, 你应该注意到一个排序了, 一个没有排序。

14.1.2. 任务并行化

my @array1 = (0..49999);
my @array2 = (2..50001);

my @result1 = @array1.map( {is-prime($_ + 1)} );
my @result2 = @array2.map( {is-prime($_ - 1)} );

say @result1 == @result2;

say now - INIT now;
考虑上面的例子:
  1. 我们定义了 2 个数组

  2. 对每个数组应用不同的操作并保存结果

  3. 并检查两个结果是否相同

该脚本等到 @array1.map( {is-prime($_ +1)} ) 完成
然后计算 @array1.map( {is-prime($_ +1)} )

应用到每个数组的俩个操作彼此间没有依赖。

为什么不并行地执行呢?
my @array1 = (0..49999);
my @array2 = (2..50001);

my $promise1 = start @array1.map( {$_ + 1} );
my $promise2 = start @array2.map( {$_ - 1} );

my @result1 = await $promise1;
my @result2 = await $promise2;

say @result1 == @result2;

say now - INIT now;
解释

start 方法计算它后面的代码并返回 promise 类型的对象promise
如果代码被正确地求值, 那么 promise 会被 保留(kept)。
如果代码抛出异常, 那么 promise 会被 破坏(broken)。

await 子例程等待一个 promise
如果那个 promise 是被 保留 的, await 会获取到返回值。
如果那个 promise 是被 破坏 的, await 会获取到抛出异常。

检查每个脚本完成所花费的时间。

并行总是添加线程开销。如果开销抵消不了运算速度的增长,那么该脚本会显得较慢。
这就是为什么,在很简单的脚本中使用 racehyperstartawait 实际上可以使它们慢下来。

14.2. 并发和异步

关于并发和异步编程的更多信息, 请查看 https://docs.raku.org/language/concurrency

15. Native Calling 接口

Raku 可以让我们通过 Native Calling 接口来使用 C 库。

NativeCall 是 Raku 自带的标准模块,它提供了一系列功能方便了 Raku 和 C 的接口。

15.1. 调用函数

下面的 C 代码定义了一个名为 hellofromc 的函数。 这个函数的功能是在终端中打印 Hello from C。它不接收参数,也不返回值。

ncitest.c
#include <stdio.h>

void hellofromc () {
  printf("Hello from C\n");
}

根据你的操作系统将上面的 C 代码编译成库文件。

在 Linux 中:
gcc -c -fpic ncitest.c
gcc -shared -o libncitest.so ncitest.o
在 Windows 中:
gcc -c ncitest.c
gcc -shared -o ncitest.dll ncitest.o

在你编译 C 库的路径下新建一个包含下面代码的 Raku 文件,并运行它。

ncitest.raku
use NativeCall;

constant LIBPATH = "$*CWD/ncitest";
sub hellofromc() is native(LIBPATH) { * }

hellofromc();
解释

首先,我们声明使用 NativeCall 模块。
接着,我们定义了一个常量 LIBPATH 来存储 C 库的路径。
其中 $*CWD 返回当前目录。
然后,我们定义一个新的 Raku 子例程 hellofromc 作为 C 库中名称同为 hellofromc 的 C 函数之包装函数。这个 C 库就是 LIBPATH 所对应的。
这些是通过使用 is native 特征实现的。
最后,调用我们的 Perl6 子例程。

总而言之,整个过程即声明一个带有 is native 和 C 库名为特征的子程序。

15.2. 函数重命名

上面的例子中,我们看到了如何通过 is native 特征使用同名 Raku 子例程来包装 C 函数从而调用它。

有时我们希望更改 Raku 子例程的名称。
为此,我们需要使用 is symbol 特征。

下面就来修改上面的 Raku 脚本,将 Raku 子例程 hellofromc 重命名为 hello

ncitest.raku
use NativeCall;

constant LIBPATH = "$*CWD/ncitest";
sub hello() is native(LIBPATH) is symbol('hellofromc') { * }

hello();
解释

由于 Raku 子例程与对应的 C 函数不同名,我们在这里需要使用 is symbol 来提供原始的 C 函数名。

15.3. 参数传递

编译下面更改过的的 C 库代码并运行 Raku 脚本。
注意我们是如何修改 C 和 Raku 代码来接收一个字符串参数(在 C 中是 chr*,在 Raku 中是 Str

ncitest.c
#include <stdio.h>

void hellofromc (char* name) {
  printf("Hello, %s! This is C!\n", name);
}
ncitest.raku
use NativeCall;

constant LIBPATH = "$*CWD/ncitest";
sub hello(Str) is native(LIBPATH) is symbol('hellofromc') { * }

hello('Jane');

15.4. 返回值

让我们再来定义一个简单的计算器,实现接收两个整数输入并返回它们之和。
编译下面的 C 库,并运行 Raku 脚本。

ncitest.c
int add (int a, int b) {
  return (a + b);
}
ncitest.raku
use NativeCall;

constant LIBPATH = "$*CWD/ncitest";
sub add(int32,int32) returns int32 is native(LIBPATH) { * }

say add(2,3);

注意其中 C 和 Raku 函数如何接收两个整数并返回一个整数。(C 中的 int 和 Raku 中的 int32

15.5. 类型

你可能要问,为什么在最后的 Raku 脚本中我们要使用 int32 来代替 Int
因为在 Raku 中像 IntRat 等类型不能用来传递与接收 C 函数中的值。
所以必须在 Raku 中使用同 C 中类型相对应的类型。

幸运的是,Raku 提供了许多数据类型来对应 C 中的数据类型。

C 类型 Raku 类型

char

int8

int8_t

short

int16

int16_t

int

int32

int32_t

int64_t

int64

unsigned char

uint8

uint8_t

unsigned short

uint16

uint16_t

unsigned int

uint32

uint32_t

uint64_t

uint64

long

long

long long

longlong

float

num32

double

num64

size_t

size_t

bool

bool

char* (String)

Str

Arrays: 比如 int* (Array of int) 和 double* (Array of double)

CArray: 比如 CArray[int32]CArray[num64]

更多关于 Native Calling 接口, 详见 https://docs.raku.org/language/nativecall

16. 社区