Raku 语法小册

on

1. 运行 Raku

如何安装 Raku 或只是准备好运行你的脚本或程序

Raku 是一种编程语言,因此它是包含一系列应用程序的开发生态系统的一部分。 在本书中将使用其中的一些内容,因此在进入 Raku 本身之前,你可能需要安装以下内容。

  • Git 是主流的源代码控制和开发工作流工具。 你可能需要它来下载一些 Raku 模块,或者以某种方式从源代码安装它。

  • 编程语言编辑器。 Raku 具有自己的集成开发环境(IDE),称为 Comma,你可以从 Edument 获得其许可证。 社区版于 2019 年 3 月发布。 该 IDE 包含你希望获得的大多数好处,包括语法高亮显示,程序运行和文档预览。 在其余的编辑器中,Atom 的支持可能是最好的,包括高亮显示和语法检查。 其他编辑器,例如 Emacs,VS Code 和 vim,也支持 Raku,如果你已经熟悉它们,可能就足够了。

  • Docker 是一个服务隔离框架,如今已广泛用于软件包分发。安装它可以帮助你避免把东西永久地安装在系统上。你还可以通过使用特定应用程序的名称来使其快速运行。

  • 在 Raku 的世界中,很多动作都发生在互联网中继通道中。尽管你可以从浏览器访问其中的一些,但是拥有自己的 IRC 桌面客户端将通过无缝身份验证和自动化实现你的入口。 你还将从命令行运行一些脚本。对它的工作原理以及如何在不同的操作系统中使用它的基本理解总是有帮助的。由于你要安装 Raku 以及其他附带的东西,因此对 apt 等打包系统(默认情况下将使用 Debian 和 Ubuntu 打包系统)以及 Windows 的 Chocolatey/NuGet 或 Mac 的 Brew 将派上用场。

准备就绪后,继续安装 Raku。

1.1. 在你自己的电脑上运行 Raku

你准备好了吗? 让我们开始吧。

但是,等等。你需要知道 Raku 是什么以了解你将实际安装的内容。

为了实现并发核心并全力支持 Unicode 和其他优点,Raku 可以在专门为其设计的虚拟机中运行。该虚拟机称为 MoarVM,它是最终运行由你编写的程序生成的代码的虚拟机。但这不是 Raku 使用的唯一 VM:Java 虚拟机也成为目标,并且在开发中,虽然已经可用,但它是 JavaScript VM(允许它在浏览器上运行 Raku,但稍后会介绍更多)。因此,这是 Raku 的基础:虚拟机(在三个虚拟机中选择)。

现在的问题是,你需要从高级 Raku 代码中为三个虚拟机生成原生代码。这可能是个问题,因为你需要将 Raku 中的每个小原语都转换为它们。通常使用某种中间语言(在这种情况下称为 NQP 或 Not Quite Perl)来解决。这是第二部分。

第三部分是解释器本身。 Raku 是由一组测试定义的语言,通过测试的任何解释器都可以被正确地称为 Raku。目前,仅有一组成员,称为 Rakudo。此外,Rakudo(大部分)是用 Raku 编写的。由于 Rakudo 是用 Raku 编写的,因此你需要 NQP 才能将其编译为要使用的虚拟机中的原生代码,并且需要安装所有三个部分:Rakudo ,NQP 和 MoarVM 或 JVM 或 JavaScript 虚拟机。

这是在安装 Raku 时实际安装的东西。现在开始进行介绍。

在任何操作系统上安装 Raku 的最佳选择是使用 Rakudo Star(Rakudo*)发行版。它包括你需要运行一个 Raku 程序的所有内容,再加上模块和实用程序,以安装新模块并在命令行上舒适地工作。每四个月有一个新的 Rakudo Star 版本:即每年的 11 月,4 月和 6 月。它包括适用于 Mac 和 Windows 的自安装程序包,以及适用于通用 Linux 的一组二进制文件。 Rakudo* 将满足你对 Raku 的大部分需求,因为它还包装了一些使你的生活更轻松的模块,例如 REPL 中用于命令行编辑的 Linenoise 库。

但是,Raku 每月都会发布新版本。有时,这些版本包括相当大的改进,并且始终会修复错误并实现新功能。有几种方法可以在计算机上安装它们:

  • 使用版本管理器 rakudobrew 下载,编译和安装最新版本。安装完成后,输入

rakudobrew list-available

将为你提供以 year.month 形式下载的版本列表,例如,2018年12月发布的版本为 2018.12。你可以通过以下命令安装该版本

rakudobrew build moar 2018.12

它将下载并构建以 MoarVM 开头的堆栈和该版本的 Raku。

  • 使用 Docker 容器,其额外的优势是你不需要任何特定工具,只需 Docker 客户端和服务器。例如,

docker run -it jjmerelo/alpine-raku:2018.11

可以用作 Rakudo 版本 2018.11 的直接替代品。你还可以使用 docker pull rakudo-star 拉取官方 Rakudo Star 容器。其他 Docker 容器也可用;但是,这是唯一的官方文件。

  • ClaudioRamírez 每月都会为 Rakudo 的诸多 Linux 发行版创建更新的软件包。在 https://github.com/nxadm/rakudo 中查看 GitHub 存储库。 pkg 提供了有关如何使用标准打包系统下载用于 Debian/Ubuntu,Fedora 和 openSUSE 的 Raku 的说明,以及如何通过直接下载来下载其他发行版(如 Alpine 或 CentOS )的说明。

  • 你可以使用软件包管理器 Chocolatey 在 Windows 中安装 Raku。只需键入 choco install rakudostar。这仅包括 Rakudo* 版本,这是使用 Raku 的首选方式。

  • 如果你的 Mac 安装了 HomeBrew,则类似的命令将启动并运行 Raku:brew install rakudostar

这些和其他第三方发行版在 https://raku.org/downloads/others.html 列出。有关更多信息,请参见表1-1。

Table 1. 下载信息

操作系统

说明

All

https://rakudo.org/files 下载

Linux

https://github.com/nxadm/rakudo.pkg 说明

Windows

choco install rakudostar

Mac

brew install rakudostar

1.2. Raku 在线

容器和 rakudo.js, 是正在进行中的 Raku 的 JavaScript 实现, 这让 Raku 在线运行变得十分容易。下面的在线 REPL(读取,求值,打印循环)允许你在旅途中或在外部计算机中测试你的脚本:

  • Glot.io 上的 https://glot.io/new/raku。就像这样插入一行并点击运行按钮。你也可以把你的代码片段保存起来并发布。

"Hello".comb.map( * ~ 0x20E3.chr ).join().say
  • tio.run 在 https://tio.run/#raku 上拥有一个 Raku 引擎。把上面的代码插入到代码标签下面并点击 Play 图标。输出会显示在下面:H□e□l□l□o□, 还会显示一些调试统计信息。

  • Ideone(在 ideone.com 下) 在其它区域把 Raku 列为 "Perl"。剪切并粘贴上面的代码, 按下 Ctrl+Enter 或点击提交按钮, 你会在标准输出插槽中获得输出。这个甚至允许运行交互式命令, 并提供一个标有 stdin 的标准输入插槽。把上面的程序更改为:

$*IN.comb.map( * ~ 0x20E3.chr ).join().say

并在单击“提交”之前写一些内容作为输入,你将得到用方括号围起来的输入,与上一个程序中的 Hello 相同。

尽管暂时有些实验,但其中最有趣的是 6pad:https://raku.github.io/6pad/。这是在你的浏览器上运行的 Raku 的实际副本,目前(2019年初)可以运行,但尚未完成。只需在左侧面板上输入或粘贴程序即可。该面板在 Raku 选项卡中打开,然后单击 Run。另外,你可以直接获取 HTML 输出,就像在 https://rakuadvent.wordpress.com/2018/12/07/ 上次日历条目细胞自动机中所做的那样。

到你读完本书时,这本书独自一人坐在“把事情做好”和《冰与火之歌》的第七本书卷下面,这些 URL 可能不再存在。但是,可能会有更稳定的 Rakudo.js 实现和/或你将能够自行设置基于容器的沙盒环境。无论如何,都会有一种在线运行代码并共享以进行演示的方法,这是这些站点的主要目标。

Internet 比 Web 宽,在其他地方,你可以检查脚本,主要是单行代码,这要归功于一系列能够计算 Raku 并在多个 IRC 频道上收听的机器人。

在下一章中,我将更广泛地讨论 IRC 和这些频道。目前,只有你已经是 IRC 的常客,你才可能对下一段感兴趣。 我将在稍后讨论它们,但是 Freenode 中有一个名为 #whateverable 的频道,该频道专门用于运行这些机器人。 Camelia 机器人使用 Raku 的最新编译版本计算你的代码,如图1-1所示。

1.3. 命名行(和其它)选项

图 1-1 使用 Weechat IRC 客户端运行 Camelia(即Evalable)

是时候运行你自己的小程序了,对吧? 只需输入 raku (如果你是从源代码,二进制软件包或其他东西安装的),或者使用的是包括运行 Docker 容器的东西。 无论哪种方式,你都会找到一个命令行,如图1-2所示。

如果你安装了命令行编辑插件(这是 Rakudo* 的标准配置,但没有其它选项),则可以使用左右光标编辑输入,并可以使用向上/向下箭头访问之前的命令。

在该命令行中,你可以

  • 编写直接求值的表达式

  • 编写语句,其结果将在下一行中打印

例如,只键入 "Hello"。这是一个字面量表达式,它将照此打印。 编写 "Hello" ~ ", " ~ "world",这个表达式将三个字符串连接在一起并打印结果。 这就是 Raku 的 REPL,它将读取,求值(作为表达式或语句)并打印结果。

尽管功能齐全,但是你可能需要从其他脚本中运行 Raku 脚本,或者只是重复运行它们。 你可以使用 raku -e 从命令行执行此操作。 所以:

> raku -e "'hello'.comb.put"

运行该小脚本,该脚本会按字母顺序对字符串进行梳理或分割,然后打印结果。 由于结果是一个列表,因此它看起来只是一组由空格分隔的字母。 由于这是一个实际程序,因此可以向其传递参数:

> raku -e "@*ARGS.join.comb.put" hello world
h el l o w o r l d

@*ARGS 数组包含你已传递的所有参数,在本例中为 hello world,将它们连接在一起,然后按字母分隔,即你看到的结果。

Raku --help 返回命令行中所有可用的选项。有些是针对高级用户的,但有些对于任何类型的用户都非常有趣:

  • -v--version 打印版本号。每当你问一个问题时,你都将需要此功能,因为人们首先需要了解的就是你使用的版本。它打印:

    This is Rakudo version 2018.12 built on MoarVM version 2018.12
    implementing Raku.d.

此消息指出以下事实:每个 Raku 解释器都由 Rakudo(解析你的程序的实际程序)和虚拟机(在本例中为 MoarVM)组成。除了说出正在使用的版本外,它还说明要遵循的规范,在本例中为 6.d。 Raku 版本不时更改,第一个可投入生产的版本称为 6.c(圣诞节),而新发布的版本 6.d(排灯节)。 6.e 尚无计划的发布时间。

  • -c 检查程序的语法,如果通过检查则打印 OK。如果这是你唯一想要的,或者你想检查外部代码的正确性,则很有用。 请记住,这并不意味着它会检查所有可能的错误,而只是检查通过读取源代码而无需实际运行即可静态检测到的那些错误。 但是,这并不意味着语法检查不会编译任何内容。 有些代码在编译阶段执行,因此:

raku -c -e "BEGIN { say «Gotcha» }"

在语法 OK 之前打印 Gotcha。查看表 1-2 中的汇总。

Table 2. 表 1-2 函数和命令

函数

命令行标记

从命令行运行

-e

命令行标记和选项的帮助

--help

只检查语法

-c

打印 Raku 版本

-v/--version

1.4. 结束语

对于大多数人来说,Rakudo Star 发行版是最佳选择。 请使用它们作为 REPL 或从命令行运行(在此阶段)单行代码。

由于 Raku 是一种没有默认 IDE 的解释型语言,因此在终端中从命令行运行它是正确的用法,你将在本书中进行操作。

2. 获取帮助

如何让别人回答你的问题并开始成为大 Perl 社区的一部分

如果你希望你了解更多,或者只是在学习道路上遇到困难时,如果不能超越本书的范围和方式来寻求帮助,那么这样的技术书是不完整的。 帮助触手可及; 但是,乍看之下,哪些来源是最权威的,或者如何与之互动以得出答案,这一点尚不清楚。 在本章中,我将讨论这些资源以及如何利用它们来更多地了解 Raku。

2.1. 第一响应者

这些是你应该首先寻找答案并解决问题的地方。 首先,请查阅 Raku 的官方文档,该文档可从 https://docs.raku.org/ 在线获取。 它在右上角包含一个有用的搜索框,你可以在其中搜索已被索引的术语(例如字符串)或在整个站点中搜索任何一组单词或句子; 下拉菜单中的最后一个选项始终是“搜索整个网站”。如果你的查询未建立索引,则会显示“不在索引中,请尝试网站搜索”,这将通过 Google 返回该网站中的包含该词的所有页面。例如,搜索 Ubuntu 将返回以下页面:https//docs.raku.org/language/5to6-perlvar

该文档分为几个部分,并有各自的首页。 语言部分(https://docs.raku.org/language.html)包括一些教程和通过示例向你介绍概念的 页面。 “开始”标题下列出的页面可能会特别有用。 函数和数据类型列在“类型和例程”标题下; 你会找到详尽的清单,列出所有,类型,例程以及它们的功能。 最好将它们用作参考或通过很少的搜索工具。 当有人告诉你 RTF M时,他们可能指的是这部分。 通常,虽然 Raku 社区试图避免这种行为,但是至少知道在哪里可以找到诸如官方参考之类的东西并不是一个坏主意。

由于这是一件大事,包括近 10 万行,因此在下载 Raku 时默认情况下未安装(如上一章所示),但是你始终可以使用以下方式获得自己的本地版本

zef install p6doc

该文档及其网站也可以作为 Docker 容器使用。 如果你输入

docker run --rm -it -p 3000:3000 jjmerelo/raku-doc

在命令行上,它将在本地主机的 3000 端口运行 https://docs.raku.org 的内容。首次运行后,它将在本地存储图像,因此你可以随时查询它。

获得文档后,你随时可以通过谷歌回答有关 Raku 的问题。请确保输入 Raku 或 raku,以正确的语言获得有关问题的答案。例如,“如何在 raku 中保存文件”将返回 Raku 文档中的关于输入/输出的页面 https://docs.raku.org/language/io

你还可以使用程序员和隐私友好的 DuckDuckGo。但是,再次输入 Raku。 DuckDuckGo 高亮在 Stack Overflow 中找到的搜索结果,在这种情况下,它返回有关如何读取和写入 XLSX 文件的页面:https://stackoverflow.com/questions/48050617/what-perl-6-modules-can-read-write-xlsx-file#48051038。这个答案还真不错,但是你的工作量可能会因其他查询而异。

在另一个搜索引擎必应上尝试相同的查询将返回各种结果,大多数与 Raku 无关。Yandex 是俄罗斯血统的搜索引擎,确实返回了几页涉及 Raku 的页面,但与如何保存文件无关。归根结底,如果你要使用搜索引擎来寻找 Raku 中问题的答案,那么最好还是坚持使用谷歌。

另一方面,还有其他方法可以获取以前未回答的问题的答案。Stack Overflow(在 DuckDuckGo 中显示为边栏)是各种程序员的首选站点。它具有一种“业力”机制,可以奖励提供良好问题和答案的用户。 Raku 社区在 Stack Overflow 上非常活跃,主要使用 raku 和相关标签,例如 Rakudo,MoarVM 或 NativeCall。

总的来说,它也非常友好和乐于助人,并提供文档或教程的链接以及动机良好的文章。但是,如果在制定问题时遵循一些规则,它总是有帮助的:

  • 明确说明你的意图。你的问题可能暗示着一种解决原始问题的方法,这可能不是最好的方法。说明你的目标将有助于响应者为你提供其他可能的到达目标的场所。

  • 将你的问题找出可以重现的最小程序。对于想要帮助遍历20行代码的人来说,这是很困难的,其中只需要三到四行即可重现它。剪切和粘贴代码也足以运行它并重现结果。

  • 将你获得的输出以文本形式原样粘贴,以便其他人搜索相同的错误将能够解决你的问题并得到答案的帮助。

  • 显示你已测试的所有内容。 如果在 Stack Overflow 中有类似的问题回答(在你编写时会显示),请链接至它们,并说明你的问题为何与它们不同。

  • 如果有一个答案几乎涵盖了你的问题,请单击对勾接受它。 如果其他人喜欢你的问题,这将帮助其他人找到解决问题的方法。 另外,如果有帮助,请慷慨地投票赞成评论和其他答案。

有时你现在就需要答案,它既不在 Internet 中的某个地方,也不在 Stack Overflow 中。 你可能会在那里问过这个问题,但在短时间内没有得到任何答案。 你可能想尝试其他方法。

2.2. IRC 频道和其它在线交流

在第一章中,我提到了 IRC,因为在这里你可以获取运行 Raku 代码的摘要。如果你不知道 IRC 是什么,则可能会跳过这一部分。现在,你确实需要知道如何使用它,但是在你使用最近的联网设备查找 IRC 之前,让我们获取你可能已经在使用的其他资源,例如电子邮件。 raku-users 邮件列表 https://lists.perl.org/list/raku-users.html,它包含了大多数开发人员和大量用户,也是一个提出问题并得到答案的好地方。在发布任何问题之前,可以在 www.nntp.per.lorg/group/perl.perl6.users/ 上搜索档案。通过上述第一响应者和文档,始终可以帮助你首先完成家庭作业。尽管该社区中没有人会回答 RTFM(请阅读精美的手册),但表明你已经阅读了手册,并且不能解决你的需求,这将有助于回答该问题的人正确使用它,并且当然,编写手册的人(也在听)可以改进它们,以便下次真正回答大多数问题时使用。

然后,有 IRC。

我在这里稍作停留,因为你可能不了解 IRC。你对 Telegram 或 Slack 足够了解。好吧,IRC 是你祖母的 Slack: 它代表 Internet 中继聊天,并且从用户的角度来看,它包括一组通道,这些通道用现在无处不在的哈希号#表示,并托管在以下服务器的一系列服务器上: 在自由软件社区中最著名的可能是 FreeNode(https://freenode.net)。因此,IRC 通道的坐标包括一个节点(如上)和哈希名称,例如 #raku。这正是希望获得帮助并通常与 Raku 爱好者讨论或闲逛的人们的首选。

如果你以前从未听说过 IRC,可能也需要一些帮助。最简单的方法是使用基于 Web 的客户端 https://raku.org/IRC。除了证明你不是机器人(不是邪恶的机器人)之外,你只需要使用浏览器即可访问该频道。有时你可能会觉得好像陷入了对话的中间(实际上是这样),因此请检查 https://colabti.org/irclogger/irclogger_logs/raku 上的日志。 将为你提供一些有关进来之前发生的情况的背景信息。

如果最终以一定的频率使用它,则可能需要获得自己的 IRC 客户端。我喜欢 WeeChat,可以将其配置为你常用的(和经过身份验证的)用户名和密码,但是你可能希望使用此处推荐的任何一种: https://opensource.com/life/15/11/top-open-source-irc-clients。随着你进一步深入在社区中,在 IRC 频道中闲逛以帮助他人并获得帮助是一个好习惯。此外,IRC 和其他聊天应用程序一样,包括有用的机器人,它们会自动回答一些问题。raku IRC 频道也不例外,它托管着整个机器人群体,其中最有名的是 Camelia(我在第一章中提到过)。 Camelia 计算 Raku 表达式并用输出回答。你可以通过在行首输入 m: 来调用 Camelia,如下所示:

在下一行中,Camelia 将回答一条消息,指出她正在使用的 Perl 的组合以及用于对其进行编译的提交哈希(即所使用的编译器的实际版本),然后给出答案。在输出之前。

这个答案出现在对话的中间,通常不认为与她开始长时间的交谈是一种礼节。因此,还有一个 #whateverable 渠道,它专门用于与机器人聊天。使用你的在线客户端来 /join#whateverable 或通过 Web 客户端来进行操作(最适合你)。

以下博客将使你每周一次了解 Raku 社区的最新动态:“ Raku及其周围的每周更改”作者: 丽兹·马蒂森(Liz Mattijsen),网址为 https://rakudoweekly.blog/。它提到了 Raku 核心和周围环境的所有发展,时事以及来自 Web 和社交媒体的评论。如果你在 WordPress 中拥有一个帐户,则可以订阅该帐户,也可以随时检查该帐户。此外,在圣诞节临近时,Raku 降临日历会发布有关 Raku 知识的教程和小文章,有时会带有嘲讽的标题和态度。他们一直追溯到 2009 年,所以有很多文章可供学习,评论或点赞。转到 https://perl6advent.wordpress.com/

Facebook 上有一个相当大的社区,网址为 www.facebook.com/groups/raku,尽管该社区主要致力于社区和文章的发布,但也会很乐意回答你的问题。人们会去那里进行首次定向,Wendy van Dijk 以及其他管理员和用户将很乐意帮助你解答问题。这可能是感受 Raku 社区的最佳场所,而不仅仅是作为一种编程语言。

大多数 Raku 开发人员在 Twitter 上都有一个帐户,你可以在此寻求帮助。仅使用 #rakulang 作为主题标签可能会引起一个或另一个的注意,但是转到帐户为 @rakuorg。我也很乐意为 @jjmerelo 提供帮助。

最后但并非最不重要的是位于 www.reddit.com/r/raku/ 的子订阅。 这也是在其他地方发表的文章的信息交换所,有时还会就技术(而非)问题进行激烈的讨论。这是网站的唯一原始部分,因为它是你发布指向其他地方的链接的地方。在所有资源中,这可能是你想要的跳过,因为这里的讨论有时会变得很激烈。但是,如果你已经是 Reddit 用户,那么订阅它并为它做出贡献将帮助你获得有关 Raku 领域其他地方发生的一切的新消息。

你还可以通过查看示例来学习,如果提交自己的示例,则可以得到反馈;最活跃的场所之一是 https://exercism.io/tracks/raku 上的 Exercism.io; Raku 轨道包括各种指导者和社区提出的练习,以及一种简单的方法来检查它们是否正确。由于 Raku 是一种非常有表现力的语言,因此你会发现 Perl 解决方案在 Code-Golf.io 的排名中占据了很多席位。但是,除非作者选择发布它们,否则它们在网站上不可见。例如,一位作者选择在此处发布: https://gist.github.com/mcreenan/ed62d5e743e18d9e91bb87ba3675dc6e

仅搜索 “Raku 示例”将为你提供大量存储库可供选择,但我的建议是首先考虑要解决的特定任务或问题,然后进行搜索或使用所有这些资源逐步接近解决方案。

2.3. 在线下获取帮助

从一开始,本地 Perl 用户组就被称为 Perl Mongers。它们并非无处不在,但是在许多国家的主要城市和首都,有几个活跃的城市。活跃的 Perl Mongers 每月举行一次技术会议,有时甚至是社交会议。也许最著名的是伦敦,但是其他活跃的 Perl Mongers 包括洛杉矶,阿姆斯特丹,辛辛那提,波特兰,多伦多和奥斯丁;其中一些已经活跃了20多年。此外,它们中的大多数都专注于我们的姐妹语言 Perl 5(或简称为Perl),并且可能没有尝试过 Raku。但是,在社区方面,它们是一个友好的地方,你可以在这里进行一般性的讨论开发或提出你的最新项目。

从这个意义上说,其他一些用户组向所有对开发感兴趣的人开放,但是你的努力可能会有所不同。例如,尽管(通常)欢迎各种类型的开发人员,但 Google Developer Groups 专注于 Google 产品。这就是为什么你可能想去参加有关 Perl 的会议的原因,每个月在世界各地可能都有一个会议。然而,主要的是 Perl 会议(每年6月在美国举行),YAPC(又一次 Perl 会议)

Japan(在1月底举行),现在称为 PerlCon。并于8月初在欧洲某个地方举行。由于他们专门致力于 Perl,因此 Raku 只是其中的一小部分,但仍在增长中,但大多数核心开发人员仍将在那里。前几天通常会提供教程,有时会在会议后举行黑客马拉松,因此请计划整整一周。熟悉在生产中使用 Raku 的项目并学习新功能始终具有一定的价值。而且总是有贴纸可以带回家,甚至更好地可以贴在笔记本电脑上。这也是与编写或记录你感兴趣的事物的人面对面获得帮助的最佳方法。

如果你不熟悉该语言,而是想要咨询顾问实际解决问题的方法,有很多人可以为你提供帮助,但是他们的数量和可用性可能会有所不同,因此你可能希望使用其中一个虚拟或物理场所进行检查。 在撰写本文时,Edument 通过 Raku.services 提供 Raku 咨询。 它位于捷克共和国布拉格。 Nigel Hamilton 还从他的公司(位于英国)提供实用的 Raku 培训: https://nigelhamilton.com/raku.html

2.4. 结束语

从文档到顾问,再到快速有效地回答你的问题,Raku 为新手和经验丰富的程序员提供了各种功能。 由于技术的原因,Raku 是你签出的那种语言,但是由于社区,你一直在使用它。 在下一章中,我们将深入研究该技术。

3. 字面值

简单数据的类型化方式以及 Raku 理解它的方式。

让我们首先了解一下如何在 Raku 中构建表达式。表达式是语句的构建块,而它们又是程序的基本构建块,并且 REPL 可以直接解释它们。这是理解该语言的语法和一般意图以及如何将其用于解决简单问题的良好起点。 但首先,

3.1. 让我们来谈论 Unicode

Raku 被设计为完全从 Unicode 开始工作。因此,你需要先了解一点,然后才能了解如何构建和使用表达式。

Unicode 是一个联盟,尽管它以定期接收新表情符号而闻名,但实际上它负责计算机内部如何表示文字信息。 一开始,为人类已知的每个字母中每个符号的数字分配了 Unicode,不仅包括字母和数字,还包括连字和符号。 一些常用的符号(例如印刷符号)被妥善收录,最终添加了表情符号和新流行的符号。 因此,在第一个级别,每个字符都有一个唯一的编号分配给它,这保证了每个程序和操作系统都以相同的方式解释它。 因此,`幺`始终是编号 12083。此外,每个字符或代码点(其正确的面额)也具有名称,通常用大写字母表示。 例如,上面的代码点称为 KANGXI RADICAL SHORT THREAD。

如果具有正确的插件,则可以在编辑器中查找字符名称。 例如,Atom 包括字符表包。 VS-code 有一个名为 Insert Unicode 的插件,emacs 可以通过按 Control-x + 8 + Return + tab,然后使用 Control-s 在单个文档中进行搜索来访问字符名称。 http://unicode.org/charts/charindex.html 上还有一个按字母顺序排列的索引。

每个代码点还具有一组属性,这些属性提示其能够执行的操作以及如何将其与其他代码点连接。 将属性视为标签,对于那些实际上不知道字母的人来说,代码点是什么。 是数字吗? 是符号吗? 是字母吗,那么,什么样的信呢? 当对代码点进行排序或简单地组合以形成复杂的单词或密码或这些符号的实际组合时,这些类型的属性可能变得很重要。 例如,数字可以与其他数字放在一起形成数字,这些数字根据位置分配一个值。 但是你不能将它们与数字结合使用,因为它们具有单个独立的值。

通常你看不到代码点,但可以看到字素,这可以理解为代码点的呈现,也可以看作是第一近似值。但是一个字素也可以是一组代码点。在某些情况下,可能两者兼有。例如,我们说西班牙语的人喜欢我们的 ñ(我们将其称为ehgne),它在 Unicode 带小写字母的拉丁文小写字母 N 中被称为。该名称已经暗示它是一个字母 n,其中包括另一个 Unicode 符号,即波浪号。事实上,如果不将其与其他符号结合使用,则会出现一整套无法打印的符号;在这种情况下,它称为合并波浪号。因此,表示此符号的另一种方法是使用两个代码点,即 n 和组合代字号。请注意,单个代码点和两个代码点使用相同的字素ñ。

表情符号和所谓的名称序列也会发生类似的情况。这些描述也包含在 Unicode 属性中,它们是代码点具有的一组特征。这些属性描述的是其他属性,是小写(小写)还是大写(大写),以便尽管所有其他符号(如波浪号)或大写(大写,小写)也可以识别代码点是否使用相同的字母,以及其他使用不同字母的字母)。 你现在不必担心这一点,但是 Unicode 将在本章(以及本书的其余大部分内容)中一遍又一遍地出现,因此牢记这一小小介绍将有助于你了解很多内容。当 Raku 解析你的代码时,这种情况正在发生,有时甚至是结束。永远记住,Unicode 很重要,这对于理解在 Raku 中如何处理任何类型的书面数据,数字,字母或任何符号是至关重要的。

3.2. 字面值表达式

从字面上看,字面量就是它们本身。 如果他们看起来像数字,那么它们就是数字; 如果它们看起来像字符串,那么他们就是字符串。 但是,Raku 在可以用字面值表示的东西方面非常丰富。 你可以使用 REPL 来键入这些字面值表达式。 如果正确,则会在下一行将其打印回给你。 这样,你就可以尝试任何形式的表达式。

3.2.1. 数值和数字

让我们从数字开始。除了通常的数字(例如3、4.5或.35)之外,分数也可以按字面值表示。 ¾ 是有效的字面值,有理数,与 3/4 一样,它们的值相同(与0.75相同)。

诸如 ¾ 之类的字符称为复合字符,可以通过多种方式从键盘输入它们。通常,它们涉及选择一个键(例如,Right-Control键)作为前缀,以指示接下来的两个将被组合的键。在某些情况下,可能需要其他应用程序。请查询你的操作系统和窗口环境在线帮助,以了解如何执行此操作。在许多情况下,你的编辑器都可以执行此操作(尽管只有在你进行此操作时),并且始终存在应急选项,可以显示来自操作系统或在线状态的一组 Unicode 字符。

数字也可以表示为上标,但是当它们是数字时,它们也可以用作运算符。 3² 中的²表示"平方"。如果单独使用,则表示 2。

默认情况下,如果可能的话,浮点数表示为有理数,因为 Raku 认为这是表示它们的更精确的方法。但是,你可以使用1e0中的"e"形式显式地要求浮点表示。这意味着 1*10^0,而exp(0)恰好是1。1E6是1百万,并且也表示为浮点数。通常,用于写数字的文字会影响其表示形式,但不会影响其值。 1E6、1000000和1000000/1具有相同的数值,尽管它们内部使用不同的类型以不同的方式表示。

实际上,它们是 Raku 中的不同类型。这是一个概念,我们将在下一章中深入探讨。

复数是可以从字面上表示的另一种类型。它们有两个部分,“实部”和“虚部”。 3+.5i就是这样一个数字,你可以在 Raku 中使用它。

通常,所有语言在处理来自其他计算机的消息的内容时,通常也需要使用十六进制,八进制或二进制的数字;前两种通常在颜色描述中找到,而后者是基本的计算机语言,因此可以在许多操作中找到。使用 0,后跟首字母:0b 在二进制数,0x 十六进制数和 0o 八进制数之前。因此,0b1111、0xF 和 0o17 代表相同的数字,即我们众所周知的15。

按照惯例,十六进制数字使用大写字母;但是,前缀 0xf 与所示版本一样有效。

除了以最常用的基数表示数字(也称为 radices,基数为复数)的壮举外,几乎所有其他语言都共享这种东西,Raku 还能够使用:number 前缀和之间的数字来表示任何基数。方括号。使用此表示法时,:3<120> 再次为15,而 :17<f> 相同。由于缺少(拉丁)字符来表示它们,因此只能以36为底数,即 :36<zz> 为 1295。

但是 Raku 中的数字不仅限于西方数字。如前所述,该语言完全支持 Unicode,尽管我暂时不会完全解释这意味着什么,但这意味着任何脚本中的数字也将表现为数字。你也可以尝试一些数字:

  • 罗马文字:ↂ(实际上是我第一次看到此符号)等于 100,000,除了(可能从时钟等得知)I,V,X。但是,你不能只需按照你在时钟中的方式将这些数字并置起来即可形成数字:XI是正确的(但只有一个代码点),而XI是不正确的。

  • 带圆圈的数字的行为也与不带圆圈的等效行为相同:⑱ 的行为与18的行为相同。存在各种各样的数字和数字,并带有多种突出显示,例如所谓的“数学粗体数字”。但是,与上面相同,并置将不起作用。

  • 也可以使用阿拉伯数字:例如٢代表两个。在这种情况下,并置将起作用:٢٥等于25。某些其他脚本(如高棉脚本)具有相同的质量:៥៨为58。通常,你将能够使用带有“数字”的代码点来形成数字。质量;并非所有数字都可以。例如,上面提到的那些并没有,这就是为什么并置无法形成数字的原因。罗马文字实际上不是位置系统,这解释了为什么它也不起作用。总共有33个字母,其中包括数字,其中包括藏文,缅甸和孟加拉文的字母表,以及不那么知名的其他字母表。

在所有这些情况下,仅使用“compose”键键入这些字素是不够的。你需要一个允许按名称搜索然后写这些数字的编辑器。大多数开发人员编辑器在其基本配置中或作为插件都包含此功能。而且,尽管你可能认为平时的数字足以满足你所有的计算需求,但你永远不知道何时找到一份工作,就需要用白话写数字。由于拉丁字母(实际上是阿拉伯语)数字有很多不同的版本,因此你可能希望将其用于装饰或强调。

Raku中提供了两个特殊数字,或者两个特殊的非数字:NaN(表示“不是数字”)和 Infinity(用 Inf 或实际符号 ∞ 表示)。 NaN与任何其他数字均不同,并且 ∞ 大于任何其他数字。为它们提供显式且字面的表示形式,使你可以将它们用作表达式中的默认值,也可以将其用作数学上正确的表达式的返回。

同样,Nil(当然不是数字,也不是其他所有东西)是 Nil,这是用这种语言应调用的未定义值的方式。你可以使用它来使变量的值无效,或表示没有任何值。 它不完全是文字,而是一个类,也可以直接键入。这样做是有原因的,但由于其功能与上述文字相似,因此在本节中进行了提及。

3.2.2. 枚举值

在某些情况下,可以采用有限且定义明确的值数量的对象集被分组为所谓的枚举值。 Raku 中有其中的一些,但是在本节中我将只介绍其中的两个。 你可以在 Raku 中这样编写它们,解释器将理解它们。

逻辑或布尔值是这些类型之一,并且仅接收两个值:布尔值是 true 或 false,因此可以简单地用 True 或 False 表示。 大写很重要。

比较的结果可以采用三个不同的值:对于两个事物,它们是相同的,第一个大于另一个,或者相反。 这些值用在称为 Order 的 Raku 枚举类型中,并用文字 Less,Same和 More 表示。

其他类型,例如文件的状态或承诺的状态,也使用枚举值,但是它们需要一些概念,稍后将在书中进行解释。

3.2.3. 字符串

字符串文字通常用引号引起来。 但是,本着 Unicode 支持的精神,可以通过多种方式进行引用。 "Hey" 与 «Hey» 一样有效,并且与你的老式的 "Hey" 或 'Hey' 一样有效。 也可以使用其他类型的引号,例如德语(和日语)「a」或成对的印刷引号 "a"。 为了简单起见,我将使用 ",但是在 Raku 中可以使用任何一组单引号,双引号或成对引号来定义字符串文字。但是,单引号或双引号之间存在区别,这与它们的处理方式有关。 带有插值和转义字符(你将在后面看到)。

另外,本着 Unicode 支持的精神,你可以在字符串文字中使用任何代码点,例如 "12three","二重鉤括弧",甚至可以组合使用不同的脚本,例如"二重鉤括弧사과·"。 实际的引号需要以反斜杠 "This is a quote \"" 进行转义。 也可以使用所谓的转义字符。 请参阅表3-1。

转义字符

意思

\a

响铃。让系统发出定义的声音。

\b

退格。向前退一个字符。

\t

制表符。向前移动一个制表的空格(与系统有关,但是通常是四个空格)。

\n

新行符。移动到下一行。

\f

换页符。等价于移动到下一页,但实际意义取决于上下文

\r

回车符。移动到行的开头。

\e

用于转义代码,例如用于插入颜色。

附加的转义字符 \x,后跟数字,用于直接访问 Unicode 字符:"\x062b" 打印 ث,"\x231b" 打印 ⌛。

此外,以 \c 开头,后跟方括号的转义序列也可以用于创建代码点的文字序列,方法是:官方名称或数字表示形式,即数字文字。例如,"\c[0x1F600]" 打印 😀。你也可以键入 "\c[128512]",这是十进制等效项。实际上,你甚至可以使用它来连接多个代码点:"\c[128512,128514,128516,128518]" 打印 😀😂😄😆。此功能与 \x 的功能非常相似,但是 \c 通过允许对代码点或代码点的组合进行正式描述而走得更远,例如 "\c[TAMIL SYLLABLE VAA]",等效于 வா。

所有这些形式都可以组合为单个字符串文字:"\c[0x1F600]Hi\x062b" 具有三个代码点,并打印为 😀Hiث。

注意 Unicode 代码点名称是由 Unicode 联盟定义的,但是在许多情况下,你可以在编辑器中查找它们。

在简单引号和双引号如何处理上述转义序列方面存在差异。 虽然双引号的行为如图所示,简单的引号会让它们通过,"\x062b" 恰好表示该字符序列,而 \c 转义字符以相同的方式处理。 两者都解释转义字符,主要是具有句法含义的字符,例如引号自身。'Hi \' 转义字符在单引号和双引号中的用法相同。 如果要具有文字序列,则 Q[] 引用构造可以做到这一点。 行为上的差异显示在表3-2中。

Table 3. 表 3-2 转义字符详情

引号

转义字符

转义序列

单引号 , ‘

已处理

按原样

双引号 , “

已处理

已处理

字面引号, Q[ ]

按原样

按原样

3.2.4. 对儿

这些文字不是上面的“原子”,因为它们实际上是由几个文字组成的。但是,由于它们按字面意义创建了一个对象,在 Raku 中称为对,因此我们以与考虑复数字面量相同的方式来考虑它们。

键值对在 Raku 中也是文字。它们在许多函数中都使用,因此有一种方便的方法可以使用粗箭头符号来表示它们。键必须是字符串,并且值可以是任何值。到目前为止,你仅能看到字符串和数字文字,因此请使用它们。

"Today" => "6 pounds"
"Tomorrow" => 3/5

此外,如果键仅由字母,破折号(-),撇号('),下划线(_)和不带破折号的数字组成,则可以消除键中使用的引号:

Today-ain't-day_1 => "No it ain't"

有一种特殊的货币对叫做冒号对,因为它以冒号开头。 :so 表示一对键为 so 且值为 True 的对。冒号后必须跟与上述描述相同的字符串。所以 :Today-ain’t-day1 等同于 Today-ain’t-day1 ⇒ True。这对也称为副词对,因为它的形式与副词相同。

你如何表达相反的 False 值? 因为 ! 在大多数语言(包括 Raku)中等效于否定,将其放在冒号和键之间将否定其值::!today-aint-day1 将其设置为 False 而不是 True。

这种文字不限于逻辑值。 使用数字也是表示一对数值为数字的缩写方式:例如 :42street。 这等效于键为 street 且值是42的一对。Raku通过查找数字的结尾和字母开头来进行解析,因此等效于 street ⇒ 42; 它也可以写成:street(42)

:42street === ("street" => 42)       # True
:street(42) === :42street            # True

3.3. 字符串?数字?同素异形体!

根据维基百科,同种异体是词素的一种变体形式。 但是,在 Raku 中,根据你的使用方式,它既可以用作数字也可以用作字符串。 实际上,它们是用特殊的 <> 引号引起来的数字。 <42> 是这样的同种异体,而 <33.5><55e55> 是编写它们的其他有效方式。 通常,任何用 <> 包围的有效文字数字都是有效的同种异体字。 你可以在数字上加上任意数量的空格,并且保留相同的结果。

3.4. 版本

版本是 Raku 中的重要人工制品,因为它们被用作类的元数据。 例如,通常可以知道所安装的版本是否早于要安装的版本。 这就是为什么(还有其他原因)它们得到自己的文字的原因,这些文字以小写字母 v 开头,后跟一个数字或一组数字(在任何脚本中),并可选地后面跟数字或由点分隔的字母。 它可能包括两个点之间的星号,但没有其他地方,并且带有加号作为后缀,表示它可能是该版本或更新的版本。 以下所有都是有效的版本字符串:

v0.0.1 v0.0.2.rc2.v3.this.is.the.good.one v2019.*
v2018.1+
v3.π
v៥ # KHMER DIGIT FIVE

然而这些是不合法的:

vπ.3
v8.3 # Includes an Unicode 8. character vVII # Roman numerals are not digits.

3.5. 结束语

理解语言如何获取数据对于建立数据处理和最终处理至关重要。 在本章中,你学习了如何键入数字和字符串数据,以及 Unicode 如何参与其中。

在下一章中,你将学习如何处理数据。

4. 表达式和运算符

理解简单的表达式以及怎样创建表达式

一旦知道如何在 Raku 中输入基本数据,接下来的自然步骤就是用它们构建表达式。 这是你将在本章中学到的内容,与上一章一样,可以在 REPL 中键入代码和表达式,如果没有错误,结果将打印在下一行,如下所示。

%raku
To exit type 'exit' or '^D'
> -38_000_000
-38000000
>

4.1. 基本表达式

基本表达式是运算符和字面值的组合。 大多数语言在其操作数中间使用运算符,例如 3+2。 它们称为中缀运算符。 在许多情况下,也使用前缀运算符(如-2)。

Raku 添加了许多新的运算符:

  • 后缀运算符在其操作数后面。

  • 环缀运算符放在其操作数括号样式的前面和后面。

  • 后环缀运算符在其操作数的后面(即后部)和周围(外接部分)。

你将在本章中探讨的最常见的运算符是中缀运算符,前缀运算符和后缀运算符。

由于 Raku 充分利用了 Unicode,因此在许多情况下,你将能够以与数学或物理学相同的方式来使用运算符。 他们超越了我们每天使用的常规,普通香草运算符。 这就是为什么方便(如果你还没有)配置系统以键入通用 Unicode 字符(包括或不包含)的原因。

在本章的这一部分,我们将 Raku 命令行(本地或在线)用作简单的计算器。 输入表达式(在示例中,或者你可能想到的任何变体中),按 Enter 键,REPL 将执行"E"操作并打印结果。

4.2. 算术

这些表达式涉及数字。 尽管可以使用任何数字,但为了与本书的编写语言保持一致,我将使用由十进制数字组成的常规数字。

让我们从最简单的前缀运算符开始:–号将正数转换为负数,如下所示:

-3/5
-7
-33.5
-38_000_000

所有这些数字都是有效的负有理数,整数,浮点数或大数。 下划线仅用于可视分组。 让我们回顾一下我在上一章中定义的同种异体概念。 在这些情况下,它们表现为数字,因此-<42>与42相同。 通常,算术运算符位于其操作数的中间:它们是中缀运算符。 如果有数字,它们将起作用:

3+7
2-2/3
1/3*2/3
3.234/3

所有这些数字都产生一个值,该值属于两者中最复杂的一种,在大多数情况下,这是一个有理数。

Raku 默认使用理性的事实意味着,与许多其他语言不同,像0.3 + 0.6-0.9这样的表达式恰好为0,而Python返回-1.1102230246251565e-16。

默认情况下,大多数数字都使用有理数,除非分子或分母太大。

大来说,我的意思是分子或分母小于1,后跟19个零。 你可能不必为此担心。

Raku 使用幂运算的方式也不常见。 x ∗ y是通常的x升到y的幂,但是你也可以使用Unicode的幂(无双关)来写322,意味着3升到22的幂(即31381059609)。 Raku 确实可以使用多种其他运算符,这些运算符是特定的或使用特定的符号。 它们如表4-1所示。

运算符

用途

div

向下整除。3 div 2 结果为 1, 但是 3 div -2 生成 -2。

%

取模或求余运算符; 整除的余数。x/y 等价于 x div y 加上 (x%y)/y。适用于任何种类的数字。

%%

整除。如果 a %% b 为零则返回真。

mod

整数取模或求余运算符,和 % 一样, 但 mod 左右两边的两个元素必须都是整数。

gcd

求最大公约数。33 gcd 121 返回 11。把它们的操作数转换为整数。100/3 gcd 121.9 返回同样的结果, 通过把 100/3 向下取整为 33, 121.9 向下取整为 121。

lcm

求最小公倍数。也是向下取整为最近的整数。例如 2018 lcm 2019 返回 4074342。

比较运算符可能应该拥有自己的段落。 首先,我们来谈谈相等与不相等。 数值相等使用 ==; 比较操作数的值,而不是字面值表示,因此 3.0 == 9/3 实际上是 True。

与许多其他语言一样,Raku 使用 == 将其与赋值运算符 = 区分开。 如果两边都是字面值,错误地将赋值运算符用作相等运算符会产生错误。 在其他情况下则不会,因此最好小心。

还有另一个原始运算符,用于检查近似相等性。

如果两个操作数的公差等于 1e-15,则 ≅ 或等价的 =~= 返回 True。 这是默认值,可以根据需要将其更改为较小(或较大)的值。 因此,

1 =~= 1-1/1e16

返回 True, 因为 1 近似等于 0.9999999999999999。

数字不等式有三种不同形式,所有形式都完全相同: ≠ 及其 ASCII 等效项 != 和 !==。

如果数字大于或小于布尔值,则它们返回。 Raku 使用小于(<)和大于(>),还有小于等于 ≤ 以及大于等于(≥)。

这是你使用的是使用 Unicode 代码点编写的运算符的第一种情况。 Raku 承认并非每个人都会配置自己的键盘或操作系统来输入它们。 这就是每个 Unicode 术语或运算符都有其 ASCII 等效项的原因。

ASCII是美国信息交换标准代码,它是Unicode的前身,仅包含 拉丁字母和一些常用符号。 它们是可以从键盘轻松键入的符号。 在本书和 Raku 文档中,ASCII基本上意味着使用"普通而简单的"字母和其他字符。 这可能就是为什么它们被称为Texas版本的原因,也可能是因为"Texas 中的一切都更大",并且它们需要多个字符(而不是Unicode的单个代码点),因此它们看起来要比同等的字符大。 无论如何,在2017年9月,所有对Texas的引用都更改为ASCII。 你可能仍会在较早的文章中找到它,例如这篇文章:www.learningperl6.com/2016/11/22/quick-tip-7-texas-and-unicode-things/

在这种情况下,≥ 等价于 >= 而 ≤ 等价于 ⇐。

等效地,!= 或 ≠ 表示不相等。 如果操作数大于另一个,则为 True。

由于可以返回数字,因此还有另一个可用于对数字进行排序的运算符:<⇒ 根据其操作数的数值返回 Less,Same 或 More。 3 <⇒ 4 返回 Less,而 3.0 <⇒ 9/3 返回 Same。

Raku 有两个原始运算符:min 和 max 分别返回参数的最小值和最大值。

所有这些算术运算符都将适用于你对数字(或数字类型,这将在本书的后面部分中提到)的通用概念。 尝试对非数字使用它们可能会失败。 但是,有两个警告。

第一个是, 枚举类型在底层只是数字。

42 + True

生成 43, 因为这是它所得到的值。

33 + Same

仍然是 33, 因为 Same 的值为 0; Less 的值为 -1, 而 More 的值为 +1; True 的值为 0。

第二个需要注意的是上面提到的所有同质异形的实际上是字符串脚手架:

333 + < 222 >

的值为 555, 因为同种异体在数字上下文中表现为数字。 实际上,算术运算符将数据放在一个上下文中尝试转换为数值。

这是上下文的一个示例,你将在以后看到(更多)上下文。 这种转换或强制转换将数据从一种类型转换为另一种类型。 例如,

40 + "2"

生成 42, 底层发生了如下事情:

  • + 充当上下文化器,表示"这里的东西将被理解为好像是一个数字。"

  • +"2" 将字符串(实际上包含数字)放在数字上下文中,该字符串将字符串强制为数字,使其行为与 +2 完全相同。

通常,所有算术运算符都在适当的条件下充当上下文化器。 这使 Raku 在处理其字面表示可能有所不同的数据时非常灵活。

4.3. 字符串

在 Raku 中你可以使用字符串做很多事情。最简单的就是通过 ~ 操作符将它们拼接在一块,所以

"Let's go" ~ " to the mall"

从两个字符串中创建一个新的字符串,中间有一个空格。如果两个字符串之一或者两个都是数字,那么拼接也有效,所以

33 ~ "p"

为 "33p"。这是强转的另一个例子。这儿的 ~ 用作字符串上下文器,使与它相连的任何东西成为字符串;~3 实际上是字符串 "3"。

如果你想重复同一个字符串, 使用 x。所以

"Let's go now" x 3

创建一个非常迫切的请愿书。

Raku 中还有相等和不相等运算符。它们返回 True 或 False。查看表 4-2.

运算符

意义

Lt

小于

Le

小于等于

Gt

大于

Ge

大于等于

它们严格是字符串运算符。 但是,它们对其他类型的字面值也有效,例如数字。 它们不会(可能)在这里起作用:它们会转换为字符串,然后进行比较。 下面这些比较都是正确的:

3 le 4
330 le 4

你还可以用特定运算符比较字符串。实际上,有三个用于比较字符串的运算符:leg,coll 和 unicmp。你可能必须回到 Unicode 一节中以了解下面要讲的东西。不用担心我会在这里等你。

与其他两个不同,leg(代表小于,相等或大于)使用字典顺序。它查看字符串的前两个字母的 Unicode(数字),并根据比较方式返回 More,Less 或 Same。 "\x12345" leg "\x12346" 始终返回 Less。这被称为字典顺序,因为它精确地使用了字符在表示字符的代码中的放置顺序。

对于大多数目的,这两个运算符的行为完全相同。实际上,默认情况下它们的工作方式相同,默认情况下,"a" coll "Z" 和 "a" unicmp "Z" 返回相同的值。但是,主要区别在于你可以使用运行时配置选项来更改 unicmp 的行为。我稍后再讲。

这三个运算符中的任何一个都将字符串的长度考虑在内。如果它们共享除最后一个字符以外的所有字符,则字符数较少的字符串总是被认为少于字符数更多的字符串。 "a" leg "aa","a" coll "aa" 和 "a" unicmp "aa" 始终为 Less,与右侧字符串中的第二个字母(或多个字母)无关。

本节中的所有运算符都将同素异形视为数字。 <3> leg <3.0> 返回 Less,因为它在字典上更小,与 <40> leg <5> 相同。其他两个运算符仅查看找到其数字(即字母)的顺序。如果与数字一起使用,它们还是会失败。

但是,在许多情况下,你将需要比较两件事,而无需事先知道它们属于哪种类型。你将在下一节中看到如何处理。

4.4. 处理语法错误

如果你已经在 REPL 中键入了上面建议的某些表达式(可能已经输入),则可能是你输入时出错了。 如果不是这种情况,则说明你是一个谨慎的人,但是迟早会在你身上发生这种情况,可能是由于大意乱写,或者仅仅是因为你试图写一些虽然可以工作的东西,但是该语言无法如此理解 。 例如,你可能一直在尝试执行图 4-2 中所示的操作。

这是一种语法错误,因为语句的组合方式存在问题。该错误是由于如下事实引起的:如上所述,+将" p"放在数字上下文中,然后解释器尝试将字符串的内容添加到数字中。它没有这样做,并且为你提供了一些有趣的信息:

  • 它告诉你它要做什么:" …​将字符串转换为数字。"这很有用,因为你知道失败的原因。

  • 它还给你一些提示,说明失败的原因。它试图将字符串强制转换为数字,但不能这样做,因为它不是以数字或句点开头(对于浮点数)。

  • 此外,它还能准确告诉你失败的地方:the和红色字符会告诉你无法解决的地方。正是在字符串中寻找数字或点的那一刻,决定无法完成。

  • 最后,它告诉你在以下情况下失败的地方

它正在评估的整个区块。在这种情况下,只有一行,所以知道它发生在第一行并不是那么令人启发,但是它对于较大的文件(如你将在本书后面看到的文件)有帮助。

通常,语法错误和其他类型的解释器/编译器错误会导致错误的报告,因为它们表明你(用户)失败了 某种程度上来说。不好的代表是由错误的错误消息引起的,以及它们在字面上被称为"错误"或"故障"的事实。在大多数情况下,它们是误解,例如在编译器或解释器中无法理解你的真实意思,这就是Raku 的误解消息的方法。他们就像"我试图按照你的意思做,而我尝试了这种方式。在某些情况下,他们甚至会建议你尝试其他操作。 这是Raku 的优点之一。仔细阅读并理解错误消息将使你走很长一段路。

4.5. 智能比较运算符

某些运算符不要求两个操作数都属于同一类型,而是会尝试针对"大"的定义检查一个操作数是否大于另一个操作数。 cmp 运算符是一个比较运算符,可处理任何类型的操作数。

33 cmp 55      # Less
"330" cmp "55" # Less
"a" cmp 55     # More, number is converted to string
"a" cmp <55>   # Same result, allomorph → string
33 cmp <55>    # Less, the allomorph → number

cmp 甚至可以比较对儿:

(a => 33) cmp (b => 33) # Less
(a => 44) cmp (a => 33) # More

它比较键,如果键相同,则比较值。如上所述。

请注意,我已将对放在括号中。 这是由于运算符优先级所致,也就是说,解释器仅根据位置选择的运算符和操作数的分组。 操作数坚持优先级较高的运算符,先执行该运算,然后再继续执行优先级较低的其他运算符; 在这种情况下,cmp的优先级高于成对使用的粗箭头,这就是为什么要使用括号:防止键粘到运算符而不是箭头上的原因。

两个功能上等价的运算符处理不等式:之前和之后; 它们将转换或强制转换为通用格式,然后返回True或False。

"a" before "b"   # True, 字符串比较
3 before "33"    # True, 强转为数字
v3.2 before v2+  # False, 版本比较
2/3 after 0.6    # True, 0.66666... > 0.6

通常,要对一系列对象进行排序时,可以使用比较运算符。 不等式运算符用于简单表达式,例如当你要做出决定时。

4.6. 注释:文档的重要组成部分

你可能已经注意到,在上面的部分中,我使用 # 来注释该特定表达式的输出,并以内联方式进行。 实际上,这是Raku 中用于单行注释的格式,这些注释扩展到行尾。

# This is a comment. It ends right here.

注释是代码中非常重要的部分。 它们用于解释随附代码的意图。 它们是如此重要,以致于在Raku 中,与其他语言不同,它们本身就是代码。 Raku 搭配进入注释并检查其中的内容,以供解释器或代码阅读器使用。

在你继续使用该语言时,这将在以后变得更加重要。 目前,你无需将表达式的这一部分剪切并粘贴到你的解释器中; 从输出的效果来看,它们将被忽略。

4.7. 逻辑

逻辑表达式返回 True 或 False。 当必须做出决定时,它们就会出现在大多数程序中。 当某个函数要求一个布尔值或逻辑值时,它将被强制为一个值,但是你可以使用运算符来明确地这样做:

so True    # True
so 33      # True, since it's non-0
so 0       # False; null values are false
so 0.0     # False; ditto
so "True"  # True as any non-empty string
so ""      # False, empty string
so Nil     # False
so <0>     # False

通常,零或 null 值将返回 False; 非零,非空值返回 True。 如你所见,在这种情况下,同种异体会以数字方式解释。 运算符不是正好相反。 并非如此,如预期的那样,True 是错误的。

单字符运算符 和 ! 表现方式相同:

? 1   # True
!True # False

请注意,运算符和操作数之间的空格是可选的,将被忽略。 你可以不加选择地使用它们和更复杂的形式,尽管通常来说,对于有经验的程序员而言,最后一种方法是更可取的。 这与其他语言类似。

下一个逻辑运算符是 and,只有两个操作数都为 true 时才为 true;否则为 false。 在此之后,如果只是一个操作数,则 or 的结果为 True。 在计算表达式之前,将操作数强制转换为布尔值(使用so)。 他们还可以使用 &&,|| 目前,它们的行为方式相同。

4.8. 结束语

运算符与文字一起使你可以将解释器用作美化的计算器。 如果使用默认发行版,则可以使用向上箭头在以前的操作上运行,并轻松编辑和重做以前的操作。 但是,本章的重点是解释如何构建语句。 它们中的大多数都是通过简单的表达式构建的,在继续在复杂的结构化数据和链接的语句中使用它们之前,你必须了解它们的工作方式,对操作数进行何种准备以及输出。 接下来,你将使用结构化数据。

5. 构建数据

如何从简单数据构建复杂数据,以及如何使用它

到目前为止,你一直在处理简单数据。通常,你将需要使用具有某些内部结构的数据。你将在本章中处理。

在本章中,你将继续使用命令行。这些示例旨在以本地或其他某种形式键入 REPL(如第1章所述)。

在本章中,你将探索两个概念,它们将在稍后充实:类(或类型)和角色。类描述该类的对象(或实例)的行为方式以及它们包含的属性。这里我们将模糊地使用术语"类"和"类型",因为目前我们还没有提到任何面向对象的概念。 Raku 中的对象属于一个类,或属于某种类型。

此外,类型可以继承角色,也可以实现角色。你可以将角色视为部分类型,但实际上,这意味着它们将能够执行某些操作,而与它们的实际类型无关。对象将具有单个类或类型,但是它可以实现许多不同的角色。后面的章节将专门讨论这一点。让这仅仅是一项进步,使你可以更好地了解 Raku 中某些对象的行为。

5.1. 代码对象

函数式语言是指使函数和任何可调用的东西或可被称为第一等公民的语言,即以与其他任何数据结构相同的方式对待; 通常,如果可以使用函数代替任何其他数据结构而无需特殊语法,则它们是一等公民。 为此,你实际上需要将其实际放在任何专门针对他们的文字中。 这就是为什么它们就在本章的开头。

简单地说,函数是可以调用或调用的东西。 在 Raku 中,它们被称为 Callables。 Callable 是一段带有参数并产生一些结果的代码,这些结果要么返回值,要么返回其他效果,例如打印某些内容。

通常,代码对象在它们周围使用花括号:

{ 'LOL' }

这是可用的最简单的代码对象(可调用),称为块。 块包含各种代码,并返回评估其中的最后一个表达式的结果。 与任何类型的代码一样,可以使用括号将此块应用于参数:

{ 'LOL' }(3) # Returns LOL

显然,这就像说明最简单代码的语法一样有用。 函数通常采用参数形式的某些输入,并返回对该输入执行某些操作的结果。 块在 $_ 中获得一个(单个)参数; 你在括号中输入的值将被替换为块内的 $_,如下所示:

{ "Hey " ~ $_}("you") # Hey you

我需要说一些有关 $_ 的事情,$_ 是你在本书中看到的第一个 Raku 变量。 稍后我将为他们整整一章。 首先,它的形状: Raku 使用信号标记来指示你要处理的变量类型。 $标记符用于标量(可以说 $calar吗?)内容,这是你在第3章中看到的内容。变量的实际名称或标识符是标记符后面的单个下划线(_)。

另一方面,$_ 是主题变量,也就是说,在大多数情况下,默认变量。 这意味着,如上面的块中所示,每当未指定用于值的容器时,它将进入该默认变量。 在上面的例子中,简单的块 {"Hey"〜 $_} 实际上没有定义其参数的名称,因此,如果将其传递一些值,它将转到主题变量。

但是,如所示,它是一个标量和简单变量。 如果使用两个参数会怎样?

{ "Hey " ~ $_ }("you", "and me")

你将收到一个错误:"Too many positionals passed; expected 0 or 1 arguments but got 2"。

定义的块足够聪明,可以知道有太多参数。 你可以使用默认的非标量变量,但这将留待以后使用,因此我将介绍 twigils:

{ "Hey " ~ $^first ~ " and " ~ $^second}("you", "me") # Hey you and me

Twigils 是跟在 sigils 之后的符号。 在这种情况下,twigil是插入符号(^),这是位置形式参数的默认值。 无论你如何称呼它们,按 Unicode 顺序排在首位的占位符变量都将排在第一位,第二排在第二位,依此类推。 你可以调用他们

{ "Hey " ~ $^fourth ~ " and " ~ $^second}("you", "me")

并仍然得到同样的结果,因为

"fourth" before "second"

为 True。参数被括号之间发现,用逗号分隔。

你可以为这些占位符使用的名称种类相同。你可以将其用作配对的密钥,如第3章所述; 也就是说,它们可以以字母或下划线开头,后跟字母,下划线,撇号,破折号和数字,但不能以破折号开头。 破折号和撇号都不能是名称中的最后一个字符。 $^a_1 有效,而 $^a'$^a-1 无效。 请记住这些规则,因为稍后你将重新使用它们。 我将在此类语法裸标识符之后调用字符串,因为你将在本书中经常使用它们。

说到对,这些位置参数不是你可以使用的唯一类型的参数。 命名参数实际上是一对,带有一个名称(或键)和一个值。 如果要使用它们,twigil 会变成冒号:

{"Hey " ~ $:greet ~ ": " ~ $:tell }( greet => 'you', tell => 'Well...' )
# Hey you: Well...

如果你想给实参赋予实名,或者只是简单地将事物更自然地表示为对,请使用它们,例如真值(在第3章中已经看到):

{"The answer is " ~ $:greet }( :!greet ) # The answer is False

作为一等公民,你必然会在许多不同的情况下找到障碍,因此有很多方法可以更简单地定义它们。 块 {"Hey " ~ $_ } 等效于 "Hey " ~ *,尽管要使用它,你仍需要将其用括号括起来:

("Hey " ~ *)("you") # Hey you

在 Raku 中,星号(*)实际上是一个名为 Whatever 的类。 在此上下文中,在操作中或作为函数的参数使用时,它将创建一个实际的对象。 当在操作中使用时,它将整个块变成 WhateverCode 的实例,实际上是一个块。 这样可以避免用户创建和命名实际上没有任何意义的占位符变量。 此外,你可以根据需要使用任意多个星号:

("Hey " ~ * ~ " and " ~ * ~ " and " ~ *)("you", "me", "anyone")
# Hey you and me and anyone

这三个参数无缝地转移到 Whatever 的三个实例。 可能会使未经训练的眼睛感到困惑,例如

( * * * ** * )(2,3,4) # 2 * ( 3 ** 4 ) == 162

但是, Raku 无需眨眼(cybernetic)就可以解释它。 请记住,花括号与占位符或主题变量结合使用,而无论在没有花括号的情况下都使用花括号(可以使用括号进行分组)。 作为他们的一等公民,你怎么会只使用字符串和数字作为参数呢? 你是对的。

{ $_("you")}( "Hey " ~ *w )

在这种情况下,你将还原为块的花括号版本。 让我们从右到左看: "Hey " ~ * 是函数的参数,用括号括起来,因为这是调用块或函数的方式。 让我们进一步向左移动:该参数将插入到主题变量 $_ 中。 你可以通过创建一个返回另一个函数的函数来走得更远,但是必须等到你阅读本章为止。

在容器上,你可以将内容分隔在不同的行和语句中。

5.2. 列表、数组、范围和序列

汇总数据的另一种方法是将它们简单地排列在一起,并以逗号分隔:

2, "33", { $^a + 3 }
# REPL will print:
# (2 33 -> $a { #`(Block|99905432) ... })

这是一个列表; 我使用大写字母,因为这是 Raku 中的一种。列表的元素可以是任何类型,当然包括其他列表,在这种情况下,你可以使用可选的括号将单个元素分组或放在尖括号中。 如果要列出不带任何引号的字符串和同种异形的列表:

(2, "33", <this is it>)
# (2 33 (this is it))

在 REPL 响应中,字符串引号被取消,但是嵌套的括号仍然指示它是列表中的列表。 对于第二个列表,请使用引号将结构 <>; 你已经看到了它在定义同种异义词方面的作用,但是在这种情况下,它允许你通过消除逗号和引号来简化列表的文字输入。 它将在每个类似单词的文字中创建一个列表元素。

分号还可以用于创建列表列表:

(2, "33" ; "this", "is", "it")
((2 33) (this is it))

方括号用于引用列表中的特定元素,从0开始:

(2, "33", <this is it>)[1]    # 33
(2, "33", <this is it>)[2][2] # it

可以在实现 Positional 角色的任何对象中使用索引。

可以通过使用方括号而不是括号来定义也可以起到位置作用的数组:

[2, "33", <this is it>] # [2 33 (this is it)]

实际上,数组是具有一系列功能的列表,其中之一是可变的:

( 2, 3)[1] = 0 # Error: Cannot modify an immutable List ((2 3))
[2, 3][1] = 0 # No problem.

目前,你可以将数组视为可变列表,但是稍后你会发现更多区别。 对于列表和数组,我将介绍一个新的 Raku 概念:副词,它们是运算符的修饰符,通常以后缀形式出现,即在运算符之后。 数组和列表使用副词来修改下标运算符 [] 的含义。

<con cien cañones por banda>[3]:exists #True

这些副词实际上在下标运算符上起作用; 因此,它们可以应用于任何位置数据结构。 副词列表如表5-1所示。

副词

意义

:exists

如果下标存在则返回 True。

:k

返回数组的下标或索引; 键的缩写,但数组使用索引或下标。 在散列中,它返回键。

:v

返回值

:kv

返回下标(或键)和值(以那个顺序)的列表。

:p

返回下标和值作为对儿。

:delete

删除下标位置的值(只在数组中)。

副词可以否定,但仅在第一种情况下才有意义。 :!exists 仅在特定下标不存在时才为 True。

范围是代表序列中指定了极限的所有元素的对象。 他们使用两个点,建立一个范围,该范围从左边的一个到右边的一个:

'α'..'ω'
3.2..11.3
-3..11

范围的两个极端必须排序。 范围将是有效的,但如果不是这种情况,则包含0个元素。 你可以使用插入号来消除其中一种极端,或同时消除两种极端。 3 ^.. 5 将排除3,而 10 ..^ 11 将排除最后一个。 范围是 Positional 对象,但它们的一个不错的特性是它们可用于索引其他 Positional。 他们将提取与 Range 中的索引相对应的元素。

minmax 运算符也可以用于生成范围:

8 minmax 3 # Range 3..8

它将选择两者中的较大者作为最高极限,而选择较小者作为最低极限。

如果范围以0开头,则不需要两个句点:只需插入号即可。 ^3 等于 0..20 ..^ 3。 通常可以在列表或数组中以下标的形式找到此构造,其中使用 Ranges 提取切片:

<con cien cañones por banda>[^3]   # con cien cañones
<con cien cañones por banda>[1..3] # cien cañones por

下标列表也可以使用:

<con cien cañones por banda>[0,3]  # con por

并与范围相结合,但在这种情况下,范围将创建一个子列表:

<con cien cañones por banda>[0,3]  # con por

并与范围相结合,但在这种情况下,范围将创建一个子列表:

<con cien cañones por banda>[1..2,4] # (cien cañones) banda

片也使用 Whatever(*) 表示"列表的末尾:"

<con cien cañones por banda>[*-3] # cañones

列表,范围和任何索引值的任何组合显然都是可能的。

也可以用其他方式定义无限范围:

'a'..* # strings that go from 'z' to 'aa'
1..Inf
300..∞

它们都是在无限处结束(或不结束,取决于你对无限的概念)的序列。 由于它们显然无法存储在非无限计算机中,因此它们实际上是示例或所谓的惰性数据结构。 惰性数据结构是功能性概念,它们使你可以在抽象平面内工作并对其进行操作,而不会产生每个人的负担。 实际上,仅在需要时才计算惰性数据结构的组件。 如果你需要惰性数据结构的第n个组件,则可以调用它:

('a'..*)[301] # kp

直到被调用的所有元素(包括被调用的元素)都将被存储以备后用。

你可以对数字范围进行操作,包括无限范围:

(1..*) + 3 # 4..Inf
(1..3) - 3 # -2..0

表 5-2 显示了范围可用的操作符。

操作符

操作数类型, 左和右

+

Range, Number

+

Number, Range

-

Range, Number

*

Range, Number

*

Number, Range

/

Range, Numbe

其余的数字运算符不能直接使用。 当 Range 使用其他类型的标量时,其他非数字运算符也是如此。

序列可以看作是更强大的范围,是非常有趣的对象。 你可以将它们视为列表,其中存在一些规则,这些规则会基于前一个规则生成元素。 他们使用省略号(…​)或省略号(…​)。

33...1    # 33 to 1, counting down
1/3...9   # 0.333 to 8.333, counting up by one
'q'...'a' # reverse alphabet, from the q to the a

序列和范围之间存在一些差异。

首先是上面的:在范围中,极端总是有序的; 在序列中,它们可以按任何顺序排列。 'q'..'a' 作为范围,将包含0个元素。

但是另一个是序列实际上是智能对象,只要它是一种算术运算,即每个元素都是前一个元素加上固定数量的序列,它们就可以从第一个元素推断出什么是实际序列。 或几何顺序,其中元素是前一个元素乘以固定量的序列:

(1,3.5...*)[^5]     # (1 3.5 6 8.5 11)
(2,4,8...*)[10..12] # (2048 4096 8192)

默认情况下假定算术级数; 你需要三个进度项,如第二个示例中所示,以使 Raku 理解这是几何级数。 但是,等等,还有更多!

(1,1, (* + *) mod 11 ... *)[32..42] # (2 3 5 8 2 10 1 0 1 1 2)

这是斐波那契模n序列的示例。这是一个周期性序列,其中模式在一个数字(与n相关)之后开始重播。如前所述,它的核心是类似脸的(* + *),即两个 * 的和,每个都称为 Whatever。如本章第一节所述,使用 Whatevers 的表达式将变为 WhateverCode,这是一个函数,在这种情况下,该函数使用 Whatever 表示序列中的先前元素。你可以根据需要使用任意数量的"任意",其中每个都以相反的顺序表示序列中的先前元素;也就是说,第一个*将是元素n-1,第二个*将是元素n-2。这样,你可以创建复杂的无限序列,这些序列可以应用在许多不同的用例中,例如 发送数据包的重试时间,或者只是在处理数学对象时重试的时间。

并非所有序列都必须是无限的。你可以将任何表达式用作…​的右侧以指示其应在何处结束,如下所示:

(2,4,8 ... * > 256 ) # (2 4 8 16 32 64 128 256 512)

在这种情况下,你可以使用WhateverCode(表达式)将其表示为序列中的数字大于256时,应立即完成。

5.3. 散列和映射

哈希和映射是具有关联角色的类的两个示例。 这意味着它们由键值对组成,要访问该值,你必须具有键。 映射和哈希之间的主要区别与列表和数组之间的相同:映射是不可变的,而哈希不是。 散列使用%()或花括号声明:

%( Óðinn => "Gungnir", Þor => "Mjolnir" )
{Óðinn => Gungnir, Þor => Mjolnir}

map 没有特定的语法,因此你将在散列上使用方法调用来创建地图。 所以

%( Óðinn => "Gungnir", Þor => "Mjolnir" ).Map

是等效的(不可变的)映射。 map 和散列也使用花括号来建立索引:

%( Óðinn => "Gungnir", Þor => "Mjolnir" ).Map.{'Þor'} # Mjolnir

该键必须加引号,但是你可以使用角引号消除遵循裸标识符语法的键的引号:

{Óðinn => "Gungnir", Þor => "Mjolnir"}<Óðinn> # Gungnir

在散列的情况下,这也可以用于更改值:

{Óðinn => "Gungnir", Þor => "Mjolnir"}<Óðinn> = "Fenrir" # will change value

做同样与 map 将导致错误。

从现在开始,我将仅使用哈希作为示例,并理解在读取值时,映射将以相同的方式运行。

以与处理数组和列表相同的方式,可以使用键列表对哈希进行切片:

%(Freya => "Hildisvini", Odin => "Hugin", Thor => "Tanngniost")<Freya Thor>
# (Hildisvini Tanngniost)

如前所述,<> 创建一个列表,其中的单词为元素; 与返回的那些键相对应的两个值。 表5-1中提到的副词也可以在这些关联中使用。 在这种情况下,所有 :k:kv 都将返回键,而不是索引或下标:

{a => 3}<a>:k # a

还有, :v 能用于提取切片中所有存在的值:

say %(Freya => "Hildisvini", Odin => "Hugin",
Thor => "Tanngniost"){"Freya","Buddy","Thor"}:v
# (Hildisvini Tanngniost)

:v 副词将消除不存在的元素,在结果列表中仅保留现有值; :v 可以以相同的方式用于 Positionals。

5.3.1. 集合、包和混合

集合是顺序不重要的唯一对象组。袋子就像套装,除了可以重复的物品。在混合中,对象可以有部分参与。它们都是不可变的,并且与哈希后缀等效,后者是可变的。从现在开始,我们将在不可变版本上使用只读操作,或将操作应用于成对。 集合看起来就像你可能只想在数学作业中使用的那种东西。当然,它们可以用于此目的,但这并不是唯一的使用方法。你是否曾经需要找出一组对象中的哪些元素以及另一组对象中的哪些元素?

那是十字路口。或相反,你需要找到一组中的元素,而另一组中没有。这是一个固定的差异。你可以使用另一种数据类型(例如列表)或其他操作来找出这一点,但是 Raku 允许仅使用一个运算符就可以非常有表现力地做到这一点。 定义集合包括要定义的特定类型(以小写形式),然后是列表或哈希,具体取决于特定类型:

set <Þor Oðinn Freya>            # set(Freya Oðinn Þor)
bag <spam spam egg spam>         # Bag(egg, spam(3))
mix { spam => 0.75, egg => 0.25} # Mix(egg => 0.25, spam => 0.75)

组合和袋子采用清单,而混合物采用散列或成对。 表5-3中列出了这些集可以执行的操作。 运算符的所有斜线版本都会否定该属性。

运算符

意义

作用于

∈, ∉, (elem)

属于(或不属于)

Sets 和 bags

∋, ∌, (cont)

包含

Sets 和 bags

⊆,⊈, (⇐)

子集或等于

Sets 和 bags

⊂,⊄, (<)

真子集

Sets 和 bags

⊇,⊉, (>=)

超集或等于

Sets 和 bags

⊃, ⊅, (>)

严格超集

Sets 和 bags

∩, (&)

交集

All

∖, (-)

差集

Sets 和 bags

⊖, (^)

对称差集

Sets 和 bags

∪, (

)

并集

All

⊍,(.)

包相乘

Sets 和 bags。结果为 Bag

⊎,(+)

包相加

这些最后的宽松操作会从成组的袋子中创建袋子,并像对待每个单独元素都是对象一样处理集合,将它们添加或"相乘"。 实际上,它可以获取常规列表并输出一个包:

<eggs spam> (+) <eggs spam bacon>
# Bag(bacon, eggs(2), spam(2))

在所有这些操作中,你都可以使用空集 ∅。 它将表现为没有元素的集合。 与其相乘或相交将得出一个空的适当类型的集合:

<eggs spam> (.) ∅ # Bag()

在这种情况下,你会得到一个空 Bag,这是那个空列表遵循"袋"的意思。

5.4. 其它 数据结构

 Raku 还可以以本机方式处理日期和时间。 有几种基本的数据结构可以执行此操作:DateTime,Duration 和 Instant。
你需要两条信息来表示日期和时间:第一,时间本身,第二,该日期和时间发生的时区。 它们共同构成了日期,可以用几种标准格式之一编写日期。 最受欢迎的是这样的:
2019-02-05T19:13:38.372152Z

这是我正在写的实际日期,它包括第一部分(在T之前),日期格式为 YYYY-MM-DD,后跟时间,格式为 hh:mm:ss.fraction。

Z表示"zulu时间"或UTC(通用时间坐标)时间; 这实际上不是我的时区,更像是

2019-02-05T20:17:39.487370+01:00

它在字符串的末尾并以 +/- 符号表示,表示马德里时区与 UTC 之间的时差。 无论如何,这些是语言通常会理解的标准格式。 即时实际上是用来表示特定的时间点。 你现在可以通过使用该函数来获取实际的 Instant:

now # Instant:1549394401.406088

你可以通过创建一个 InstantTime 来将其转换为 DateTime:

DateTime(now) #2019-02-05T19:20:41.927543Z

持续时间用于表示时间长度(以秒为单位)。 两个 Instants 和两个 DateTimes 之间的差异将始终返回 Duration:

now - now # -0.0014179, what it takes to process the statements.

通过添加或减去将被解释为秒的整数,持续时间可用于计算过去或将来的日期:

DateTime(now + 3600) # 2019-02-05T20:24:35.224185Z

你可以从"持续时间"中添加或减去实数,还可以对数字取模,以查找例如从"即时0"以来经过的小时或天的分数:

now % 3600 # 1841.894321680069 (seconds from the full hour)

其他经常与输入/输出操作结合使用的数据结构是缓冲区,在 Raku 中称为 Buf。它们只是一系列二进制元素。 它们是可变的,每个元素都可以单独更改。

这也是创建对象的新方法。 Raku 是一种面向对象的语言,因此具有类(是对象的一种)和对象(是该类的特定实例)。 你将生成一个类的新对象; 许多语言也为此使用了"新"一词。 因此,将通过以下方式从数字列表中生成一个新缓冲区

Buf.new( 42, 15, 33) # Buf:0x<2A 0F 21>

Bug的元素在显示时会转换为十六进制。 本质上,如果要使用二进制数据,则要使用错误,但它们也是位置错误的,因此可以将其编入索引:

Buf:0x<2A 0F 21>[2] # 33

从某种意义上讲,它们也是字符串的二进制表示。 但是与它们一起工作将需要你在下一章中看到的内容。

5.5. 结束语

你已经了解了如何创建数据并从中访问数据的一部分。 替换。 这与你稍后在构建语句时将使用的表示形式相同,因此如果你保留可以 处理。 数据可以是可变的,也可以是不可变的,有时根据其名称可以使用不同的名称。 如果你可以按位置访问部分数据,则称为位置数据;如果可以通过键访问,则称为关联数据。 各种函数都是可调用的。 使用哈希和数组的组合,你可以创建尽可能复杂的结构,并包含任何类型的数据。 但是,还有其他类型可用,用于特殊的信息,例如日期或数据的二进制blob。 显然,到目前为止,你只能使用裸数据结构进行操作。 你需要将它们存储在某个地方。 这是下一章的内容。

6. 处理复杂数据结构

如何以多种不同方式转换数据

编程涉及以多种不同的方式处理数据,更改和合成信息。 而且函数直接应用于数据,这就是为什么我们暂时不以任何方式存储数据的原因。

Raku 也是面向对象的和函数式的。 第一个意思是一种方法将应用于数据,并且只需将一个句点附加到数据结构(为方便起见,可以用括号括起来)并将方法名放在后面,就像这样:

%( Óðinn => "Gungnir", Þor => "Mjolnir" ).Map

你所做的就是调用 %( Óðinn ⇒ "Gungnir", Þor ⇒ "Mjolnir" ) 散列的 Map 方法,它是一个对象;这把原始 Hash 转换为 Map。

让我们看看转换函数如何访问数据结构的内部,以便 Raku 可以转换数据。 但是首先,你需要知道如何打印出值。

6.1. 不同的打印方式

打印东西的最简单方式是使用 say

say 33; # outputs 33

但是请记住,在 Raku 中一切都是对象。这等价于:

33.say # outputs 33

请注意,say 在每行末尾添加回车符。 如果你需要输出几个东西,你需要将它们放在圆括号中:

say( 33, 44, "Hey") # 3344Hey

如果你想将几个东西串在一起,可以使用字符串连接操作符:

say( 33 ~ 44 ~ "Hey") # 3344Hey

但是可以插值主题变量(你将在接下来看到),就此而言,可以插值任何变量,例如你将在下一章中处理的变量,只需将其插入变量中,然后 say 将打印出值:

$_ = 33; say("The value is $_") # The value is 33

你正在使用上面的 = 向主题变量显式地分配一个值。 这不是使用它的常用方法。 请在本章后面的内容中对此进行更全面的解释。

如果未显式显示任何对象,则大多数函数会将主题变量用作要应用的默认对象。 因此

$_ = 33; .say # 33

$_ = 33; $_.say # 33

是一样的。

你将需要在整个地方插入 say 来查找结果本章将要进行的处理。 请特别注意最后一个示例,该示例将在本章中使用。

6.2. 主题变量、它们的类型以及如何生成主题变量

你已经看到,在处理代码块时,变量 $_ 是如何神奇地显示以包含作为参数传递给该块的数据的。 我们称其为主题变量,在本例中为主题标量变量。 它与姐妹一起伴随着主题位置变量 @_,主题可调用变量 &_ 和主题关联变量 %_

可以说,用于插入参数的括号是 Topicalizer,因为它们会生成主题变量。 他们是你做这种事的第一个例子。 但是,它们将不是唯一的。 许多语句充当主题处理程序,将值放入主题变量。

given 语句只是一个简单的主题描述器,仅此而已。 它将它后面的任何表达式放入主题变量中,以便可以通过在 void 上调用方法来直接或间接使用它,如下所示:

.say given <away>           # away
given "Up" { say "You ♡" }  # You ♡
given { "bar" } { .().say } # bar

在第一种情况下,你是在 void 上调用 .say 方法,这等效于在主题变量 $_ 上调用它,该变量已从 given 中获取了值。 在最后一种情况下,你隐式使用主题 Callable 变量。 {"bar"} 创建一个块(返回" bar")并将其分配给 &_。 简单的句点(.)是方法调用运算符,当其后带有空括号时,它将在隐式主题变量中调用该函数。

当你要将主题设置为某个对象并对其执行一些操作时,主题化很有用,例如:

say "Array has ", .elems,
    " elements which add up to ", .sum given 1..33
# Array has 33 elements which add up to 561

given 语句将范围从1到33进行了主题化,并应用了两个操作:.elems(用于计数看起来像列表的任何元素的元素数)和 .sum(将它们相加)。 在这种情况下,使用的主题变量应为 @_。 但是,由于你是隐式使用它(在 void 上调用方法),因此并没有真正看到或注意到它。

虽然给出了一个纯主题化器,但执行了加法操作。 该语句可以用作后缀或前缀; 它计算后面的表达式是否已定义,并且仅在定义后才运行代码。 但这也主题化。

.say with <this song>                   # (this song)
with Any { .say } orwith "Hey" { .say } # Hey

在第一种情况下,定义了 <this song>,因此将运行语句(在这种情况下位于条件的前面)。

括号中的输出表明它正在打印一个列表。 如你所见,尖括号创建了列表,say 能够处理它们。

但是,在第二条语句中,Any 被认为已定义,这就是为什么它会运行到下一条语句。这是一种面向对象的语言,它具有相互继承的整个类层次结构。排名第二的是 Mu(Mu)。到目前为止,你所看到的所有类都将 Any 子类化,但这不是你用来创建对象的类。在这种情况下,你仅将其用作未定义的示例,因为 Raku 将其理解为未定义。定义了带有值的对象,即使该对象为空或 False。但是,类并没有通过定义来定义;它们不包含对象的具体实例。任何其他类名将产生相同的值。

本示例介绍了另一个有趣的语句 orwith,它是一种在条件不成立的情况下执行的 else(对于ifs)语句。你只能在 with 语句之后将其用作链中的第一条语句。 它还使用另一种形式,显示 Raku 如何允许语句–条件–表达式

  • say - with - <this song>

语法和

  • condition – expression - { statement }.

  • with - Any - { .say }

在后一种情况下,必须将函数显式显示为带有花括号的块。 唯一的区别是,你可以根据编码标准或可识别的可读性随意选择它们。 请记住,在 Raku 中,有多种方法可以做到这一点。

with 相反的说法。 当然,它的调用没有以下内容:

say "With or" without Mu # With or

Mu 是位于层次结构顶部的类。 这是最顶级的从其他所有类继承的地方。 但这无关紧要; 它只是一个类,因此未定义,因此无需在其左侧运行该语句。

andthen 运算符等效于 with,不同之处在于它仅用作后缀:

"Away from Toronto" andthen .say # Away from Toronto

如果未定义,其行为会略有不同。 它会返回一个空列表,而不是不运行代码或转到下一个无语句的地方,因此

Any andthen .say

不会显示任何信息,但它实际上是一个空列表。 事实上,空列表。更多关于这一点。

6.3. 基本对象方法和如何调用方法

Raku 类层次结构的最顶层是 Mu。 Mu 托管整个类层次结构中可用的方法。 你将找到的所有常规对象都是 Mu 的一个实例。 因此,有趣的是你可能需要使用一些方法。 它们在表6-1中。 有关整个列表,请参阅第2章中的“第一响应者”部分。

方法

描述

defined

如果是类型对象则返回 True, 如果是类则返回 False。

isa

如果对象属于类或子类则返回 True。

does

如果对象就是类则返回 True。

gist

返回对象的表示形式; 通过在对象上被调用 say。

perl

返回可以评估的再现对象的方式对象。

so

转换为对应于该对象值的布尔值。

not

如上述,但否定的值。

print

转换为字符串,并输出该值。

put

打印,并增加了在最后一个新行。

到目前为止,你已经在所有类的所有对象上使用了这些方法。

42.defined.say      # True
<1 2 3>.print       # 1 2 3
<1 2 3>.gist.print  # (1 2 3) (same as <1 2 3>.say)
0.so.put            # False
1.not.put           # False

如上所示,由于方法的应用程序还返回一个对象,因此该对象也可以应用于方法。 42.defined 返回一个布尔值,如每个示例所示,可以说或打印。 在 Raku 中,总是有很多方法可以做某事,并且其中一些方法也可以用作简单例程,位于参数之前,如下所示:

put not 1 # False, same as 1.not.put

除非有一些歧义,否则无需在函数调用中使用括号:

say not 1, 2            # False2

"," 被解释为将要打印的两个项分开,因此你必须使用

say(not((1, 2)))

代替。 由于不是作为例程使用单个参数,因此必须在 (1,2) 周围加上括号以使其成为单个对象。 第二组括号将 not 例程应用到该括号,最终将结果说出来。

确实有很多括号。 这就是为什么在大多数情况下,首选 object.method 语法的原因。

层次结构的下一个是 Any 类,顾名思义,该类几乎被 Raku 类层次结构中的任何类继承。

实际上,它的唯一同级是 Junction。 稍后对此进行更多讨论。

不打算实例化任何实例,但它包含子类使用的许多方法。 通常,这些方法假定那里存在一些内部结构。 表6-2仅列出其中一些,即使用最广泛的应用程序。

方法

描述

elems 

返回元素的数量

min

返回最小的元素(在数字意义上)

max

返回最大的一个

minmax

返回一个范围内的最小和最大bookended

keys

返回键,这是指数如果它的位置

values

返回值的列表,这相当于列表本身的清单,并在哈希值。

unique

返回的每一个元素单一时间

repeated

返回只出现一次的元素

squish

只返回其重复相同的一个序列的最后一个元素

由于这些方法属于 Any,因此它们可以被 Positional 和 Associative 以及简单标量使用(尽管其中大多数将返回平凡的值),如下所示:

("þ" xx 3, "ð" xx 4, "ß" xx 5).Mix.elems.say # 3
say 3.elems           # 1
say (5,7,-3,2).minmax # -3..7

你将在本章和本书的其余部分中使用这些功能,因此请记住这些功能。 通常,它们按照它们的意思进行操作,并且名称与其他语言中使用的名称相似(但 elems 除外,通常将其称为长度或大小; elem 和元素中的一样,内容更多)。

Raku 与极简语言相反。 创建它不是要记住,因为有许多不同的功能和选项。 只需大致了解你可以使用对象及其语法的方式,那么搜索引擎将是你的朋友,以掌握实际的表达方式。

6.4. 以函数式的方式处理数据

你已经检查了函数式编程的宗旨之一:作为一等公民。让我们来谈谈:使用函数而不是循环构造进行处理。事实上,这是有目的的,在本书中排在第一位。通常,使用函数来处理数据结构将更高效,并且更接近于你用数学表达你的意图。也就是说,如果要将函数 f 应用于对象 x(它是一个列表),则最好将其表示为 f(x)或 xf(以对象形式)而不是循环,后者表示首先从中获取第一个数字。列表 x,然后将 f 应用于第一个元素,然后应用于第二个,依此类推。习惯于进行功能性思考,你的程序将更具表现力,而且速度更快。

这就是为什么你将看到的第一个函数是将另一个函数应用于任何数据结构并称为 map 的原因。这不是你在 map 集中看到的那种映射,而是一个数学映射,即一个应用程序f,它以 a 作为输入,b 作为输出:f: a→b

say (1..5).map: ( 0x2134 + * ).chr # (א ב ג ד i)

这会将从0到5的列表映射到希伯来语字母的第一个字母。 你还在此处使用了一种将参数传递给函数的新方法:冒号用于将 map 与使用的参数分开。

在这种情况下,这完全等同于使用成对的括号,只是它可以保存字符并使其更整洁。 map 有另一种语法。   这是一种例行地图,将对象作为第二个参数,如下所示:

say map ( 0x2134 + * ).chr, 1..5 # (א ב ג ד i)

在这种情况下,括号也是可选的。 通常,对象方法的例程形式始终将对象作为第二个参数。

如上括号所示,map 返回一个 Sequence 作为输入,无论它是列表,Range(在这种情况下)还是其他序列。 但这是一件好事。 还记得无限序列吗? 它们也可以映射到无限远的地方:

say (1,1, * + * ... *).map: *2 # (...)

括号中的省略号是 Raku 所说的:“嗯,这东西很大。 让我们切一下。”

say ((1,1, * + * ... *).map: *2)[1000..^1005]

将打印大量的数字,它们是平方斐波纳契数列的第1000至1004个元素。 从它们的章节中可以看出,惰性序列仅在需要它们时才计算它们的元素,在这种情况下,它们就是这样做的。

其他一些功能也从一个域映射到另一个域,但是你想知道哪些元素满足条件。 与 Linux 命令行实用程序中的 grep 一样,它将为你提供帮助。 想知道斐波那契数列的第一个被3整除的元素吗?

say((1,1,*+*...*).grep:*%%3)[^5] # (321144987676)

grep 将仅返回将使表达为真; 其余的将被丢弃。 3和21显然可以被3整除,因此它们包含在输出中。 它们之间的其余部分将被跳过。

就像 map 一样,这也是一个 Sequence; 这意味着你可以链接 map 和 grep:

say (1,1, * + * ... *).grep( * %% 3).map( *2 )[10..14] # (491974210728665289 23112315624967704576 1085786860162753449801
# 51008870112024444436089 2396331108404986135046400)

你可以根据需要进行任意操作,将 a 映射到 b 并将其过滤到 c,然后再次返回到映射和过滤。 当然,如果有 map,那就是缩小。 MapReduce 是事物,是可用于大数据处理的框架的名称。 map 和 reduce 是通常在任何功能语言中都能找到的功能对。 请注意,reduce 通过将函数应用于前两个元素,然后将其递归应用于此和下一个元素的结果,从而将函数应用于整个列表,最终产生单个结果。 因此,它是一个二进制函数或带有两个参数的函数; 它可能是中缀运算符。 毫不奇怪,为此目的使用了一种称为 reduce 的方法。 例如,让我们计算斐波那契数列的第一项的乘积:

say (1,1, * + * ... * > 10000).reduce: * * * # 106099439760946345047026104938595829760000

这种表达方式带有大量星号。 除了一个以外的所有例子都是“无论如何”的例子。 那只是产品符号; 它是倒数第二个。 你创建了一个斐波那契数列,当一个项大于10000时该序列将停止,因此只有几个项。 你需要序列是有限的,因为 reduce 不会作用于惰性数据结构。

你正在应用的函数位于冒号后面,并且它是这样的(你可能已经猜到了):第一个星号是到目前为止减少的结果,第二个是乘积,第三个是当前值 元件。 此示例显示其工作方式:

say (1..10).reduce: * ~ "→" ~ * # 1→2→3→4→5→6→7→8→9→10

到目前为止,最左侧的“任意”将接收结果,而当前元素倒数第二。 你可能不希望遵循元素到达的顺序。 使用你在前几章中看到的占位符:

say (1..10).reduce: { $^b ~ "→" ~ $^a } # 10→9→8→7→6→5→4→3→2→1

就像在上一个示例中看到的那样,无论在适当的上下文中将表达式所处的内容都变成了代码,并用大括号将其神奇地包围起来。 这些占位符不是这种情况:仅当将它们放在花括号包围的代码块中时,它们才会被理解。 另一方面,他们按字母顺序接收参数。 第一个参数(到目前为止的结果)将按字母顺序进入第一个参数,在这种情况下为 $^a,依此类推。 因此,在这种情况下,序列被反转。

使用 reduce 操作的另一种方法是使用元运算符。 元运算符用方括号括起来的二进制中缀运算符表示它们实际上用作归约运算符,即应用于第一个和第二个,然后应用于结果和第三个,依此类推:

say [*] 1..13 # 6227020800

实际上是 13! 或阶乘13,是从13到1的所有数字的乘积。中缀运算符 *[] 包围,以将其转换为化简运算符。 这不限于数字:

say [~] <a b c d> # abcd

你可能需要了解部分结果,并与它们一起生成列表。 所谓的三角归约元运算符就是这样做的:

[\~] <a b c d> # (a ab abc abcd)

归约运算符总是返回序列,它们可以是惰性的(或无限的,取决于你如何看待它们)。 将它们应用于延迟序列将产生另一个延迟序列,这将在需要时立即准备好结果。 例如,

say ([\*] 1..*)[31] # 263130836933693530167218012160000000

这是一个很好的示例,说明了 Raku 如何为你节省了大量的代码,以及如何将其他语言中的复杂循环减少为一行。 但是也有循环。 所有功能操作都可以链接在一起,因为它们返回一个值:

say (1,1, * + * ... * > 10000).map( *2 ).grep( * %% 2 ).reduce (*+*)
# 126886032

你将map应用于该序列,该序列将返回另一个序列,向其应用 grep,仅过滤出偶数值,并最终缩减为该结果,从而得到结果。

但这可以使用进给或火箭运算符 ==> 以不同的方式表示,如下所示:

say (1,1, * + * ... * > 10000 ==> map( *2 ) ==>
grep( * %% 2 ) ==> reduce( * + * ) )

从语法上讲,该运算符的作用是在左侧获取操作数,并将其用作右侧例程的第二个参数。 请记住,所有这些功能都具有方法和例程形式。 因此,火箭操作员的第一个应用实际上等于

map( *2, (1,1, * + * ... * > 10000) )

这显然在视觉上不太清晰。 你可以将 .grep 应用于此,或者再次将其包装在对 grep 的常规类型调用中,依此类推。 在 Raku 中可以使用所有三种语法(方法,提要运算符,例程),因此你只需选择一种对你或你的同事而言更易读的语法。

6.4.1. 向量运算符和元运算符

多个运算符对列表执行操作,从而生成其他列表。 请参阅表6-3。

运算符

动作

X

向量乘法。 在左侧创建每个元素对与右侧每个元素对的列表。

Z

Zip 运算符。 创建一个成对的列表,其中元素在左侧,相应的元素在右侧。

他们可以在不同长度的列表进行操作:

say ^2 X 3..5 #((0 3) (0 4) (0 5) (1 3) (1 4) (1 5))

但 Z 将减少到最短长度:

say ^2 Z 3..5 # ((0 3) (1 4))

作为元运算符,它们非常方便。 只需将你要应用的二进制运算符附加到结果对中的每一对:

say 10..12 X** 3..5
# (1000 10000 100000 1331 14641 161051 1728 20736 248832)
<J Q K Ace> X~ <♣ ♦ ♥ ♠>
# (J♣ J♦ J♥ J♠ Q♣ Q♦ Q♥ Q♠ K♣ K♦ K♥ K♠ Ace♣ Ace♦ Ace♥ Ace♠)

在第一种情况下,**(提高到)是应用于对的运算符; 第二个是字符串连接运算符 ~。

6.5. 循环

在 Raku 中,循环控制结构是关键字,也就是说,它们纯粹是由语言解释的语法,而不是定义为对象系统一部分的函数。 这在处理它们的方式上提供了更大的灵活性 由于它们被明确和立即识别,因此在使用它们时会提高效率。 循环构造有很多不同,它们是否返回值以及它们的终止条件,以及它们如何生成下一个迭代。 我将在表6-4中进行总结。

关键字

主题

终止

下一个迭代

返回值

for

IterationEnd

Iteration.pull-one

loop

三元表达式中的条件

Increment in ternary

while

表达式

表达式

until

表达式

内部的

repeat

表达式

内部的

do

只运行一次

N/A

for 关键字是循环的主力,它用于迭代允许它的数据结构。 这些数据结构具有 Iterable 角色。 你知道的所有位置和关联数据结构都具有该角色,因此你可以使用它们。 但是这样做的目的是在该数据结构上调用 .iterator。 对于每次迭代,它将在生成的迭代器上调用 pull-one,并将其与常量 IterationEnd 进行比较以了解是否已完成。 通常,这对用户是透明的,因为 Raku 将负责为每个数据结构生成合理的值:

.say for %( :a, :b, :c ) # c => Trueb => Truea => True
.say for <a b c>         # abc»
.say for set <a b c>     # a => Trueb => Truec => True

在上述所有情况下,你都使用隐式主题变量。 你还可以使用 for 作为后缀,如果循环中只有一条语句,这将很方便,从而节省了花括号:

for set <a b c> { say $_.value??$_.key!!"" }; # «b a c »

显式主题变量 $_ 将为 Pair 类型。 你将只打印键,它是集合中的元素。 你可以在区块中执行此操作。 但是你可以显式声明一个变量。 该变量可以是你想要的任何值,具体取决于迭代器每转将返回的内容。 在这种情况下,矢量运算X的结果将是一个包含两个元素的列表:

for 1..3 X <A B C> -> @pair {
    say (@pair[0] == 1)??~@pair!! ~@pair ~ "s"
} # 1 A1 B1 C2 As2 Bs2 Cs3 As3 Bs3 Cs

在这种情况下,这很有用,因为你希望在代码中清楚地显示该事实。 但是请记住,在 Raku 中,总是有不止一种方法来执行某项操作,如果需要,可以使用隐式 @_ 进行简化。

由于 for 是返回值的循环结构之一,因此你可以在循环结束时保留打印内容,或者在循环前面使用单个 say 语句:

say (for 1..3 X <A B C> -> @pair { (@pair[0] == 1)??~@pair!! ~@ pair ~ "s" })
# (1 A 1 B 1 C 2 As 2 Bs 2 Cs 3 As 3 Bs 3 Cs)

你需要用括号括起来以捕获输出,该输出将是一个列表,如其周围的括号所示。 loop 语句还会产生结果,并且可以与 C 样式的三元组语句一起使用:一个用于变量声明,下一个用于检查终止,最后一个用于增加值:

say (loop (my $i = 3; $i < 200 ; $i*=3 ) { $i2 })
# (9 81 729 6561)

语句 do 几乎不是一个循环,因为它只是收集表达式的输出,以便可以将其分配给变量或简单地对其进行捕获。 它可以是在不允许它们的语句中捕获复杂表达式的一种方法。

while 和 repeat 语句的行为略有不同,主要是在检查终止条件方面:

$_ = 3; repeat { $_*=3; say $_2 } until $_ > 20;
$_ = 3; while $_ <= 20 { $_*=3; say $_2 }

这两个循环将打印81和729,并且将运行相同的次数。 但是,重复循环将至少迭代一次; 如果不满足条件,则 while 循环甚至可能不会运行一次。 作为条件,while 和 until 彼此相反,但是可以在相同的条件下使用:你可以使用 while 结束重复循环,或者使用直到直到满足条件的 iter 循环。

有一些命令仅在循环内部起作用,并且与可用的循环构造无关。 下一条命令将跳过其余的迭代,并开始下一个:

$_ = 1; until $_ > 200 { $_*=3; next if $_.comb.elems == 2; say $_2 }
#9
# 81
# 59049

此循环将跳过具有2个数字的3的所有倍数(字符串中-comb 的数字–elems–等于2),仅生成具有单个数字(例如3或9或3)的平方。 像243。

另一个这样的命令是 last,它将退出循环。 它可以与 loop 关键字一起使用,并且无需任何循环变量即可在需要时退出:

$_ = 3; loop { $_*=3; say $_2; last if $_ > 20 }
# 81
# 729

6.6. 决定

做出决定的最简单方法之一就是使用三元运算符。 此运算符检查表达式的值,如果为 true,则返回第一个结果,如果为 false,则返回第二个结果。 在大多数语言中,它使用 ?: 作为运算符,将表达式与 true-result 和 true 与 false 分开。 Raku 使用 ??!!(或它们的 Unicode,等效的单个字素 ?? 和 !!):

say (max 1,7 ... * > 30) < 34 ?? "Smaller" !! "Greater" # Smaller

你可能想知道该序列中第一个大于30的数字是否小于34。它恰好是31,因此小于31并且返回第一个结果。 但是在数百种计算机语言中,如果奉献为卓越的决策声明。 你可以在条件之前或之后以传统方式使用它,如下所示:

say "Smaller" if (max 1,7 ... * > 30) < 34 # Smaller

当然,你会错过另一部分,如果条件为假,那部分会做一些事情。 你已经看到 with 可以与 orwith 一起使用。 但这还需要另一个条件,因此让我们介绍一下其他著名的语句:

if (max 1,7 ... * > 30) < 34 { say "Smaller"} else { say "Greater"}
# Smaller

由于 Raku 使用花括号将块分开,因此你可以将所有内容放在一行中。 如果根据表达式的结果需要执行的操作超过返回单个值,则使用 if 代替三元运算符; if 也将作为一个块,返回一个值。 本示例将其与 elseif 结合使用,如果先前的表达式为 False,它将运行,但执行其他操作:

say (if M > 1000 { ">1k" } elsif M == 1000 { "1k" } else { "<1k" }) # 1k

你将整个块括在括号中以传递结果。

与 if 相反的是,除非,否则仅在表达式的结果为 false 时才运行该块(或返回块中的最后一个元素):

say "Not eight" unless 7 == VII # OUTPUT: «Not eight »

也可以使用 with 语句,但仅当你要测试定义性时才可以使用。 还有一种使用给定测试不同值的方法,我之前已经提到过:

given M cmp 1000 { when More {say "M"}; when Same {say "S"}; default {say "L"} } # S

给定的语句将表达式的结果以及测试值的主题(在隐式的主题变量中)进行主题化。 在这种情况下,它将打印"S",因为M是1,000的罗马数字。

6.7. 结束语

在本章中,你将重点放在采用数据结构产生结果的单行反应式函数上。 这是所有程序的基础,所有程序基本上都是转换数据结构的过程。 但是,使用单个数据结构而不将结果存储在任何地方只会使你走得很远。 你将需要使用数据容器,这将在下一章中解决。

7. 存储数据:容器

如何将数据存储到变量中,指定可以在哪里看到这些容器,以及如何更改或销毁它们 当学习一种新的编程语言的语法时,重要的是要分别理解每个发生的事情:如何编写不同的类型,可以使用哪些函数来处理它们以及如何在程序中进行决策。 从字面上看,这些动作是通过对每行的每个字符以及由这些字符创建的每条语句进行分析并采取行动来实现的。 到目前为止,你已经处理了可以单行编写的语句,无论是在REPL中还是在第1章中提到的一种在线服务中。从本章开始,你将使用可能会用到的完整脚本 想要使用编程编辑器进行编辑并存储在本地。 让我们看看完整的 Raku 脚本是什么样的:

given M cmp 1000 {
    when More {say "M"}
    when Same {say "S"}
    default {say "L"}
}

该文件的输出应为 "S",与上一章中看到的单行语句相同。关于上一章中显示的版本,有几处更改:

  • shebang行已添加到开头 文件。这行包括三个部分。第一个是 shebang自己,#!,它向 shell 表示后面是需要加载的程序以解释文件的其余部分。第二部分是 /usr/bin/env,它指示你应该寻找它,而不是硬连接解释器的位置 在环境中,也就是说,查看命令解释器变量会告诉你应该在哪里寻找可运行程序。之所以需要这样做,有几个原因,其中包括 rakudobrew 之类的版本管理器(在第1章中提到过)更改了 Raku 二进制文件的路径。紧随其后的 raku 表示解释程序的名称;该行等效于在命令行中运行哪个 raku ,即它将返回解释器的实际路径,尽管env也会加载它。 OSX用户仍然可以使用它,尽管它并未真正使用,但在Windows中也不会受到损害。该shebang行的目的是仅通过发布文件名即可运行脚本。

  • 包含 use v6 行。 这是一个编译指示,它是一条指令,告诉解释器如何处理文件的其余部分,但不是针对 Raku 解释器,而是针对 Perl 5,如果看到,它将发出错误,例如 这个:

Perl v6.0.0 required—​this is only v5.20.0, stopped at given.p6 line 3.

  • 将句子扩展到不同的行时,该行末尾的分号已删除。 Raku 总是以分号结束句子,除非行尾有花括号。 这就是为什么在这些情况下不需要它的原因。 你需要立即运行此文件,我将其称为 give.p6。 你可以在以下资源库中找到本章中的所有脚本: https://github.com/JJ/raku-quick-reference-apress 在适当的章节名称下的目录下,以及本书的Apress网站上。 如果正确安装了 raku ,则使用给定的 raku p6 可以在任何地方使用。 如果你使用的是 Windows,则也可以编写 named.p6。 如果你使用的是 Linux,BSD 或 OSx,则需要发出 chmod + x named.p6 以使其可执行,然后运行 ./given.p6。

因此,现在你可以创建和运行自己的程序。 程序在几个地方保持状态。 接下来让我们了解一下。

7.1. 容器存在于哪里: 作用域

首先,让我们看看我们是否在同一页面上。我将在任何地方谈论所谓的变量。但是,这是一个负载词,例如,它可能有所不同。在 Raku 中可能是这样,也可能不是,所以我更喜欢谈论容器。容器存储值并具有一系列其他属性。此外,该名称还使容器与其内容脱钩。变量更像是你在一条数据上打的标签或标签。容器具有实际的代理机构,并且可以存储数据,但是即使它们与任何数据都没有关联,也可以对它们执行一些其他操作。 容器的第一件事就是遵循裸标识符语法的名称,我在第3章中谈到对时提到了容器,在第5章中提到了带有twigils的占位符的名称时又提到过。 标识符还有扩展的语法。这将在本章后面解释。 提醒一下,他们必须以字母或下划线开头,并且可以 后面跟字母,下划线,撇号,破折号和数字(只要没有破折号,并且以字母,下划线或数字结尾,则为最后一个)。以下是所有有效的标识符名称:

first-class secondary chapter1 Chapter_2 cool it’s-here

在大多数情况下,容器名称的前面有一个符号,该符号与它们所包含的数据将具有的默认角色有关。 这些信息显示在表7-1中。

Sigil

默认规则

$

标量

@

位置

%

关联

&

可调用

因此,$chapter1 默认情况下将包含一个标量,@secondary 将包含具有位置角色的数据,例如列表或数组,而 %cool 将包含一个关联值。 当然,&first-class 将包含 Callable。 也可以使用无 Sigil 的容器,但让我们稍等一下。 当将一种数据类型分配给容器时,这可能意味着某种强制:

my $var = 3; say $var # 3
my @var = 3; say @var # [3]

相同的数据。然而,在第二个例子中,因为它被分配给一个 Positional,它被有效转换成一个数组。

my @var = :a; say @var # [a => True]
my %var = :a; say %var # {a => True}

同一对将成为包含一对的单元素数组,或者成为具有单个键的哈希,具体取决于容器使用的符号:

(my @var=1,2).say # [1 2]
(my %var=1,2).say # {1 => 2}

在这种情况下,分配给 @sigilled 或 %sigilled 变量的列表将成为一个由两个元素组成的数组或一个键为第一个元素的哈希。如预期的那样,对具有奇数个元素的列表进行相同的尝试将失败。另请参见这种工作方式:变量声明用括号括起来。关于 Raku ,你应该记住的重要一点是,这是一种奇怪的一致性语言。括号不仅仅是语法。他们创建了一个列表,该列表可以说出来,如下所示。但是,括号本身并不能创建范围。该变量在声明后即可在括号之外使用。

容器中的下一个必需属性是作用域。作用域指示容器应在何处可见或可访问。我的意思是块作用域,即它受到周围花括号的限制;该变量将在声明该变量的块的外部看不到。这称为词法作用域。

让我们在第一部分中重写程序以使用作用域变量:

my $is-it-K = M cmp 1000;
given $is-it-K {
    when More {say "M"}
    when Same {say "S"}
    default {say "L"}
}

$is-it-K 变量(也可以使用大写;这里的K用于表示1K,如1,000)将包含标量值。 你不必担心所包含数据的实际类型; 在这种情况下,它将是 Bool,但是变量可以容纳任何内容。

你正在使用=进行分配。 声明变量后,即使它是其他类型,也可以使用并获取新值:

my $is-it-K = M cmp 1000;
my $result = ( given $is-it-K {
                     when More {"M"}
                     when Same {"S"}
                     default {"L"}
                  }
             );
$is-it-K = "Yes" if $result eq "S";
say $is-it-K;

这将显示 "Yes",即 $is-it-K 的值,该值已从 Bool 更改为 Str,没有问题。 新代码还显示了如何使诸如给定的语句返回将存储在 $result 中的值。 与在块中一样,此语句将返回最后执行的指令的值,在这种情况下,该值将为 "S" 字符串。 一般来说,函数或表达式返回一个值的好习惯,而不是对它执行用户可能想要或可能不希望的任何操作(例如使用say)。 那就是你在这里所做的。

但是,实际上看起来像一个函数。 函数是 Raku 中的一等公民。因此,让我们将它们存储在某个地方并从那里使用它们:

my $is-it-K = -> $test { given $test {
                               when More {"M"}
                               when Same {"S"}
                               default {"L"}
                            }
                        };

say $is-it-K(M cmp 1000) eq "S" ?? "Yes" !! "No";

结果是完全一样的,但是你使用了尖的块语法来声明一个块。 该块将使用在“尖的”部分后面的→声明的一个或多个变量,而不是你到目前为止使用的占位符。 该块将返回最后一条语句返回的值。 声明后,无论尖或尖的块都类似于标量,因此可以将其保存在标量容器中。 这是介绍 & 符号的好时机:

my &is-it-K = -> $test { given $test {
                               when More {"M"}
                               when Same {"S"}
                               default {"L"}
                            }
                        };
say is-it-K(M cmp 1000) eq "S" ?? "Yes" !! "No";

当你使用 "&" 号声明变量(即创建容器时的操作)时,你会说它将是一个函数。 因此,你可以根据需要跳过 "&" 号,并使用函数名称以通常的方式调用它。

到此为止,你可能已经意识到该函数的作用是将测试结果转换为字符串,因此你给它指定的名称可能有点不公平。 另外,它是一个功能完善的函数,因此你最好这样声明它:

sub cmp-to-s( $lhs, $rhs ) {
    given $lhs cmp $rhs {
        when More {"M"}
        when Same {"S"}
        default {"L"}
    }
}
say cmp-to-s(M, 1000) eq "S" ?? "Yes" !! "No";

例程使用 sub 关键字声明,并且像其他变量一样是变量。 默认情况下,它们还返回上一条语句返回的值。 但是与块不同,你实际上可以使用 return 语句 明确说明你要返回的内容。 你可以将结果分配给变量并返回该变量,这将在需要时帮助你调试它:

sub cmp-to-s( $lhs, $rhs ) {
    my $result = ( given $lhs cmp $rhs {
        when More {"M"}
        when Same {"S"}
        default {"L"}
    });
    return $result;
}

say cmp-to-s(M, 1001) eq "S" ?? "Yes" !! "No";

你已经用括号将给定的语句括起来,在这种情况下,该括号用作评估器。 从语法上讲,不使用它们是不正确的,因为它将在简单的分配中使用一个块。

你不需要,但可以根据需要声明它们的作用域; 假设一个词汇范围(我)。 在此情况下,& 标记将起一定作用,它是要区分容器(包含功能代码)与包含的容器(功能本身)。 当你想获取容器的值时,可以使用标记。 要使用容器的值时,可以放下标签:

sub hello() { "Hello"; }
my $hello = hello; # Will contain "Hello"
my &copy-of-hello = &hello; # Will contain a copy of the function say "Original ", $hello, " and copy ", copy-of-hello;

在这种情况下,你可以使用变量来包含例程的结果,以及所使用的版本以指示例程本身。 你获得前者的值,然后调用后者,显然后者返回相同的值。

变量的范围也可以是时间或温度。 这些变量具有块作用域,但继承外部作用域的值:

my $us-fugit = now;
sub this-will-take( $n ) {
    temp $us-fugit;
    my @fib = 1,1, * + * ... ∞;
    my $nth = @fib[$n];
    (my $save, $us-fugit) = ($us-fugit, now);
    return $nth, $us-fugit - $save, $us-fugit;
}

for 100,1000,10000 {
    my ($res,$took,$final ) = this-will-take( $_ );
    say "Computing $res took $took from start finished at $final";
}
say "Everything took ", now - $us-fugit;

你正在使用相同的变量名 $us-fugit 来测量“将要执行的例程”内部和外部的时间; 它将用于保留花费的时间以及完成的时间。 $us-fugit 使用例程外部的值进行初始化,并在例程结束时重新分配。 你在此处使用数组赋值:(my $save, $us-fugit) 的左手边=(现在是 $us-fugit);声明 $save 并且还使用已经声明的 $us-fugit,然后以完全相同的顺序分配它们。 你可以在循环的第一行中使用类似的技术来声明三个变量。 请注意,在这种情况下,my 在括号之外,这意味着你声明所有三个变量的范围,该变量的值将由例程返回。 请注意,此举将有效地返回一个列表,并且其值将依次分配给这三个变量。 另一方面,状态变量会阻止一次调用的值到下一个并仅初始化一次:

sub this-will-take( $n ) {
    state $us-fugit = now;
    my @fib = 1,1, * + * ... ∞;
    my $nth = @fib[$n];
    (my $save, $us-fugit) = ($us-fugit, now);
    return $nth, $us-fugit - $save, $us-fugit;
}

for 100,1000,10000 {
    my ($res,$took,$final ) = this-will-take( $_ );
    say "Computing $res took $took, finished at $final";
}

我已经用黑体字对主要更改进行了排版(来自上一个示例)。 两条线也已消除。 第一次调用子例程时,将初始化 $us-fugit,此后它将保留该值,以便你可以测量从返回上一调用的值之前的瞬间起所花费的时间。

此外,你可以使用anon从词汇范围中隐藏变量。

7.2. 类以及如何分辨类

Raku 使用鸭子类型,但有一定的限制:它将类分配给文字,然后根据其外观将其分配给保存它的容器。 但是,你还可以通过使用类型约束来约束容器的容纳范围,类型约束位于范围和变量名声明之间,因此

my Int @array= 1,2,3

将完全由 Ints 组成,并且如果你尝试为元素分配其他内容,则程序将失败。 你显然可以限制标量的值:

my Str $name = "Þor"

在散列的情况下,涉及两个值:键和值。 使用与上述相同的语法将限制值类型:

my Int %hash = %( 'eggs' => 3, 'zucchini' => 2 )

但是你可以限制两个:

my Int %hash{Bool};
%hash{True} = 3;

花括号之间的类将限制键的类型,在这种情况下,键的类型必须为 Bool,而变量名之前的类将限制值。 这也说明了一个事实,即容器是一个类的实例,即有效生成带有特定类的容器的元级类。 Raku 包含一个元对象协议,可以轻松地从头开始创建新类型,还可以进行内省或询问某些方面 一些数据,包括一个容器。 它甚至具有特殊的语法 访问元类的方法:尖号 ^。 当你找到一个时,它的意思是“该方法不是来自类的,而是来自元类的。”在本章中,我们主要对类和类型感兴趣。 你可以使用 ^name 询问有关容器属于哪种类的元类:

say {3}.^name;    # Block
say (my @a).^name # Array

在这种情况下,请检查如何动态定义容器,方法是给它指定一个范围,然后对其进行检查。 同样,在这种情况下,它显示了如何将变量定义为 Positional(使用 @sigil)将在默认情况下为它提供 Array 类,这是最简单的 Positionable 可变类。 元对象协议远不止于此。 例如,你可以实时修改属性。 我将在有关类的章节中介绍更多语法。 其他元对象方法可能会有用:

3.^methods.say
# Returns usable methods: new Capture Int Num Rat
Str.^mro.say
# Ordered class hierarchy: ((Str) (Cool) (Any) (Mu))

7.3. 赋值、强制、可变性以及绑定

容器需要容纳。 这就是为什么创建它们的原因。 这就是他们的目的。 但是,有不同的包含方式,也有不同的方法来查找包含的内容并进行更改。 首先要弄清楚的是,容器和包含对象可能具有不同的类:

my @a = 1,2,3;
say @a.^name;     # Array
say (1,2,3).^name # List

容器的行为与可变的原始行为最接近。 在某些情况下,你可能需要容器的行为完全符合你的期望。 在这种情况下,你使用绑定:

my @a := 1,2,3;
say @a.^name; # List

在这种情况下,@a 将绑定到列表,因此其行为将与该列表完全相同,类等于 List 以及所有。 但是,绑定还意味着其他含义:尽管它始终是容器,但除非绑定到其他对象,否则它们的命运将受到约束。 在这种情况下,对 @a 的任何分配都会失败(因为列表是不可变的)。 然而,

@a := 4,5;

将 @a 重新绑定到另一个列表,这是完全可以接受的。 绑定到不可变值不是绑定的最常见用法。 实际上,为容器创建别名也是一个用例:

my @to-be-bound = <a b c>;
my @binder := @to-be-bound;
say @binder; @to-be-bound[1] = 'þ';
say @binder; # [a þ c]
@binder[1] = 'p';
say @to-be-bound; # [a p c]

这两个变量相互绑定并且实际上是相同的。 另一方面, Raku 中有实际的常数。它们实际上只在编译期间被分配了一次。 他们永远不会改变。

constant %what = { doesn't => 'change' };
# {doesn't => change}

如果你尝试更改该值,使该容器不可变, Raku 将抱怨。 某些数据结构也是不可变的,只要你尝试修改所包含的数据而不是所包含的值,这些数据结构就会被容器继承。 无 Sigil 变量是一种特殊情况。 对于初学者来说,他们没有 默认类型,因此它们可用于任何类型的数据。 但是主要区别在于,它们充当分配的数据的别名,因此也绑定到该特定数据,如下所示:

my @de-sigilled = ^3;
(my \up-to-three = @de-sigilled).say;
@de-sigilled[3] = 3;
up-to-three.say;
up-to-three[4] = 4;
say @de-sigilled; # [0 1 2 3 4]

无符号变量的范围声明使用转义字符以避免混淆; 以后使用时将不再使用。 在这里,你可以看到 @de-sigilled 如何绑定多达三个(越来越不准确的描述),并且可以使用其中一个更改数据。 在这种情况下,你可以使用简单的索引机制来索引(原始)端,以扩展数组直到包含0到4。 常量也是用于在语言核心中定义特殊数字的机制。 只要 Raku 完全支持 Unicode,它也可以包括一些传统上用字母或字母组合表示的数字。 它们显示在表7-2中。

数字

值/解释

pi, π

3.141592653589793

tau, τ

2*π,6.283185307179586

e, 𝑒

欧拉常量, 2.718281828459045

7.4. 上下文

上下文是 Raku 使用的一种机制,用于在变量上调用特定方法以使其在被要求是其他东西时能够正常运行。 最简单的例子是当你需要打印变量的内容时。该变量必须位于字符串上下文中,因为它将被可视化。但不仅如此:例如,某些运算符会强制使用数字上下文。 但是在 Raku 中经常发现的一种上下文称为接收器上下文。如果没有人使用操作返回的值,则调用该方法。 通常,这不是你要放置对象的东西,而是如果你不掌握例程产生的值,就会以一定的频率出现某种错误。 对于其他上下文,表7-3显示了 contextualizer 运算符(即将应用于对象以将其放入该对象中的运算符),以及将为此目的在该对象上调用的函数。 表7-3。上下文,它们的运算符以及他们对强制给它们的对象的调用方法

上下文

上下文器

方法

Sink

不使用值

sink-all(对于序列)

Number

算术运算符(+,-,*,/)

Numeric

String

~, also put

Str

Item

$

item

List

, 和 @

list

也许我们需要注意列表的最后两个元素。 逗号是列表上下文,从某种意义上说,它将列出左侧的内容,并将其与右侧的内容组合在一起。 如你所见,分配给 Positional 容器还将自己创建一个列表:

say (3,).^name;
say (3, my @ = 2,3,4).^name;

这两个是列表,在第一种情况下具有单个元素,在第二种情况下具有两个元素的嵌套列表。 在第二种情况下,你创建了一个仅由符号组成的匿名 Positional。 因为你正在定义 这样,你只需为数字分组,然后将它们放在列表上下文中即可(你可以使用括号),而无需给它起一个名字。 在前面提到的意义上,该变量也是状态变量,因此无需声明就可以按原样使用。 但是 @ 还是列表上下文运算符,换句话说,它与先前的运算符结合在一起。 该项目等效于标量上下文。 中的物体 标量或项目上下文似乎没有内部结构,并且表现为单个实体:

(my $itemized = $[1,2,3]).perl.put; # $[1,2,3]
$itemized[2].say; # 3
@$itemized.perl.put;

你首先要在 $ 前面放置一个数组。 这样,你可以将其分配给标量,也可以将其用于 Positional,如第二条语句所示。 你在这里使用 .perl,这是数据结构的机器可读表示。 在这种情况下,它会在数据结构前面打印一个$,这样说:“嘿,这实际上是一个项目。” 在示例的最后一行中,你将看到 @ 如何用作标量数据结构前面的前缀运算符,以将其返回到其原始的,带有状态的状态。

项上下文对于理解复杂结构(如数组)作为数据容器的性质非常重要。 看看这个例子,你将数组的最后一个值和最后一个倒数放在数组中:

my $first = 3; my @latests;
for ^3 {
    @latests = ($first, $first *= 3 );
}

say @latests;

你可能希望它容纳 [27,81] 之类的东西,对不对? 数组值是不可能的。 好吧,他们是。 此表达式打印 [81 81]。 怎么会? 问题在于,逐项仅在你说完时才发生。 分项使值成为事物,项目,从而使它们凝胶化。 你可以通过仅打印它们来逐项列出它们。 但是,当你这样做时,第一个和第二个元素将具有相同的值,因为你在同一条语句中分配了它们。 让我们这样更改它:

my $first = 3;
my @latests;
for ^3 {
    @latests = (+$first, $first *= 3 );
}
@latests.say

这样,你就必须将其逐项列出,使其成为事物,即存储在数组中的值。 可以预期,这将打印 [27 81]

7.5. 扩展的标识符

到目前为止,你已经看到了标识符,尽管带有破折号和撇号有些奇怪,但它与任何其他语言几乎相同。 但是,以防万一你还没有注意到, Raku 与任何其他语言都不一样。 除了常规标识符(遵循裸标识符语法)之外,它还包括扩展标识符。 通常,扩展标识符类似于常规标识符,后跟所谓的副词。 你已经了解了副词的样子:副词以冒号开头,但是作为扩展标识符的一部分,副词将在冒号后包括(或不包括)常规标识符,后跟引号。 我们来看一些例子:

:won't-do
:will-do<now>
:how're-you«doing»
:<+>
:<©>

所有这些示例都是有效的副词。 请注意,仅允许在引号内使用特殊字符; 这正是他们的功能。 如上所述,你还可以使用任何一种引用构造。 要创建扩展标识符,可以将常规标识符(遵循裸标识符语法)与一个或多个副词组合:

I:<❤>:this
person:age<54>:doing<stuff>

使用此扩展标识符语法的容器当然是完全有效的:

say (my $I:<❤>:this = 7)
say (my @person:age<54>:doing<stuff> = [True,False])

接下来,还将使用此语法来定义术语。

7.6. 不带条件的项

在第一近似中,项可以定义为不需要参数的例程。 你已经找到了现在的形状,它会以即时形式返回当前时间。 那是核心时间中定义的另一个术语。

say  time

返回当前的 POSIX 时间,即自1970年1月1日以来经过的秒数。 你也可以根据自己的目的定义这些术语。 请记住,由于它们使用扩展标识符语法,因此可以使用任何字符:

sub term:<✔> { True }
sub term:<R> { srand(time); return rand }
say "{✔} {R}";

在第一种情况下,你要定义的术语对于所有目的都是恒定的。 这样做的好处是,你可以使用任何字符来定义常量,这与常量本身不同,常量本身仅限于常规标识符的语法。 你还可以定义自己的超级随机数R,该超级随机数R在每次调用时使用时间项作为随机生成器(使用 srand)的种子。 由于你使用的是无符号变量(一个函数也是一个变量),因此你需要以某种方式指出它们将在字符串中进行解释。 这就是为什么要用大括号包围它们。 用花括号括起来的字符串中的表达式将由 Raku 解释,并将结果放置在字符串中。

7.7. 原生数据

Raku 的一个有趣特性是它如何处理在编译器正在运行的体系结构和操作系统中使用本机表示形式的数据。 这就是我们所说的本地数据。 在处理输入/输出流时,或在处理以另一种语言(如C)编写的低级本机代码时,你可能会找到此类数据。 在其他情况下,你可能只想将其用于可能的性能改进。

但是,有两种本机类型,有或没有大小。 主要区别在于,后者可以通过称为 NativeCall 接口的方式在调用以其他语言创建的函数时使用,而前者通常可以使用。 两者都将自动装箱,即在变量中使用时将转换为常规(非本地)数据类型。 请参阅表7-4。

原生类型

自动装箱为

固定大小类型

int

Int

int8, int16, int32, int64

uint

Int

byte, uint8, uint16, uint32, uint64

num

Num

num32, num64

str

Str

void

当你使用 NativeCall 接口与 C 程序一起使用时,将看到大小类型的重要性。 本机数据的主要用途是加速某些数字运算:

sub bm( &to-time ) { my $start = now; return to-time() ~ "⏰" ~ now - $start; }
my int @natives = ^5_000_000;
say bm( { [+] @natives } );
my @regular = ^5_000_000;
say bm( { [+] @regular } );

在我的机器上,整个操作可能需要9秒钟左右的时间,但是使用本机整数将使其速度比常规整数快三倍。 最后,我提到了上面的 void 本机类型。 这种本机类型在 Raku 中没有等效项,但是它主要可以与 C 编程接口交互使用。

7.8. 结束语

在理解之后,在第一章中, Raku 如何表示和处理数据和简单表达式,这是第一章,通过学习如何存储数据(和函数;记住它们是一等公民),你已经创建了 你使用它的第一个程序。 Raku 具有强大的数据存储语法和模型,其中包括绑定和本机类型等概念。 通过编程解决任何问题,首先包括处理数据表示。

在接下来的章节中,你将基于此基础,从如何定义和处理功能开始。

8. 函数

定义和使用函数作为一等公民。

8.1. 可调用的:代码、块和例程

你已经在前面的章节中看到了具有一流公民职能的 Raku 如何凭空产生代码块并与之协同工作。本节的标题列出了用于函数的类 复杂程度从高到低。它们都是可调用的;代码是实现该角色的最简单的类型。块可以定义签名或将在代码本身内部使用的变量的名称;例程是一个具有自己名称的块,可以指定将通过 return 语句返回的数据。 例程本身可以是不同的类型。它们通常不直接使用。方法是在类中使用的例程,这些例程保留对对象及其属性的隐式引用。子方法是类中不继承的特殊方法。子程序是通过 sub 关键字定义的常规例程。最后是宏,这些宏是生成其他代码的代码段(并且暂时还没有完全实现)。

在 Raku 中,正则表达式或正则表达式是特殊方法,是 Method 的子类。稍后你将看到原因。

其余大部分章节将大体上应用于例程。 通常,你将使用 sub,因为在下一章中将全面介绍面向对象的编程。 让我们从最重要的部分开始,定义例程的调用方式和返回的内容。

8.2. 签名和捕获:调用函数和返回值

看起来很简单,对吧? 你声明要提供用于调用该函数的参数的名称,仅此而已。 也许你添加了一个很好的类型。 举例来说,假设你要创建一副由数字和西装组成的纸牌:

sub deck( @numbers, @cards ) {
    return @numbers X~ @cards;
}
say deck( 1..3, <bastos espadas>);
say deck( <J Q K>, <♥ ♣ ♠ ♦> );

到目前为止,你已经使用了标量参数,因为这只是说明前几章要点的全部。 但是,这里有两个数组作为参数。 这意味着参数将被转换为数组。 在第一种情况下,1..3 是一个 Range,但是它被强制为一个数组。 参数名称之前的符号将用于检查类型信息。 它必须是这种变量,或者有一种转换为该变量的方法。 与你之前在变量分配中看到的不同,类型检查要严格一些,并且标量不会升级为 Positionals,也不会被强制关联到其中。 但是,如果你使用不可靠的参数,则可以采取一些强制措施:

sub deck( @cards, *@numbers ) {
    return @numbers X~ @cards;
}
say deck( <bastos espadas>, 1,3,5); # (1bastos 1espadas ..)
say deck( <♥ ♣ ♠ ♦>, "Ace" ); # (Ace♥ Ace♣ Ace♠ Ace♦)

含糊的参数(以星号开头)会将所有其他用作参数的参数吞噬到数组中。 在第一种情况下,每个参数都放入数组的元素中;在第二种情况下,使用单个字符串时,它将成为具有单个元素的数组。 你也可以使用→限制例程的返回类型,如下所示:

sub deck( @cards, *@numbers --> Seq ) {
    return @numbers X~ @cards;
}

X 操作返回一个序列; 如果尝试返回其他任何内容,则将引发类型检查异常。 除了声明变量的范围外,还可以对参数进行简单的类型检查:

sub deck( @cards, UInt $how-many, *@numbers
          --> List ) {
    return (@numbers X~ @cards)[^$how-many];
}
say deck( <bastos espadas>, 4, 1,3,5); # (1bastos 1espadas 3bastos 3espadas)
say deck( <♥ ♣ ♠ ♦>, 2, "Ace" );       # (Ace♥ Ace♣)

此代码引入了另一个参数 $how-many,该参数需要为无符号整数(Uint)。 这将限制生产的卡的数量。 同时,你正在从 Sequence 中提取切片,该切片始终会产生一个列表,这就是你更改返回类型的原因。 你已经有三个参数,并且如果你看一下卡片组调用,可能很难区分参数在哪里结束和下一个开始。 如果你有四个或五个以上,最好给参数命名而不是使用它们的位置来知道每个参数是什么。 命名参数使用你在本书前面看到的对语法:

sub deck( :@suits, UInt :$how-many = 2, :@cards --> List ) {
    return (@cards X~ @suits)[^$how-many];
}
say deck( suits => <bastos espadas>,
            how-many => 4, cards => (1,3,5) );
my @suits = <♥ ♣ ♠ ♦>;
my @cards = "Ace";
say deck( :@suits, :@cards );

请注意,你使用的是 $how-many 的默认值。 默认值也可以在具有相同语法的位置参数中使用,尽管只有它们占据最后一个位置。 位置参数使用冒号,并且它们同时指示变量的名称 将在例程中使用,以及它们将在调用中使用的键的名称。 在第一个通话中,你将使用这些键。 但是,在第二种方法中,你使用另一种形式,如果你使用与定义相同的语法来调用,则变量本身的名称就是关键。 此外,你还保存了一个命名参数,并插入了默认值。输出与以前相同,但是可能已经达到了一些清晰度。

你可能还希望例程为你做一些转换。 你可能需要将返回类型或任何参数设为其他类型。 Raku 可以让你强制转换参数并即时返回类型,如下所示:

sub deck( Array(Set) \suits,
    UInt :$how-many = 4, :@cards
    --> Seq(List) ) {
    return (@cards X~
      suits.values.map("❖❖" ~ ∗.key);)[^$how-many];
}
my $palos = set <bastos espadas>;
say deck( $palos, how-many => 4, cards => (1,3,5) );

你应该在这里注意几件事。首先,它说明了只要位置优先,就可以将 named 和 Positional 参数一起使用。在这种情况下,你声明一个名为 suits 的 Positional 参数。 那是你应该注意的第二件事。在常规签名的情况下,无符号变量很有用,因为它们不会对论点施加任何形式的作用;可以使用标量,位置,关联或可调用参数来模糊地调用无 sigil 参数。但是,在这种情况下,你使用它是因为,第三点,你应该注意到,你是从一个不是 Positional 的集合强制到一个是 array 的强迫。印记会在此过程中发生变化,因此你可以摆脱它们。 你还返回了一个 Seq,例如,它对于迭代更方便。括号中的类是源,在两种情况下,外部是目标,参数和返回类型强制。 最终,函数的参数集具有一定程度的复杂性,并且该复杂性由 Capture 对象捕获。 Capture 对象是 Positionals 和 Associatives 的混合,因此可以简单地视为混合数据结构。不仅如此:它实际上可以用于调用函数,而不必枚举例程调用中的参数。你在这里执行以下操作:

sub deck( @suits, UInt $hand = 3, :@cards
          --> Seq(List) ) {
    return (@cards X~ ("❖❖" X~ @suits) )[^$hand];
}
my $capture = \(<Bastos Espadas>,
                :cards("Sota","Caballo","Rey"));
say deck( |$capture );

这个例子仍然是你的纸牌游戏的演变,只生成很少的程序,它使用第二个 Positional 参数的默认值简化了签名,但是有趣的部分是在其中插入反斜杠(\)来定义 Capture的地方。 前面的括号。 这是你在第3章中看到的相同的字面定义。 请注意,你同时使用 $ 符元,以混合使用位置和联想。 即使是 Capture,你也必须区分用这种变量调用子捕获和将 Capture 用作整个捕获集之间的区别。 此例程的参数。 竖线恰好做到了:捕获变量,将其转换为 Capture,该 Capture 将在例程本身中解构为其参数集。 当然, Raku 是一种功能语言,你可以创建创建其他功能的功能:

sub dealer( @cards --> Callable ) {
    my &deck = sub {
        state @cards-we-have = @cards;
        my @shuffled = @cards-we-have.pick: *;

        my $card = @shuffled.pop;
        @cards-we-have = @shuffled;
        return $card;
    }
    return &deck;
}
my &deck = dealer( <Bastos Espadas> X~ "❖❖" X~ <Sota Caballo Rey>);

deck().say for ^3;
# Every time, something like Espadas❖❖Sota

功能发牌人将一副纸牌作为输入,并创建另一个处理这些纸牌的功能,(通过状态变量)存储剩余的纸牌。 此示例中的主要问题是,你使用词法范围的变量 @cards 初始化了状态变量 @cards-we-have,并在变量 &deck 中返回了发牌人。 这些类似对象的函数本身非常有用,可以在你的程序中获利使用。 你显然也可以将其用作参数,甚至可以为其声明类型约束:

sub draw( @cards, &drawer:(Positional) ) {
    return drawer(@cards);
}
my @cards = <Bastos Espadas> X~ "❖❖" X~ <Sota Caballo Rey>;
sub first-drawer( @c ) { @c.shift };
sub last-drawer(@c) { @c.pop };

say draw( @cards, &first-drawer ); # Bastos❖❖Sota
say draw( @cards, &last-drawer );  # Espadas❖❖Rey

使用 & 符号,很明显第二个参数是一个函数,它将告诉你要从卡片组中提取哪张卡。你定义其中两个:一个拿第一张牌,另一张拿最后一张牌。由于参数声明该函数必须采用位置参数,因此你只需使用 @sigil 将该参数限制为这些函数,即表明它实际上是位置参数。然后,你可以使用这些已定义的函数调用 draw 函数,并记住在开始时添加符号,以指示你正在使用包含函数的容器,而不是调用函数本身。

你还使用了 pop 和 shift 这两个函数来提取 数组的最后一个和第一个元素。 Raku 包括用于处理数组的整套函数,它们也可以使用 List,Any 和 Mu 函数(由于它们是超类)。你可以在完整的参考手册中访问它们,网址为 https://docs.perl6.org/type/Array。通常,任何称为 "A-Class" 的 Raku 类都将在 https://docs.perl6.org/type/A-Class 上托管参考手册。

8.2.1. 子集和类型约束

Raku 中的类型系统从对象定向中获取定向,但也从功能语言中获取定向。 子类型通过条件从预先存在的类型中派生出类型,如“除了……之外,就是这样”。 子集的6个概念与此相差一个级别,因为子集不是正确的类型(尽管可以在许多情况下使用它们),但是你可以在程序中完全利用它们。 你将使用一个来限制卡可以使用的数字:

subset CardNumber of Int where 0 < * <= 10;

sub card( CardNumber $card-number, $suit ) {
    return "$card-number de $suit";
}
say card( 2, "Bastos");
say card( 9, "Espadas");

卡号子集将卡的数量限制为1到10。 正在使用 Whatever 进行检查; 以与处理表达式相同的方式,* 表示要检查该类型的任何内容。 你可以将子集而不是类型用于例程签名。 它可以让任何可以检出的数字通过,如果没有,则失败,就像在进行类型检查一样,仅是你即时创建的数字。 子集很棒,最好为与你要解决的问题有关的对象定义子集。 但是你也可以使用它们来限制签名本身中的参数值:

sub card( Int $card-number where 0 < * <= 10, $suit ) {
    return "$card-number de $suit";
}

这将与以前相同。 如果你的支票比较复杂,最好不要给它加上例行定义,但除此之外,从技术上讲,这样做是可以的。

8.2.2. Junction

我将在本书的后面介绍这些对象,但是在声明函数参数时它们非常有用,因此现在该讨论它们了。 连接点是多值对象; 他们可以取几个值, 并且它们与它们并行运行,包括比较:

my $cat = "Dead" | "Alive";
say $cat eq "Dead";  # any(True, False)
say $cat eq "Alive"; # any(False, True)

使用|创建连接点。 粒子,并且原则上可以具有任意数量的值。 这将导致类型为 "any" 的 Junction,可以在所有类型的操作中用作替代,如此处所示。 但是,返回值将是将该操作应用于所有组件的效果,如输出所示。

这不是唯一可用的结点类型。 表8-1中显示了它们,以及它们将折叠为的布尔值。

类型

意义

运算符

any

如果其中任意一个为真则结果为真

|

all

都为真时结果才为真

&

one

有一个为真则结果为真

^

none

都不为真时结果才为真

N/A

默认情况下,结点的类型为 any。 如果要创建任何其他类型的 Junction,则需要使用以下类型为列表添加前缀:

my $cat = one <Dead Alive>;
say "Dead or alive" if $cat eq 'Dead' and $cat eq 'Alive';

当且仅当比较结果之一为 True 时,if 后面的表达式才为 True。 在两种情况下都为 True,因此将打印 "Dead or alive"。 如表8-1所示,你还可以使用

my $cat = "Dead" ^ "Alive";

结点对于约束例程的参数非常有用:

sub card( Int $card-number where 0 < * <= 10,
    $suit where "Bastos" | "Espadas" | "Oros" | "Copas" )
{
    return "$card-number de $suit";
}
say card( 2, "Bastos");
say card( 9, "Espadas");

该脚本将与上面的脚本相同。 但是,在这种情况下,你还将通过交点将可使用的西装限制为西班牙甲板上的西装。 + Junction 语句很简单,等于包含 * 的等式,但是它是如此普遍,以至于简化为仅参数必须遵循的选项。 如果在这里使用另一个词,则会出现类似 约束类型检查未能绑定到参数 '$suit'; 预期会遇到匿名约束,但得到了 Str(“钻石”) 将被抛出。 它告诉你“它不符合匿名约束”这一事实(可能会有些混淆),将最佳实践定义为任何种类的约束作为子集。 但是,上述解决方案是完美无缺的,并且在技术上是正确的。

8.2.3. 智能匹配

你已经看到了许多取决于它们实际比较的相等运算符。但是有时候你可能不知道某些东西的实际类型,例如,作为函数参数。或者你根本不在乎。 Smartmatch 进行救援。此运算符使用像这样的双弯曲行:~~

该运算符不关心其操作数的类型。你需要(智能)猜测要比较的内容,并告诉你是否正确。为此,可能需要某种类型的强制,但是智能匹配将为你带来快乐。该运算符将为你执行的一些匹配包括

  • 平等比较与类型无关。

  • 容器是否属于某个类别?

  • 两个对象相等吗?

  • 列表模式是否相同?

容器是否发挥作用?该元素在范围内吗? 这个字符串是哈希中的键吗?

还有许多其他用途,并且它在特定课程中的作用始终是直观的,但是有时它比你想象的要强大。当然,它也用于正则表达式,甚至检查文件的状态。在后面的章节中将对此进行更多介绍。 以下是一些示例:

say "1" ~~ 1;
my $a-set = set <1 2 3>;
say $a-set ~~ Set;
my @bound = <3 33 333>;
my @bind := @bound;
say @bound ~~ @bind;
say @bound ~~ (3, *, 333 );
say @bound ~~ Iterable;
say 3 ~~ ^5;
my %myth-objects = %(Þor => "Mjólnir",
                     Oðinn => "Hugin") ;
say "Oðinn" ~~ %myth-objects;

所有这些示例均返回 True。 该运算符确实有一个小问题:它不是对称的。 在大多数情况下,你可以互换左侧和右侧,并且它们的工作原理相同。 但是,它实际上是作为右侧对象的一种方法实现的,因此交换可能以不同的方式起作用。 例如,

say (3, *, 333 ) ~~ @bound ;

将变为 False,因为左侧的对象不再是数组,右侧的对象不再是与左侧对象智能匹配的对象。 使用 Junctions 时,其行为就像是在比较之后是折叠为单个布尔值,因此

say "Sota" ~~ "Sota" | "Caballo" | "Rey";
say "As" ~~ "Sota" | "Caballo" | "Rey";

将返回 True 和 False,以及使用 eq 后跟通过 so 或 ? 折叠的结果。 事实上,这就是签名的作用:它与参数值智能匹配。

8.2.4. 解耦签名

你已经看到 Raku 能够捕获所有将用于在 Capture 中调用例程的值。 实际上,它还具有称为签名的对象,其中包含调用例程的签名的格式。 它们主要用于检查例程是否通过 smartmatch 遵循特定签名。 有时,事先不知道调用例程的所有可能性。 你知道总体布局,但是细节可能有所不同,并且可能使用几种可能的类型作为参数。 有一个简单的解决方案:不用精打细算,但是你会丢失所有类型信息,并且可能会针对所使用的不同类型定制实现。

my $card-printer = :(Int,Str);
sub print-card( Int $card-number,
                Str $suit,
                &printer where .signature ~~ $card-printer) {
    return printer( $card-number, $suit);
}
my &de-printer = -> Int $card, Str $suit { "$card de $suit" };
my &printer:<♦> = -> Int $card, Str $suit { "$card ♦ $suit" };
say print-card( 2, "Bastos", &de-printer);   # 2 de Bastos
say print-card( 9, "Espadas", &printer:<♦>); # 9 ♦ Espadas

签名在第一行中定义;文字签名在一组括号之前使用冒号。在其中,你可以放入例程定义中的所有内容,包括参数名称在内。但是,它们无关紧要:签名代表参数的结构,而不是其实际名称。这就是为什么在这里你只需消除它们以使结构裸露,以便可以更清晰地看到它。 例程的第三个(位置)参数是将签名的功能与智能匹配一起使用的地方。隐式变量(和对象)在 where 子句中的行为与在块中的行为相同:.signature 是应用于对象的方法,该方法将填充参数。 你定义了两个例程;它们之间没有什么特别之处,只是你已使用扩展语法定义了第二个。这似乎是适当的,因为它就是要引入的那个精确的符号。 你调用打印卡例程两次,每个定义的打印机功能调用一次。他们俩都签出OK,因为它们具有正确的签名,因此它们被调用并返回格式化的卡名。

8.3. 函数中的多重分派

签名检查使 Raku 有了另一个不错的技巧:根据参数与实现签名的匹配方式,使用相同的名称调用不同的函数实现。 这些被称为多重,并且它们遵循多个时间表的技术。 从根本上讲,这就像重载具有多种含义的例程; 但是,与 Raku 一样,它带有类固醇。

enum Palo <Bastos Copas Oros Espadas>;
enum Suit <♣ ♦ ♥ ♠>;
multi stringify-card( Palo $p, Int $n ) { "$n de $p" }
multi stringify-card( Suit $s, Int $n ) { "$s\c[EN QUAD]$n" }
say stringify-card( Bastos, 3 );    # 3 de Bastos
say stringify-card( Suit::<♣>, 5 ); # ♣ 5

在定义了两个枚举(按照你在第3章中学习的方式)之后,你可以使用这些相同的枚举来定义两个整数,每个整数处理不同种类的卡名。 但是,由于它们是枚举,因此你必须使用完全限定的名称,其中包括枚举的名称。 你正在使用。 尽管你必须使用西装的完全限定名称,但你可以节省一些文字,因为它不能直接用作枚举。 你可以通过对参数使用模式匹配来保存它。 但是,与此同时,定义所有这些功能必须遵循的原型将是很有趣的,作为要添加其他签名时应使用的签名的指南。 另请注意,默认情况下,multi 是 subs。 在这种情况下,你将省略子声明。 上面的声明等同于 proto sub。 原型或原型还会在其后的所有功能之间创建链接:

proto stringify-card( Str, Int) {*}
multi stringify-card( $p where (* ~~ any <Bastos Copas Oros Espadas>),Int $n ) { "$n de $p" }
multi stringify-card( $s where (* ~~ any <♣ ♦ ♥ ♠>), Int $n ) { "$s\c[EN QUAD]$n" }
say stringify-card( "Bastos", 3 ); say stringify-card( "♣", 5 );

你可以使用此链接来级联处理一些信息:

proto stringify-card( | ) {*}
multi stringify-card( Str $p ) { "▶ $p" }
multi stringify-card( $p where
                      (* ~~ any <Bastos Copas Oros Espadas>),
                      Int $n ) {
      samewith "$n de $p"
}
multi stringify-card( $s where (* ~~ any <♣ ♦ ♥ ♠>),
                      Int $n ) {
      samewith "$s\c[EN QUAD]$n"
}
say stringify-card( "Bastos", 3 ); # ▶ 3 de Bastos
say stringify-card( "♣", 5 );      # ▶ ♣ 5

这个字符串化卡片的原型放宽了签名,留下了非常通用的 | (接受任何签名); 你可能还用过

proto stringify-card( Str, Int $? ) {*}

你不需要插入变量名,因为它们实际上并不是签名的一部分;但是,带有问号的可选参数确实需要至少得到一个提示。这就是为什么你使用简单的标记后跟问号的原因。该声明将以与上述相同的方式工作。始终建议尽可能准确地使用签名。但是,在上述情况下,你实际上并不预先知道如何使用卡的级联处理,因此暂时将其保持这种状态。引入了一个新的多重游戏,主要用于通过在任何卡牌前面加 "Play" 符号来对字符串中表示的卡牌进行后处理。由于这是“组”的一部分,因此你无需使用子名称:你可以使用 samewith,它使用提供的一组新参数调用相同的 multi。

这只是与一组乘法一起使用的例程之一。其余的重新分配功能如表8-2所示。

命令

意义

callsame

callwith

nextsame

nextwith

nextcallee

8.4. 项和运算符

有时,应用程序的业务逻辑需要使用标识符,以逃避字母数字字符(任何字母)的约束领域。 正是出于这个目的而发明了 Raku 中的扩展语法,它使你可以定义使用 Unicode 字母中的任何类型字符的任意函数(即,参数数量)。 如果这些函数不接收任何参数,则将它们称为术语。 如果它们具有1个或多个参数,则称为运算符。 例如,你的应用程序逻辑可能会告诉你抓一张牌。 当然,你用铅笔画画

sub term:<✏> {
    return (^10).pick => <♣ ♦ ♥ ♠>.pick
};
say ✏; # 2 => ♥, for instance.

你已定义术语✏来抽牌,并且每次都会抽一张不同的牌。 定义完毕后,你就不需要括号(因为它不需要参数)或术语词本身; 你可以直接使用它。 你使用方法 pick,每次从无限套牌中获取随机元素。 有兴趣的读者将学习如何使用有限的套牌进行相同的操作。 提示:使用状态变量来保留已绘制的卡片。 由于卡片自然是一对,由西装和数字组成,因此你可以使用一对来握住它们。 但是,有一种简单的方法可以创建它们吗? 再一次,你的业务逻辑可能会导致你定义一个操作员,以便进行更简单(或更直接)的定义。 例如,你可以使用它们来定义和构建纸牌:

sub infix:<⚙> (Str $s, Int $n) { $s => $n };
sub postfix:<♣> (Int $n ) { "♣" ⚙ $n };
sub postfix:<♦> (Int $n ) { "♦" ⚙ $n };
sub postfix:<♥> (Int $n ) { "♥" ⚙ $n };
sub postfix:<♠> (Int $n ) { "♠" ⚙ $n };
say qq:to/CARDS/;
{10♦}
{6♥}
{2♠} {4♣}
CARDS

你在此处定义两种运算符:infix(带有两个参数,在中间使用)和 postfix(单个参数,在参数后面)。 表8-3显示了可以定义的所有运算符类型及其含义。

运算符类型

意义

Posix

1

Prefix

1

Infix

2

环缀

1

后环缀

2

在这种情况下,代表西装的字符定义为后缀运算符,而构成卡片的齿轮为中缀运算符。 由于打印对可能包含箭头,并且在这种情况下你不希望使用它们,因此你正在使用其他构造进行打印。 首先,在字符串中用 {} 括住任何表达式将对该字符串求值。 然后,你使用的是引用结构 qq:to/WORD/,它允许你定义多行字符串。 双重 q 表示它不是文字(将是单个q),但对其进行了评估。 这样可以打印一组对齐的卡:

♦ 10
♥ 6
♠ 2
♣ 4

有趣的是, Raku 是一种非常一致的语言,其大多数解释器都是使用 Raku 本身编写的。 这为你提供了许多在语言本身中使用这些运算符的示例。 的 例如,{} 提取附加在哈希键上的值,它被定义为后缀运算符,该运算符将哈希和键作为参数。 方括号本身是用于构建数组的后缀运算符。 定义复数的虚部的字母 I 也以这种方式定义。

8.5. 结束语

本章专门讨论功能。你已经了解了如何定义它们,包括非常特殊的术语和运算符,它们不过是带有有趣名称(和字符)的函数。捕获和签名是同一件事的两个方面:捕获抽象了调用例程的参数;签名抽象了参数的类型以及它们的组织方式。 签名中的类型约束(在其他语言中也称为合同)使你有两个有趣的概念:交集或以相同方式表示替代状态或值的方式,以及 smartmatch(用于多类型匹配的运算符)。 函数中的多个调度使 Raku 更加接近于函数语言,在函数语言中,类似的概念(称为模式匹配)是定义函数的默认方式。 总而言之,这些功能使 Raku 成为一种语言,能够使你编写的程序尽可能接近你的业务逻辑,从而使其在视觉上反映你将要使用的实际对象。如此具有表现力的程序往往会更短,而且这种长度(和表现力)也使语言解析更加有效。 但是, Raku 也是一种多范式语言。你将在下一章中看到如何定义类。

9. 角色和类

将代码和数据捆绑在同一块中并重复使用是主要部分所谓的面向对象语言中的角色和类 在过去的几十年中,面向对象编程已成为计算领域的主流范例之一。 它提供了对问题域的简单抽象,并为自然建模提供了一种方法。 但是,有很多方法可以使你的程序面向对象。 与 Raku 一样,它不会选择其中之一:它包括所有内容,并让用户选择他们喜欢的内容。 这种编程风格并不排除你已经看到的其他风格:你可以使用对象或面向对象的风格通过其类接口访问函数来进行功能风格的编程。 在本章中,你将对对象和类进行编程,但是还将探索角色以及处理对象的更精确,更奇特的方式。

9.1. 创建类和对象

类描述直接在其上操作的数据结构和功能。 它们实例化到对象,即对象是类的实例或实例。 类中的对象可以这样声明和使用。

在这种情况下,“类”和“类型”可以互换使用; 在 Raku 中,所有类型都是类,因为它们可以实例化并具有附加的方法。 你已经看到了许多内置类型。 Raku 中的所有类型实际上都是类。 在大多数内置类型中,创建是通过使用文字或对它们进行的任何操作来隐式完成的。 但是对象是通过 new 方法创建的; 你可以根据需要使用它:

say (my $new-int = Int.new(3)) ; # 3

当我说一切都是一个对象时,我也是指类本身。 实际上,它们被称为类型对象,你可以像使用任何其他对象一样使用它们:

my $Simply-an-Int = Int;
say (my $new-int = $Simply-an-Int.new(3)); #3

$Simply-an-Int 是一个类型对象,与一个类相同,并且可以像其余的类一样使用new。 应该有一些方法可以区分简单对象和类型对象,对吗? 实际上,存在一种称为类型笑脸的机制,可以在签名中使用该机制来区分它们。

proto stringify-card( Str, |) {*}
multi stringify-card( $s where (* ~~ any <♣ ♦ ♥ ♠>), Any:D $n )
{ "$s\c[EN QUAD]$n" }
multi stringify-card( $s where (* ~~ any <♣ ♦ ♥ ♠>), Any:U $n )
{
    "$s\c[EN QUAD]" ~ (1..10).pick;
}
say stringify-card( "♣", 5 );
say stringify-card( "♥", Int );

类型表情符号添加 :D或 :U(如“确定”或“不确定”)以表示对象实例或类型对象。 在第二个多重中,你使用 类型对象只是为了表明你应该随机生成卡值; 但是 $n 确实包含该类型,并且可以按你想要的任何方式使用它,例如从类型对象生成该类型的元素。

"$s\c[EN QUAD]" ~ $n.new((1..10).pick);

在这种情况下,输出将完全相同,只有你确保已生成该特定类型的对象。 另请注意,你已将签名中的类型放宽为“任意”。 所有内置类型都将Any子类化(事实上,Junctions除外)。 由于参数值将与定义智能匹配,因此大多数类型(包括Int)都将满足该约束,也就是说,它们将是将Any(参数的类型)作为子类的类型对象(由:U type smiley表示)。 这不是处理签名中类型的唯一方法; Raku 提供了使用特定变量的类型值的类型捕获:

sub stringify-card( $s, ::T $n ) {
    when T ~~ Str { "$n of $s" }
    when T ~~ Int { "$n\c[EN QUAD]$s" }
    default { "$n$s" }
}
say stringify-card( "♣", 5 );     # 5 ♣
say stringify-card( "♥", "Ace" ); # Ace of ♥

类型捕获用两个冒号和要保存该类的容器的名称表示。 你可以使用when子句和smartmatch来区分 $n 将使用的不同类型,具体取决于它是数字还是符号。 就像@或$一样,将这些双冒号视为一个符号,它告诉你标识符将保留一个类型。

你可以在任何类型的例程中使用上面的附加语法,但是你将专注于作用于对象实例的例程,这些例程称为方法。 常规例程的主要区别在于它们 将对象本身作为隐式参数。 你已经看到,使用对象名称后缀的句点来调用它们。 例如,使用.match匹配字符串的一部分:

"J ♣".match("J").say # ⌈J⌋

9.1.1. 内省

你已经看到带有 ^ 前缀的方法,如 ^name 一样。 这些是元对象协议中方法的调用,作用于生成所有类的类。 由于对象是类的实例,所以类型对象是元类的实例。 Raku 允许你访问这些级别中每个级别的内部工作。 你可以使用HOW方法找到此高阶对象:

"J ♣".HOW.say; # Perl6::Metamodel::ClassHOW.new

HOW不是命题,而是“高阶工作”的首字母缩写。 实际上,所有带有^前缀的方法都是这些对象的方法,因此上面的代码等效于

"J ♣".HOW.name("").say

深入研究类的内部工作称为自省。 你在第7章中已经看到了此方法以及 ^ 方法和 ^mro。 但是你可以使用其他(元)方法。 请参阅表9-1。

元方法

它做什么

can($name)

返回使用该名称的方法列表

lookup($name)

返回与该名称的第一种方法,或者一个未定义的值,如果不存在

attributes()

返回实例变量列表

你无需访问这些元方法即可检查是否可以对某个对象执行某些操作。 在方法名称前加上? 如果存在,将调用该方法,否则将返回Nil。

for (3, 1..3, "m" ) -> $m {
    .say with $m.?bounds()
}

这将在多个对象上调用 .bounds,这是 Ranges 专用的方法。 由于with语句仅在定义了结果的情况下运行,因此只会在列表的第二个元素(实际上是Range)中打印内容。

9.1.2. 声明和使用类

你也可以创建自己的类:

class Card {
    has $.value;
    has $.suit;
    method show() { "$!value of $!suit" }
}
my Card $deuce = Card.new( value => 2, suit => '♥' );
say $deuce.perl;
say $deuce.show;

用 class 关键字声明一个类。 类代码括在方括号中,其中包括方法和属性。 用关键字 has 声明方法,它们是实例变量,当创建该类的新对象时它们将接收一个值。 这些属性使用 twigils: 代表角色的常用符号,在这种情况下,后跟一个句点,表示它们是公共属性。 这意味着 .value(和 .suit)方法是自动生成的。 你可以在这里使用它们,例如:

say $deuce.map( { .value, .suit } ); # ((2 ♥))

将别名 $deuce 映射到默认变量 $_,并将 .value 和 .suit 应用于它们。 你可以声明一个方法 show,该方法返回带有值的字符串。 在此方法中,存在实例变量的值。 你可以使用方法形式($ .value)来访问它们,但是你将使用新的符号 $!,它是 self 的缩写。 $!attribute 检索当前对象的 attribute 属性值。 在上面的示例中,这就是 $!value 和 $!suit 的含义。 你也可以使用 self,这是一个表示对象在其方法内的术语。 可以达到相同的效果

method show() { "{self.value} of $!suit" }

在这种情况下,由于self前面没有符号,因此需要大括号才能在字符串中求值。 由于所有类都是Mu的实例,因此它们都有一些方法。 .perl 是其中之一:它产生对象的机器可读表示,如果进行评估,从理论上讲该实例可用于实例化。 上面的示例使用默认的 new 创建对象。 此默认新名称具有一个签名,其中包括所有实例变量作为命名参数。 但是,你当然可以将默认值添加到该默认值:

class Card {
    has $!value;
    has $!suit;
    multi method new ($value, $suit ) {
        return self.bless( :$value, :$suit );
    }
    submethod BUILD( :$!value, :$!suit ) {}
    method show() { "$!value of $!suit" }
}
my Card $deuce = Card.new( 2, '♥' );
say $deuce.perl; # Card.new
say $deuce.show; # 2 of ♥

如上所述,如果你使用隐藏属性,则这尤其必要。 隐藏的属性使用 !twigil 既表明它们将被隐藏(也从 .perl 中隐藏),也没有为它们创建访问器的事实。 你仍然可以使用默认的 new 来添加值,但是如果要在指示如何实例化对象的文档中公开实现,则隐藏实现没有太多意义。 如果你不想覆盖默认的新值(子类可能仍会使用),则需要将新值 new 声明为 multi; 在其中,你可以使用默认构造函数的签名调用 self.bless; 如你所知,:$value 等于 value ⇒ $value。 但是幸运的是开始了对象的构建阶段。 由于未使用默认构造函数,因此也需要指定它。 生成对象的过程按表9-2所示的不同阶段进行。

Phase

动作

BUILD

创建对象时将值分配给实例变量

TWEAK

创建对象后,将值分配给实例变量

在课堂上,你将使用子方法来实现这些相位器。 始终会使用 bless 从默认的new或(几乎)显式地按以下顺序隐式调用它们:

new or bless → BUILD → TWEAK

实际上,子方法的定义中没有代码。 通过使用签名中的属性,调用 BUILD 的值将直接绑定到它们,而无需在子方法的主体中显式分配它们。 如果你没有覆盖默认的构造函数,则可以选择一种替代方法:你可以通过以下方式显式调用它:

return Card.new( :$value, :$suit );

就像你在课堂上使用的一样。 这将获得完全相同的结果。 但是你仍在使用 show 来获取价值。 你的类可以重载每个类具有的方法(在第6章中已提及),例如 .perl。 但是要显示该对象,只需将方法的名称更改为 gist 即可重载 .gist:

method gist() { "$!value of $!suit" }

然后你可以直接使用

say $deuce

这将调用该方法。

你还可以将默认值分配给实例变量。 在这里,你使用一个新属性 $!victories 来存储特定卡赢得的次数:

class Card {
    has $.value;
    has $.suit;
    has $!victories = 0;
    method new ($value, $suit ) {
        return self.bless( :$value, :$suit );
    }
    submethod BUILD( :$!value, :$!suit ) {}
    method better-than( Card $c ) {
        if $c.suit eq $!suit {
            if $!value > $c.value {
                $!victories++;
                return True
            } else {
                return False;
            }
        } else {
            return False;
        }
    }
    method gist() { "$!value of $!suit won $!victories times" }
}
my Card $deuce = Card.new( 2, '♥' );
say $deuce; # Will print "2 of ♥ won 0 times";
say Card.new( 3, '♥' ).better-than( $deuce );
say $deuce.better-than: Card.new( 3, '♦' );
say $deuce.better-than: Card.new( 1, '♥' );
say $deuce; # Will print "2 of ♥ won 1 times";

通常,类属性遵循与范围声明或签名接近的语法。 通过附加等号和所需的值来分配默认值。 请注意,尽管使用了不同的 twigils 进行声明,但始终使用 !.twigil 以获取其值。 在这里,你还使用了基于冒号的调用方法,这在使用地图和其他对象时就已经见过。 它只是使用括号的一种更简洁明了的替代方法,但是只有在最后一次调用时才可以使用它一连串。

你介绍的新方法只需要使用另一张 Card, 如果值较高且它们属于同一套衣服,则返回 True。 如果它不属于同一套衣服,则不能说它的好坏(取决于游戏),因此返回 False。 由于方法实际上是例程,因此它们也可以被多次调度:

class Card {
    has $.value;
    has $.suit;
    method new ($value, $suit ) {
        return self.bless( :$value, :$suit );
    }
    submethod BUILD( :$!value, :$!suit ) {}
    multi method better-than( Card $s where $s.suit eq "Joker": Card $c) {
        return True
    }
    multi method better-than( Card $s: Card $c where *.suit eq $!suit) {
        if $s.value > $c.value {
            return True
        } else {
            return False;
        }
    }
    multi method better-than( Card $s: Card $c where *.suit ne $!suit) {
      return False;
    }
}
my Card $deuce = Card.new( 2, '♥' );
say Card.new( 3, '♥' ).better-than( $deuce );
say $deuce.better-than: Card.new( 3, '♦' );
say Card.new( 0, "Joker" ).better-than( $deuce );

你以前有一个相当长的双重测试,需要考虑所有可能的值进行相当广泛的测试。但是,多个计划允许你使用方法本身的签名 决定采用哪个路径以及可以使用参数约束 定义它。这就是你在新版本的代码中所做的。但 你需要做更多的事情:使用冒号作为签名的一部分。在第一个近似值中,冒号是一个分隔符,使你可以识别调用方(在左侧)和签名的其余部分(在右侧)。从这个角度来看,它只是一个别名:你可以使用变量的名称(及其公共访问器)来执行此操作,而不是使用 self 或使用 $! 直接访问属性。 但是,签名的这一部分也具有与其余部分相同的优点:你可以对其进行约束,以便仅通过对对象属性施加约束就可以将方法定向到实现。第一个多重是这样做的:假设一个小丑击败了所有其他牌。你将使用“ Joker”作为西装,其值等于0,因此,如果纸牌的西装是Joker,那么它将始终返回 True。该方法仅在西装为Joker的对象中调用。

这似乎都像语法糖,用于保存 ifs 和 else。 但是如果在很多情况下 ifs 可能很笨拙,那么解释器将能够更有效地使用这些方法定义。 例如,它可以更有效地缓存这些方法调用的结果。 这也是一种更实用的编程方式,可以匹配模式而不是执行多个决策。

9.2. 创建角色:方法和属性

除了使用封装,代码重用和(也许)继承的基本定义之外,还有许多方法可以理解面向对象。当你谈论从旧类创建新类时,其中一个第一类就是干部。你可以组成类来创建新类,可以实现接口但不包括代码,或者可以通过包含这些类的对象的属性来简单地重用代码。也就是说,你可以重用代码和/或接口。一旦代码将被重用,你就可以不使用实例变量。最重要的是,你还可以决定是否使类成为通用类(即,应用于任何其他类)还是特定类,以便烘焙可以应用它们的对象。 与许多其他方面一样, Raku 不会为你做出任何选择。它允许你重用代码,接口,实例变量以及几乎所有内容。或不。你可以决定如何实际编写代码并执行其约定。在 Raku 中,角色是编写和重用代码,接口或两者的方法。 角色是包含属性和方法的数据结构,也可以是通用的。类扮演(实现或组成)一个角色,包括该角色的所有代码和属性作为其自身的一部分。因此,角色是组合的或混合的,或者,如果它们包括未实现的方法,则由特定的类来实现。

在这里,你会使用 role 这个词来定义它们:

role Card-values {
    has @.values;
    method one { @!values.pick };
}
class Card-types does Card-values {
    has @.suits;
    method get-one { [@!suits.pick, self.one ] };
}
my @values = (2..10);
@values = @values.append( <J Q K Ace> );
my $french-cards = Card-types.new( :@values, suits => <♦ ♠ ♥ ♣> );
say $french-cards.get-one;

你已经定义了一个角色,用于存储卡值,这很棘手,并且不能减少为 Range 中的简单数字。 你可以使用do将该角色组成一个包含西装的类。 由于 Card-values 由 Card-types 组成,因此 Card-types 包含 Card-values 声明的所有属性和方法。 Card-types 声明的新属性与混合属性处于同一状态,默认构造函数同时使用这两种属性。 你可以使用 self.one 来调用该方法,因为方法一已经是该类型的一部分。 这种对象建立很有意义。 一方面,卡类型不是卡值,因此直接继承是没有意义的。 另一方面,声明 Card-values 属性不会使值成为对象的固有值或一部分,因此你仍然需要使用该属性作为对象来显式调用它。

实际上,将诉讼也作为角色会更有意义:

role Card-values {
    has @.values;
    method one-value { @!values.pick };
}
role Card-suits {
    has @.suits;
    method one-suit { @!suits.pick };
}
class Card-types does Card-values does Card-suits {
    method get-one { [self.one-value, self.one-suit ] };
}

这将与以前相同。 卡类型混合使用两种角色,并使用它们的界面生成随机卡。 只需添加所需的 do 子句,即可混合你在类中所需的角色。 但是,当然有很多西服和卡牌值。 角色具有一个非常有趣的功能: 可以对它们进行参数化,也就是说,可以使用一种或几种类型来通用定义它们,供开发人员选择:

role Card-values[::T] {
    has Str $.name;
    method one { T.pick };
    method better-than ( T \lhs, T \rhs ) {
        return lhs < rhs;
    };
}
enum french-digits <2 3 4 J Q K Ace>;
enum spanish-digits <2. 3. 4. Sota Caballo Rey As>;
class French-card-values does Card-values[french-digits] { };
class Spanish-card-values does Card-values[spanish-digits] { };
my $french-cards = French-card-values.new( name => "French" );
say $french-cards.better-than( french-digits::«3», french- digits::«J»);
my $spanish-cards = Spanish-card-values.new( name => "Española" );
say $spanish-cards.better-than( Rey, Sota );

参数化在要参数化的角色名称之后的方括号中。如本章前面所述,在这种情况下,它使用类型捕获,但也可以使用变量。关于类型捕获的好处是,在这种情况下,类型可以稍后用于定义实例变量或方法调用。 在角色上使用类型实例化角色时,必须确保该角色可以执行角色内部应做的任何事情。它在这里做两件事:使用 .pick 方法,该方法实际上存在于 Any 中,因此存在于其任何子类中(请记住,除 Junction 外的所有核心类),并通过 < 使用数字比较。实际上,这是你在枚举中利用隐式顺序的地方:它们的声明顺序与声明的顺序相同。由于你事先不知道类型,因此使用无符号变量保存其值会更安全;这些变量即使是“关联”或“位置”也可以保留。 使用枚举时,你将需要输入完全限定名称,并在此处显示枚举名称和符号: French-digits ::«3»。如果它是常规字符串(例如J),则不需要它,但是为了清晰起见,你仍然可以使用它。 实际上,已定义的类没有自己的任何代码,而更多是对参数化角色的重命名。在这些情况下,可以使用称为角色修剪的功能,该功能包括直接使用(可能已参数化)角色,就像它们是类一样:

my $french-cards  = Card-values[french-digits].new( name => "French" );
my $spanish-cards = Card-values[spanish-digits].new( name => "Española" );

在这里的示例中消除类定义并替换这些行将为你提供完全相同的结果。 尽管 Card-values [french-digits] 是一个(已参数化的)角色,而不是一个类,但通过修剪可以创建它的实例。

9.3. Giving,使用和混合角色

关于容器在运行时的行为, Raku 系统非常灵活。 它可以做的非凡的事情是使用but将角色混合成一个值:

role Card {
    method Str(::T:) {
        when T ~~ Str {
            my @pair = self.comb;
            return @pair[0] ~ " of " ~ @pair[1..*].join("");
        }
        when T ~~ Pair {
            return self.value ~ " of " ~ self.key;
        }
    }
}
my $deuce = "2♠" but Card;
say $deuce.Str; # 2 of ♠
my $ace = :Bastos("As") but Card; say $ace.Str; # As of Bastos

but infix 运算符将其左侧的值与右侧的角色,对象或类混合。在这种情况下,它是一个简单的标量,但是通过混合使用 “ Card” 角色,你已经设法为其提供了一个不错的接口,该接口以或多或少统一的方式将其转换为字符串,而与将其定义为 Str 还是一双。在这种情况下,Str 方法定义中的大量冒号使用类型捕获来捕获对象的类型。因为它后面跟着一个冒号,这意味着它将被应用于对象本身,自身。这种灵活性以及可以直接将其应用于对象的事实非常独特,它允许你使用值,无需为其定义新类型就可以为其添加功能。 $ace 中最后一个值的类型将为 Pair + {Card},表示它是一个 Pair 对象,但其中混入了 Card。 如果混入一个简单的对象(例如标量),则将创建一个匿名角色。尽管混合对象的调用方法将访问该对象的“部分”,但这将是透明的:

my $deuce = "2♠" but 2;
my $ace = "Ace ♠" but 100;
say $deuce.^name; # Str+{<anon|1>};
say $deuce.Int < $ace.Int; # True

你还可以通过使用与定义类相同的关键字来将角色与容器相关联:

role Hand {
    method draw () { self.pick };
}
my @my-hand does Hand = <5♠ 3♦ 8♦>;
say @my-hand.pick;

@my-hand 是一个简单的数组,但是你可以混合使用 Hand 角色,从而允许它使用 draw 方法,如下所示。 请注意,由于存在错误,标量变量不能使用。

9.4. 继承

继承和封装是面向对象编程的两个主要支柱。 继承表示两个类之间的关系-a。 纸牌游戏的一种特定类型是-(更通用的)纸牌游戏,或者仅仅是一种游戏。 因此, Raku 用来表示继承或子类化:

class Game {
    has Str $.name;
    method score( @deck ) { ... };
}
class Brisca is Game {
    has %!scores = {
        As => 11,
        3 => 10,
        Rey => 4, Caballo => 3, Sota => 2
    };
    method score( @deck ) {
        my $score = 0;
        for @deck.grep( any %!scores.keys) -> $c {
            $score += %!scores{$c};
        }
        return $score;
    }
}
class Guiñote is Game {
    has Int $.diez-de-últimas;
    has %!scores = {
        As => 11,
        3 => 10,
        Rey => 4,
        Caballo => 2,
        Sota => 3
    };
    method score( @deck ) {
        my $score = $!diez-de-últimas;
        for @deck.grep( any %!scores.keys ) -> $c {
            $score += %!scores{$c};
        }
        return $score;
    }
}
my @deck = <As 3 7 8 Rey Caballo>;
my Brisca $game1 .= new: name => 'brisca1';

my $game2 = Guiñote.new( name => 'Este guiñote', diez-de- últimas => 10 );
say $game1.score( @deck ); # 28 say $game2.score( @deck ); # 37

你定义一个基类 Game。西班牙的纸牌游戏类似于现代纸牌游戏:你抽一手牌,然后从你的手牌中选择一张牌来玩。如果该张牌的得分高于另一张,则你将赢得两张牌。如果没有,则另赢。游戏结束时,你会得到一堆纸牌,并且会得到一张分数,这取决于纸牌的固有分数以及其他因素,例如你赢得了最后一局(diez deúltimas,最后一局得10分)。有不同的游戏(在不同地区和拉丁美洲的游戏方式不同),但基准 是根据你在游戏期间可能获得的纸牌价值和额外胜利以不同的方式对一堆纸牌评分。但是每个人都有基于卡片的分数,这就是为什么你定义分数方法的原因。通过使用 yadda yadda yadda 运算符(…​),你可以使该基类不可实例化,并强制所有子类重新实现它;因此,游戏将是一个抽象的基本类,没有实际的代码。

该运算符也可以在角色中使用,强制这些类混合角色以重新实现它。 除此方法外,子类还将继承实例变量 $!name。仅公共实例变量将被继承;私有(用 !twigil 声明的那些)也将对派生类保持隐藏或私有。如果需要使用它们,则必须定义这些私有变量的公共接口,考虑到你将无法从构造函数初始化它们,而只能在 TWEAK 阶段通过公共接口进行初始化。 方法也可以是私有的。让我们通过以下方式重新定义 Guiñote 类:

class Guiñote is Game {
    has Int $.diez-de-últimas;
    method !_score-card( $c ) {
        my %scores = :11As,
        3 => 10, :4Rey, :2Caballo, :3Sota;
        return %scores{$c} if $c ~~ any %scores.keys
    }
    method score( @deck ) {
        my $score = $!diez-de-últimas;
        $score += self!_score-card($_) for @deck;
        return $score;
    }
}

私有方法带有 ! sigil 标记。 ,它们使用 self!method-name 进行调用。期间可以访问公共方法,而 ! 在此类领域中,是私有或隐藏活动(例如属性或方法)的指示器。按照惯例,你要去 将前面的下划线(_)用作此隐藏状态的视觉指示器。隐藏的方法显然不是继承的,而且实际上是对类本身以外的任何东西隐藏的。 这两个派生类使用不同的计分表,主要区别在于你对 Sota(Jack)和 Caballo(Horseman,相当于其他级别的 Queen)进行评分的方式,还在于 Guiñote 使用 “diez deúltimas” 在计算套牌分数之前将其加到最终分数。这些类需要实现分数,否则创建一个对象将失败。这两个类使用 grep 和 Junction 过滤掉任何 cardshoisescoreis0.any % !scores.keys 创建一个 any Junction 的卡片,并且grep仅匹配那些与任何密钥相对应的卡片。 你使用不同的语法进行实例化和定义。你声明第一个游戏的类,该类允许你使用 .= 调用新方法并创建实例。通常,op = b等于 a = a op b。 Raku 通过方法调用对此进行了概括:a-class a .= new 等效于a = a-class.new。这是你在此处使用的语法。在第二种情况下,你无需声明类:在 Raku 中进行鸭类输入会自动将 Guiñote 类分配给创建的对象。 继承的类可以具有基类作为静态类型:

my @deck = <As 3 7 8 Rey Sota>;

my Game $game1 = Brisca.new: name => 'brisca1' ;
my Game $game2 = Guiñote.new( name => 'Este guiñote', :0diez- de-últimas );
say $game1.score( @deck );
say $game2.score( @deck );

将变量声明为基类将防止它在不在层次结构中的类中实例化。 但是,你将能够将另一个子类作为值分配:

my Game $game = Brisca.new: name => 'brisca1';
say $game.score( @deck );
$game = Guiñote.new( name => 'Este guiñote', :0diez-de-últimas );
say $game.score( @deck );

你正在重复使用同一变量,该变量能够容纳层次结构中任何位置的动态类型。

9.5. 结束语

Raku 中的面向对象具有两个关键概念:角色和类。 角色既混合在类中,又混合在对象和变量中,并且类被继承,子类从基类中获取实例变量和方法。 此外,你现在知道要处理签名中的类对象,并且现在应该了解元对象协议 好一点。 你应该能够设计和使用类层次结构,包括 角色,到现在为止。 在下一章中,你将把这些知识集成到更高级别的结构,模块中,这些结构,模块将用于创建多文件,复杂的程序。

10. 模块

将文件中的功能分组以便重用和提取 模块通常以单个名称将大量功能打包到单个文件中。由于它们是一种多范式语言,因此这些模块具有不同的风格,但是分为两个粗略的类别:单独的例程和类/角色/语法。如你先前所见, Raku 将大多数功能打包到了类中,但是并非所有功能都是如此,当然,这取决于你来决定哪一个最适合你的问题。

模块化有助于使代码文件简单且一致。它还可以帮助记录和分发工作。让我们开始创建模块。

10.1. 重用代码

Raku 引入了 compunit(即编译单元)的概念,该单元是同时分析和编译的代码。到目前为止,你看到的包含在单个文件中的脚本是 compunits。当你将代码拆分为不同的文件时,其中一个是主文件,其余的是文件运行时加载的库,其中每个都是一个 compunit。

通常,你应该尝试创建可装载的 compunit,它们基本上是库,它们在功能上是一致的。他们被称为包。包实际上只创建了一个命名空间,这将有助于避免与其他名称冲突。实际上,它们不过是模块,类和语法(实际上是类的一种)的通用名称。它们可以直接用于打包名称空间中的代码和标识符:

package Pack {
    our $packed = 7;
    say $packed
}

软件包将具有某些功能关系的代码包含到单个文件中,尽管这比语法要求更符合惯例。 Raku 使用模块命名法作为单一命名空间下所有可重用代码的通用名称; 此类程序包和其他类型的程序包会创建单独的命名空间,以便你可以重复使用任意数量的标识符,并且它们之间不会发生冲突。

module Draw-Two {
    our sub draw-two( --> Slip ) {
        state @deck = 1..10 X <♠ ♦ ♣ ♥>;
        if @deck {
            my @shuffle = @deck.pick: *;
            my Slip $draw = (@shuffle.pop, @shuffle.pop).Slip; @deck = @shuffle;
            return $draw;
        } else {
            return [].Slip;
        }
    }
}

say gather {
    while my $new-draw = Draw-Two::draw-two() {
        given $new-draw {
            .say;
            take $_;
        }
    }
}

在这里,你定义了一个名为 Draw-Two 的模块,带有一个名为 draw-too 的子项。这不是一个很普通的名字,但是无论如何你都不知道 模块还定义了可以共享数据的范围。该子例程将返回一个 Slip,它基本上是一个可嵌入的列表:push 将清单转换为列表,它将插入清单,将清单的元素作为连续元素插入列表,而不是创建嵌套列表。这在你的小程序中很有用,在该程序中你将返回随机排列的纸牌。

你可以在此处看到另一个有趣的容器声明功能: 你使用的是你的而不是我的(你在第7章中看到了)。默认情况下,subs 具有词法作用域,因此你需要明确地说出来。 Subs 和以这种方式声明的任何其他容器都具有包作用域,但是与词法作用域变量(用 my 声明)不同,只要使用完全限定的名称(包括包名),就可以在模块外部使用它们。由于默认情况下例程具有词法范围,因此你将经常使用此功能来创建可从最初声明它们的外部访问的功能。当然,你也可以将其用于变量:

package Pack {
    our $packed = 7;
};
say $Pack::packed

请注意,使用变量的FQN或完全限定名称时,符号$移到最前面。 该例程使用状态变量(请参见第7章)来成对绘制卡片,并将它们作为两个元素(数字和花色)的两元素(卡片)列表返回。Deck 用完后,它将返回一个空的支票。

声明时即可使用模块功能。由于你想收集所有元素(在一个随机排列的牌组中成两半绘制),这将是一个新列表,因此可以使用“收集/获取”。 collect 语句位于循环或其他语句之前,并包含在发布给 take 命令的所有数据结构的列表中。在这种情况下,使用给定,你还可以在将其滑到甲板上之前打印所绘制的内容。这个收集/获取循环将打印整个套牌两次:一次(成对输入)(一次给定),一次(整个列表)(一次收集前的输出)。 包是模块化的途径。最佳实践是为每个模块提供自己的文件,该文件通常使用 .pm6 扩展名(如 Perl Module 6 中一样)。在这样的文件内部,可以在文件的开头使用单位声明来指示后面的所有内容都是同一名称空间的一部分,从而节省了缩进和花括号:

unit module Draw-Two;
our sub draw-two( --> Slip ) {
state @deck = 1..10 X~ <♠ ♦ ♣ ♥>; if @deck {
my @shuffle = @deck.pick: *;
my Slip $draw = (@shuffle.pop, @shuffle.pop).Slip; @deck = @shuffle;
return $draw;
} else {
return [].Slip;
} }

这将转到一个文件,该文件通常具有与模块相同的名称,扩展名为 .pm6,即 Draw-Two.pm6。 通常使用 kebab 大小写(用短划线分隔)和大写形式为模块和类(它们都是包)命名。 它们之间实际上没有常规区别。

事实上,你也可以在类上使用 unit 关键词:

unit class Card-Values;

has @.values;
method one { @!values.pick };
method better-than ( $lhs, $rhs ) {
    return @!values.first( * eq $lhs, :kv )[0] > @!values.first( * eq $rhs, :kv )[0];
};

效果是完全一样的:文件的其余部分被理解为 Card-Values 类的声明,并且你无需使用花括号将它们括起来。 需要将以此方式定义的类和模块导入主程序,以供使用。但是首先,你需要了解特征以及如何在包装中广泛使用它们。

10.2. 特质或容器属性

特性是在编译时强制执行的容器属性。除了包含在容器本身中的属性(例如类型或其动态类型)之外,还可以通过与该类型正交的特征来强制执行其他属性。由于它们在编译时起作用,因此,它并不了解内容,实际上可以用来塑造内容,限制其表示形式或创建将在创建对象时运行的代码。

特性可以应用于每种容器。通常,它们使用它们所修改的容器,如果使用关键字 if,其后是特征名称以及某些情况下的参数。 你已经看到用于声明一个类的超类。这种用法与使用它声明特征是一致的,因为两者都引用变量本身的表示形式或形状。

表10-1中列出了最重要的特征。

Trait

应用于

意义

copy

参数

rw

参数、属性、例程

readonly

参数、属性

tighter

操作符

equiv

操作符

looser

操作符

assoc

操作符

default

属性、变量、例程

required

属性

DEPRECATED

属性

export

例程、 类

pure

例程

此示例将基于你已经看到的添加了许多特征的类:

class Card-Values {
    has @.values is rw is required;
    has $.pintan is rw is default("Bastos") = "Espadas";
    method one() is DEPRECATED { @!values.pick ~ $!pintan };
    method draw( $cards = 1 ) { (@!values.pick: $cards ) X~ $!pintan };
}
my Card-Values $cards .= new: values => <As Sota Caballo Rey>;
# Will print a message complaining about deprecated code
say $cards.one;
$cards.pintan = Nil;
say $cards.draw;
$cards.values = ("Ace", 2..10, <J Q K>).flat;
say $cards.draw( 2 );

这段代码定义了两个具有读写访问权限的属性,它们创建了一个 object.attribute 访问器,可以对其进行修改; 你可以使用它来更改牌和西装的值。 卡的价值 将是必需的,因此如果不提供这些值,则对象创建将失败。 另一方面,$.pintan 属性具有默认值,当你通过其访问器使它的值无效(分配给 Nil)时,该默认值将弹出。 DEPRECATED 方法将向标准错误显示一条消息,指示该方法的名称,并且它“看到”了不赞成使用的代码实例。

出口特性在模块中非常重要。 它可以有效地告诉编译器,一旦将其加载到其他地方,哪些例程将在包范围之外显示:

package Moves {
    sub shuffle( *@deck ) is export {
        @deck.pick: *;
    }
}
import Moves;
say shuffle( "As de bastos", "3 de oros", "Sota de espadas" );

该程序将打印参数的改组版本,以便在每次调用时进行改组。 重要部分以黑体突出显示。 你正在使用包作为单个功能的通用包装; 你可以使用类,模块或语法,在这种情况下,它不会起作用。 它的行为方式相同,只需创建一个名称空间即可将 shuffle 驻留在其中并与可能存在于另一个程序包中的另一个 shuffle 隔离。 使用 is export 可以将其作为可导出例程发出信号。 稍后,将仅使用 import 来导入 Moves 中的符号到当前的主命名空间中,以便你可以直接使用 shuffle,就像你在下一行中所做的一样。 请注意,导入不适用于外部文件。 它需要一个现有软件包作为参数。 但是,此命令可以帮助你有选择地从包中导入:

package Moves {
    sub shuffle( *@deck ) is export(:shfl) {
        @deck.pick: *;
    }
    sub card-sort( *@deck ) is export {
        @deck.sort;
    }
}

import Moves :shfl;
say shuffle( "As de bastos", "3 de oros", "Sota de espadas" );
# say card-sort(<1♠ 5♣ 3♥>); # Purposefully commented out

在这种情况下,你添加了一个要导出的参数。 它实际上是一个对,shfl ⇒ True,但这将是选择性导入的一种标签或组。 当你在要导入的软件包的名称后面使用它时,它实际上表示你只对带有该标记的例程感兴趣。 这也从 :DEFAULT 组中排除了该例程,该例程是在不使用 export 的情况下添加的; 在这种情况下,import Moves; 只会导入卡片排序,因为 shuffle 不在 :DEFAULT 组中。 你需要将此组显式添加到列表中,以防默认情况下也要导出该组:

sub shuffle( *@deck ) is export(:shfl :DEFAULT) {
    @deck.pick: *;
}

其他例程将被忽略; cart-sort 已被注释掉,因为如果使用它会导致错误。 你可以使用特殊标签 :ALL 导入所有内容:

import Moves :ALL;

这将导入两个子例程,你将能够使用它们(显然是通过删除注释)。

10.3. 在外部文件中使用模块

了解了声明和导入符号的工作原理后,你可以将程序拆分为两个或更多不同的文件。 严格来说,需要是在编译时用于加载 compunits 的关键字。 加载的 compunit 将是在搜索路径中找到的 compunit,其具有指示的自变量作为文件名。 你将把 Moves 包移到一个名为 Moves.pm6 的文件中,并与以下程序一起使用:

need Moves;
import Moves;
say shuffle( "As de bastos", "3 de oros", "Sota de espadas" );
say card-sort(<1♠ 5♣ 3♥>);

但是,如果直接使用 perl6 need-package.p6 运行此命令,它将无法正常工作, Raku 指示

Could not find module Moves to import symbols from

请注意,不需要在导入语句中产生此错误。 发生这种情况的原因是,编译器首先报告该软件包不存在,然后再报告该软件包不存在,因为它没有找到它。 因此,在这种情况下,需求不是正确的,只是在还没有时间指出发生这种情况的原因之前就已经报告(并保释)了。

首先,你需要了解 compunit include 在 Raku 中的工作方式。与许多其他语言一样,在安装模块时有一个放置模块的搜索路径。 该搜索路径还包括所有系统类和模块所在的目录。 它通常不包括当前目录,在主要操作系统中用“。”表示。 这仅是合乎逻辑且安全的措施,但是在这种情况下,这会使你的程序无法在同一目录中找到程序包。

解决该问题的一种方法是使用 -I 命令行标志:

perl6 -I. need-package.p6

-I,后跟目录名称,该目录将添加到搜索路径。 由于 Moves.pm6 与该文件位于同一目录中,因此足以找到该模块,将其导入并运行程序的其余部分。 因此,使用需求与在以下位置声明模块大致相同。 你仍然需要将符号导入到当前名称空间中。 由于这两个语句经常一起使用,因此它们在 use 命令中组合在一起,

use Moves;

这与两个命令的作用完全相同。 由于 use 将需求和导入结合在一起,因此它使用与导入相同的语法来选择性地进行导入,因此

use Moves :shfl;

只会导入 shuffle,将其他例程保留在自己的包中。 只要它被声明为我们的程序,它仍然可以在主程序中使用; sub 默认为词法作用域,在这种情况下,它将不会在作用域之外被看到:

our sub card-sort( ∗@deck ) is export { # in Moves.pm6
    @deck.sort;
}
say Moves::card-sort(<1♠ 5♣ 3♥>); # in use-package.p6

但是,它们全都在编译时工作。 如前所示,在继续运行程序之前先加载外部模块。 模块名称实际上是常量,其含义与上一章中定义的常量相同:它们是在编译时定义的,并且在程序执行期间不会更改。

你将定义另一个软件包 Moves-Pro,

unit package Moves-Pro;
sub shuffle( *@deck ) is export {
    @deck.pick( * ).reverse;
}

通过颠倒洗牌并通过掷硬币来决定使用哪个来增加洗牌的额外扭曲:

my $module = Bool.pick?? "Moves" !! "Moves-Pro";
require ::($module);
say ::("$module")::EXPORT::DEFAULT::('&shuffle')( "As de bastos", "3 de oros", "Sota de espadas" );

$module 变量将包含要加载的模块的名称,并要求将其加载。 如果使用常量,则 require 将与使用无区别,除了它将在程序执行期间起作用。 但是,如果它是一个变量,则需要通过双冒号前缀使用间接查找。 间接查找是一种插值变量以生成符号名称的方法。 你还可以使用它动态生成变量名:

sub shuffle-pro( *@deck ) {
    @deck.pick( * ).reverse;
}
sub shuffle( *@deck ) {
    @deck.pick( * );
}
my $shuffle = Bool.pick?? "shuffle" !! "shuffle-pro";

say &::($shuffle)( "As de bastos", "3 de oros", "Sota de espadas" );>

你将在变量 $shuffle 中保留要调用的例程的名称。 你将分解对变量名称的间接查找,其形式如下:&::($shuffle)。 首先,标记表明这将是例行工作。 然后,双冒号表示你打算查找名称。 你需要用括号括起来,以表明你将要使用变量,或者通常使用任何表达式来生成变量名。 例如,考虑到部分名称是共享的,你可以使用

my $pro = Bool.pick?? "" !! "-pro";
say &::("shuffle$pro")( "As de bastos", "3 de oros", "Sota de espadas" );

你甚至可以在部分的印记,你都在进步,如

say ::("\&shuffle$pro")( "As de bastos", "3 de oros", "Sota de espadas" );

你需要在转义符的转义符("")之内处理转义符,否则将其理解为常规调用。 间接查找以相同的方式工作,生成一个或另一个名称。 现在,你可以按需分解常规名称了:

  • ::("$module") 将解析为符号,即模块名称。

  • ::EXPORT::DEFAULT 访问 DEFAULT 组中的导出例程表。

  • 最后,::('&shuffle') 将返回你要查找的符号。 请在这里注意单引号。

这些符号表可用于所有已加载的模块:

use Moves;
say Moves::EXPORT::.keys; # (ALL DEFAULT shfl)
say Moves::EXPORT::DEFAULT::.keys; # (&shuffle &card-sort)
say &Moves::EXPORT::DEFAULT::shuffle( "As de bastos","3 de oros", "Sota de espadas" ); # Return the usual

符号表是一个嵌套的哈希,使用 .keys 可以访问其键。第一级EXPORT将返回包含要导出的标签或组的哈希的名称。每个软件包将包含 ALL 和 DEFAULT;这(上面已定义)还包括你已定义的 shfl。深入研究嵌套的哈希,DEFAULT 哈希将包含符号的名称(在本例中为例程),默认情况下将导出。通过将标记放在查找的前面,并将变量的名称放在最后,你可以间接访问它,尽管在这种情况下它是已导入的,因此长名与使用 shuffle 完全相同。

第一次加载模块时,会对其进行预编译。这意味着它将转换为虚拟机格式,可以在下次运行时直接使用;这些文件相对于程序运行的位置存储在名为 .precomp 的目录中。这些目录和文件由 Raku 管理,你无需对其进行任何操作。删除它们都没问题,尽管运行该程序可能会花费一些时间,因为在你执行操作时会重新生成它们。

10.4. 指令

语法是 Raku 使用的文件级指令,用于激活某些功能或以某种方式解释文件的其余部分。 它们通过使用来激活,并且采用与包含外部模块相同的语法。

你已经看到一个:use v6,实际上 Raku 忽略了它,而是致力于告诉 Perl 5 解释器这不是它的地盘。 但是,还有更多。 请参阅表10-2。

指令

参数

意义

v6.c, v6.d

MONKEY-SEE-NO-EVAL

lib

soft

strict

worries

variables

use 的反面是 no: 它禁用编译指示。

no strict;
$totally-new-variable = 7

尽管未声明 $totally-new-variable 的范围(默认情况下是强制性的),但这不会产生任何类型的错误。

libprag 的用法很有趣,并且将允许你在同一目录中使用模块,而无需给 Raku 提供特殊标志:

use lib <.>;
require draw-two-cu <&draw-two>;
say draw-two;

可以使用默认的 perl6 draw-2-require.p6 运行。 此外,你正在此文件上使用它,

sub draw-two( --> Slip ) is export {
    state @deck = 1..10 X <♠ ♦ ♣ ♥>;
    if @deck {
        my @shuffle = @deck.pick: *;
        my Slip $draw = (@shuffle.pop, @shuffle.pop).Slip;
        @deck = @shuffle;
        return $draw;
    } else {
        return [].Slip;
    }
}

say "loaded";

尽管它确实声明了具有 export 特性的 draw-two,但它没有声明任何类型的模块。 由于未明确声明任何程序包,因此你不能使用标签对例程进行分组。 在这种情况下,使用例程名称本身(&draw-two)会将其包括在当前名称空间中,如其执行所示。

请记住,目录相对于程序正在执行的路径; 也就是说,在这种情况下, Raku 将只在运行程序的目录中搜索,而不是在相对于程序脚本所在的目录中搜索。 如果从另一个目录运行它,它将找不到外部文件。 在这种情况下,你需要将所有目录(相对于可能执行该目录的位置)添加到目录列表中。 在这种情况下:

use lib <. Chapter10>;

这样,你将能够从同一目录和上面的目录中运行它而不会出现错误。

perl6 Chapter10/draw-2-require.p6

在这种情况下。

10.5. Raku 生态

Raku 具有丰富的生态系统,在撰写本文时,它包括 2000 多个模块,并且每天都在其中添加新的版本和模块。 Rakudo Star 发行版中包含最重要或使用过的模块和类,这是建议大多数用户使用的模块和类。接下来,你会看到它们。

10.5.1. Rakudo Star 模块

Rakudo Star 模块由发行经理选择,因此使用此发行版的开发人员将能够满足许多需求,而无需下载任何其他模块。整个列表位于 https://github.com/rakudo/star/blob/master/modules/MODULES.txt,在撰写本文时,它包括大约 60 个模块。

例如,一组模块专用于处理 JSON。其中包括 JSON::Fast,JSON::Marshal,JSON::Unmarshal,JSON::Name 和 JSON::Class。如今,JSON 是数据结构序列化的标准,并且还广泛用于配置文件。你很有可能需要在程序中编写如下代码:

use JSON::Fast;
say to-json { 3 => '♠', 8 => '♣' };

如果你的发行版是 Rakudo Star,则可以直接使用它。 如果没有安装,显然你将需要按照在下一部分中看到的方式进行安装。 这将打印哈希的 JSON 表示形式,例如:

{
  "8": "♣",
  "3": "♠"
}

除了 to-json 之外,此模块还实现 from-json; “斋戒”的名称表示其意图。

还有一些其他模块以 Web 为目标,可以轻松地从 Web 下载内容。 其中包括 LWP::SimpleWWWHTTP::UserAgent。 前两个是简单的客户。 最后一个稍微复杂一点,它允许对客户端进行更多配置,以创建复杂的 Web 客户端以及与 API 的交互。 URI模块还从其各个部分检查并创建统一资源定位符。

例如,你可以为生成随机卡的 API 创建简单的客户端:

constant URL='https://deckofcardsapi.com/api/deck/';
use WWW;
my $deck-id = jget(URL ~ 'new/shuffle/?deck_count=1')<deck_id>;
say jget URL ~ "/$deck-id/draw/?count=2";

该 API 的文档可在上面的域中找到,它会生成随机的牌组,你可以从这些牌组开始抽牌。 你使用单个 WWW 命令 jget,该命令下载URL的内容并从 JSON 对其进行解码。 由于采用了 API 用来返回数据的格式,因此你只需一个命令,即可检索为你生成的随机卡组的卡组 ID。 你可以使用该变量从另一个 URL 检索两张卡片,然后在程序中的最后一条语句之后直接打印该卡片。 同样,WWW 将包括其余的 HTTP 命令 put 和 post,如果使用的是 JSON,则以 j 开头。

这些模块首先经过全面测试,然后对简单程序非常有用,但你迟早可能需要使用生态系统中的一个模块。

10.5.2. 生态模块

大多数编程语言都包含发布开源库的标准方法,并且允许任何人通过简单的命令行程序下载它们。 可以在 https://modules.raku.org 上找到所有模块,但是它们位于以下两个位置之一:

  • CPAN 是“综合Perl 存档网络”,作者可以在其中上传其开源模块。 CPAN 与 Perl 5 共享; 你需要被授权在此处上传模块。

  • 任何开放式存储库,例如 GitHub,以及较少见的 GitLab 和 BitBucket。 在这种情况下,作者只需将其模块的元数据添加到GitHub存储库中的列表即可。 模块所在的位置(大部分)是透明的。 安装这些模块的方法是通过 zef 命令行界面。

Zef 已随 Rakudo Star 一起安装,但你也可以使用 rakudobrew build zef 通过 rakudobrew 安装(或升级),或克隆存储库,然后在其中键入

perl6 -I. bin/zef install .

你可能从上面记得,它使用相同的目录(.)搜索模块,然后运行带有参数 install 和的二进制文件(bin/zef)。 因此,你基本上是在使用 zef 自安装 zef。 你最可能从 zef 中使用两个命令。

zef search web

将在生态系统和 CPAN 中搜索名称中包含网络的任何模块;它还将包括任何已安装的模块。 请参见图10-1。

在会话中首次发布该文件时,将需要一些时间来下载CPAN和生态系统的更新索引。根据网络(和服务器)的状态,这可能需要一段时间。如果你编写 zef search -update,则可以更新索引。

此处的主要结果在“包”列中。 From 会说它是从哪里获得的:从 cpan(cpan)或 Raku 生态系统(p6c)本地(它将指示 LocalCache)。但是,如你所见,描述已被裁剪。

你可能在那里获得更多信息(或使用实用程序 grep 对其进行过滤)。如果你需要完整的说明,请使用

zef search web -- wrap

这将充实“描述”列以包含完整的描述。使用 zef 或通过 modules.raku.org 找到所需的内容后, 你可以使用 install 来安装发行版(其中包括几个模块),如下所示

zef install Cro::HTTP

例如。这不仅将安装此模块,还将安装将在其配置中定义的所有上游依赖项。再次取决于网络的状态和依赖项的数量,这可能需要一些时间(在这种情况下肯定会,因为它是一个具有很多依赖项的复杂模块)。如果已安装,并且发行位置中的版本与你的本地版本匹配,它将发出这样的声音并拒绝安装,除非你发出 -force 标志。

安装发行版后,它们将在特定位置创建 Raku 所谓的存储库。它们将自动添加到库搜索路径中,因此你只需要使用它们就可以将它们放入程序中:

use lib <. Chapter10>;
use Cro::HTTP::Router;
use Cro::HTTP::Server;
use JSON::Fast;
use Draw-Two;
my $application = route {
    get -> 'cards' {
        content 'application/json', to-json draw-two;
    }
}

my Cro::Service $croupier = Cro::HTTP::Server.new:
    :host<localhost>, :port<31415>, :$application;
$croupier.start;
say "Server started";
react whenever signal(SIGINT) { $croupier.stop; exit; }

你正在使用整合到 Cro::HTTP 发行版中的几个模块:Cro::HTTP::Router 和 Cro::HTTP::Server。其他类(例如,Cro::Service)将自动合并。你还使用了前面提到的 JSON::Fast 库和你自己的 Draw-Two 模块。开头的 use lib 用于说明此模块可能位于的目录。

在这个紧凑的程序中,你将使用 Cro::HTTP::Router 的命令路由来描述路由,然后继续声明将有效地成为服务器的服务;它将侦听端口 31415,并且仅响应本地主机请求。 需要关闭程序末尾的 react 语句,它也是 Raku 的并发功能。Cro 是创建并发,分布式应用程序的出色工具。

你可以在浏览器中直接输入 http://localhost:31415/cards(它会打印“服务器已启动”)或使用 curl/wget 或其他 CLI 客户端,直接从浏览器中访问此 API。请参见图10-2。

图10-2。 启动应用程序后,通过 curl 从命令行使用 Cro API 每次通话都会返回不同的卡片,直到卡座完全耗尽为止。

10.6. 结束语

本章将引导你走上从 Raku 中的模块语法到丰富的 Raku 模块生态系统之门的旅程。 在此过程中,你已经了解了特征及其在例行签名和容器属性的精确描述中的重要性。 现在你可以创建自己的模块化应用程序了。 在这些情况下,可能会(并且会)出错。 在下一章中,你将看到如何应对运行时错误以及如何以编程方式解决它们。

11. 错误以及如何处理错误

当 Raku 难以理解你的意思时。

如前几章所述,对于某些东西(通常是开发人员和计算机之间,或者有时是用户和计算机之间的误解),错误是一个非常不好的名字。 这只是举起虚拟牌并说:“好吧,我没明白你的意思。” 由于错误只是传达某种东西的一种方式,因此有很多方法可以使用它们,有时甚至可以解决它们。 语言已经建立了一定的机制。 但是,大多数问题来自与运行程序的用户的交互。 与这种语言一样,有很多方法可以做到这一点。

11.1. 命令行参数

首先,从几个主要程序 Deck 中定义一个要使用的小模块:

unit class Deck;
has @.cards = 1..10 X~ <♠ ♦ ♣ ♥>;
method !_shuffle {
    @!cards = @!cards.pick: *;
}
submethod TWEAK {
    self!_shuffle;
}
method draw ( UInt $how-many = 1 --> Slip ) {
    if @!cards {
        self!_shuffle;
        my @draw = gather {
            for ^$how-many {
                take @!cards.pop
            }
        }
        return @draw.Slip; }
    else {
        return [].Slip;
    }
}

该程序引入了一个新的语法概念:子方法 TWEAK,该函数在对象创建后但在(由新对象)返回给用户之前调用。 你将始终需要洗牌,这是默认自动生成或由用户输入的。 对于入门者,你将从这个小程序中完成它:

use Deck;
my Deck $this-deck .= new;
say "One card ", $this-deck.draw;
say "Three cards ", $this-deck.draw( 3 ).join(" ❦ ");

请记住,在本章和以后的章节中,请使用 lib。 在每个脚本的开头都假定为“目录与模块”。 该程序将创建一个 Deck 实例并从中提取一些卡片。 让我们决定要抽多少张牌: 除非另有说明,否则在本章其余每个脚本的开头将假定使用 Deck 作为行。

my Deck $this-deck .= new;
my UInt $how-many = (@*ARGS[0] // 1).UInt;
say "Cards ", $this-deck.draw( $how-many ).join(" ❦ ");

在这种情况下,你将使用动态变量 @*ARGS,其中包含在命令行中使用的字符串数组。 如果你用

% perl6 Chapter11-notest/use-deck-args.p6 3

然后 @*ARGS[0] 将包含 "3" 作为字符串。这就是为什么你需要转 通过使用 (@*ARGS[0] // 1).Uint 将其转换为明智的选择。 draw 方法不使用任何内容(默认为1)或使用 Uint。// 是定义或运算符:如果已定义,则将值返回到左侧;如果不是,它将返回右侧的值。这是为变量分配默认值的一种很好的习惯用法。没有参数,它将默认为1。

@*ARGS 是动态变量或更准确地说是动态范围变量的示例,所有这些变量都使用 * twigil。可能获得值的变量取决于程序或编译器环境或其方式 运行时,都是动态变量。这些变量类似于全局变量,因为它们是在块的词法范围之外定义的(在这种情况下,在 Raku 本身中),但是全局变量在任何内部块中可见,而动态变量在任何从以下位置调用的块中可见定义动态变量的块。即使未定义它们(如此处所示),使用它们也不会导致错误。 表11-1包含对最有用的变量及其包含的内容的快速参考。

变量

内容

%*ENV

%*CWD

%*DISTRO

%*PERL

%*PROGRAM-NAME

%*COLLATION

这里的主要问题是你将参数作为字符串获取,因此需要进行一些转换。 此外,这假设你可以访问命令行。 在某些情况下,该信息可能在其他地方可用,例如环境变量。

使用以下命令从命令行定义环境变量 各种取决于操作系统的机制和 你正在使用的外壳。 在 Linux 中,你应该在最流行的 shell 中键入 export VARIABLE_ NAME = variablevalue。 在许多情况下,包括部署到云中,它们都是预定义的。

但是你也可以使用环境变量。 例如,你在上一章中使用固定端口启动了 Cro 服务器。 你可以这样更改:

my Cro::Service $croupier = Cro::HTTP::Server.new: :host<localhost>, :port(%*ENV<CRO_PORT>), :$application;
$croupier.start;
say "Server started at %*ENV<CRO_PORT>";

然后你可以定义

export CRO_PORT=7777

并像以前一样运行它; %*ENV<CRO_PORT> 的值将被定义。 这个变量有一个好处:它使用同种异体字代替字符串。

my Deck $this-deck .= new;
my $how-many = %*ENV<HOW_MANY> // 1;
say "Cards ", $this-deck.draw( $how-many ).join(" ❦ ");

该程序看起来与以前相同,除了你不需要转换环境变量的值。 由于它是同种异体(我在第3章中已经谈到过),因此根据需要,它的作用与字符串相同。 你可以通过这种方式输入的数据数量有限。 例如,输入命令行标记或 variable = value 类型的参数很复杂。 Raku 确实具有处理此问题的机制:sub MAIN

sub MAIN( $how-many = 1) {
my Deck $this-deck .= new;
say "Cards ", $this-deck.draw( $how-many ).join(" ❦ ");
}

MAIN 例程是运行时将要调用的例程 在命令行中使用此脚本。 你已经从使用动态变量切换为使用 Signatures 附带的所有功能:默认值,以及将 $how-many 转换为所需的类型。 由于此签名使用单个 Positional 自变量,因此在运行程序时使用的 Positional 自变量(与以前相同的命令行,在程序名称后带有可选数字)会自动绑定 到你在签名中声明的变量。 但是,再次,例程和签名的所有功能都随你而来。

my Deck $this-deck .= new;
multi sub MAIN() {
    say "Your card ", $this-deck.draw;
}
multi sub MAIN( $how-many ) {
    say "Cards ", $this-deck.draw( $how-many ).join(" ❦ ");
}

在这种情况下,你将 $this-deck 声明为一个词法变量,它将 在两个多重 MAIN 中可见。 其中一个将在没有参数的情况下运行,另一个(带有不同的消息)将在有一定价值时运行。 请参见图11-1。

通过将 MAIN 变成一个多重,你可以根据输入甚至进行类型检查来运行不同的代码:

multi sub MAIN( UInt $how-many ) {
    say "Cards ", $this-deck.draw( $how-many ).join(" ❦ ");
}

在这种情况下,如果使用非数字的参数运行它,则会发生异常并打印

Usage:
Chapter11/multi-main-type.p6
Chapter11/multi-main-type.p6 <how-many>

此使用消息由 Raku 根据 MAIN 子项的签名自动生成。 你可以轻松地使用它来声明命名参数甚至别名。 可以通过命令行或 MAIN 子程序的签名机制轻松捕获调用参数中的异常。但是,在程序中可能需要处理一些更复杂的异常。 在本章的其余部分,你将看到 Raku 如何做到这一点。

11.2. 失败和异常

例外是意外的情况。 某些本不该发生的事情通常是与程序的外部程序有关的:不存在的文件或 404 版本的 URL,即格式错误的文档。 一个程序不能仅仅停止工作:它需要解决该错误并继续前进。 通常,在这种情况下,它将引发异常。 例如,这个程序

use JSON::Fast;
say from-json "foobar; baz";

将失败,输出将指示该字符串的某些信息,而不是正确的 JSON,不是。 你可能已经注意到的第一件事是,即使你重定向标准输出,也会打印错误。 所以

perl6 exception-config.p6 > /tmp/foo

即使你已使用 > 重定向标准输出,仍会产生相同的文本,因为该消息将打印在标准错误输出而不是标准输出中。 通常,会将异常打印到该设备,默认情况下,该设备是同一控制台。 你还可以自己产生这种错误:

my $this-deck = Deck.new( cards => ( <A J Q K> X~ <♠ ♦ ♣ ♥> ) );
multi sub MAIN( UInt $how-many ) {
    die "There aren't that many cards" if $how-many > 16;
    say "Cards ", $this-deck.draw( $how-many ).join(" ❦ ");
}

die 会产生一个异常,如果不采取任何措施,该异常将有效地退出程序,并在标准错误输出中输出消息:

There aren't that many cards
  in sub MAIN at Chapter11/die.p6 line 15
  in block <unit> at Chapter11/die.p6 line 8

死者要做的就是创造一个例外。 异常表明某事 是错误的,他们掌握了产生它们的信息。 异常还是许多标准和临时异常类的基类,所有这些异常类的形式均为 X::Namespace::SpecificException。 但是,此异常是不确定的。 在强类型语言中,异常需要具有特定的类型。

if $how-many > 16 {
    X::Numeric::Overflow.new.throw ;
}

所有异常都使用 X:: 命名空间,其次要命名空间具有异常类型,最后一部分指定特殊类型的异常。 在这种情况下,你使用了“溢出”,这与使用大于可能或可用大小的大小有关。 主要的次要异常类型包含在表11-2中。

异常类型

上下文或应用

OS

操作系统错误

ControlFlow

Control

SecurityPolicy

AdHoc

Dynamic

Method

Role

Pragma

IO

NYI

OutOfRange

默认的异常类型有几百种,并且所有它们中都有一些约定(例如,NotFound 或 Unknown)。 到目前为止,只有其中一些记录在案。 但是,它们的用法是常规用法,你可以随意使用它们,并根据需要进行处理。 例如,你可以在上面的示例中使用 X::OutOfRange 而不是所选的 X.:OutOfRange。 这将在下一节中看到。

该程序还处理应在另一个级别处理的异常。 实际上,模块本身应该照顾出现的任何问题。 问题在于从模块内部引发异常不会给任何人实际处理它的机会。 输入失败。 失败包裹着异常,它们使开发人员有机会使用它们或在抛出错误之前对其进行检查。 让我们修改之前使用的 Deck,在 Decker 中将其更改为该方法:

method draw ( UInt $how-many = 1 ) { if ( $how-many > @!cards.elems ) {
return X::OutOfRange.new( got => $how-many,
range => "1.." ~ @!cards.elems).fail
}
if @!cards {
self!_shuffle;
my @draw = gather {
for ^$how-many { take @!cards.pop
} }
return @draw.Slip; } else {
return X::OutOfRange.new.fail; }
}

你实际上是在使用 X::OutOfRange 及其实例变量进行初始化,以表明你所拥有的内容以及它在 Range 范围之外的方式。 但这会创建一个异常,因此你可以调用 .fail 来创建故障。

实际上,你从修改过的方法中返回了该失败,消除了返回约束,因此它可以返回Slip或失败。 如果你从这里以44号致电

my Decker $this-deck .= new;
my UInt $how-many = (@*ARGS[0] // 1).UInt;
say "Cards ", $this-deck.draw( $how-many ).join(" ❦ ");

它会产生这样的错误:

Argument out of range. Is: 44, should be in 1..40

错误消息由其属性的异常组成。 当你实际尝试使用故障时,它将引发它所承载的异常。 从这个意义上讲,它不是在为你买很多东西,而只是在你处理问题的发源地。 但是,有关失败的有趣事情是,你可以通过实际处理它们来避免这种情况。 你将在下一部分中看到操作方法。

11.3. 处理错误

异常或失败形式的错误并不表示程序应停止。 如果只是通知用户或编码者,则可以处理它们。 如果知道的话,甚至可以重试或从中恢复 你正在处理。 可以通过检查变量的定义性来处理故障。 由于失败是 Nil 的子类,因此不会定义它,这与例程返回的适当变量不同,该变量始终会被定义:

my Decker $this-deck .= new;
my $draw = $this-deck.draw: (@*ARGS[0] // 1).UInt;
if $draw.defined == False {
    say "Oops, something went wrong →\n\t", $draw.exception;
} else {
    say "Cards ", $draw.join(" ❦ ");
}

你知道 $draw 将包含返回值或失败。 然后检查是否未定义,即 $draw.defined 是否为 False。 在这种情况下,你知道这是一个失败,并且包装了一个异常,你可以提取该异常。 该异常将字符串化为异常消息,并输出如下内容(参数为44):

Oops, something went wrong → Argument out of range. Is: 44, should be in 1..40

这样,你可以在这种情况下使用自己的错误消息来有效地处理原始异常。 你还可以执行更适合参数的操作,例如使用默认值。 通常,我们谈论的是在处理异常时引发异常并捕获它。 可以在任何级别捕获异常,但是我们通常尝试在最高级别捕获它们。 让我们尝试这个称为 Deckeroo.pm6 的新模块。 仅此方法将更改:

method draw ( UInt $how-many = 1 --> Slip) { if ( ! @!cards.elems ) {
X::AdHoc.new( payload => "No more cards" ).throw }
if ( $how-many > @!cards.elems ) { X::OutOfRange.new( got => $how-many,
range => "1.." ~ @!cards.elems).throw
}
    if @!cards {
        self!_shuffle;
my @draw = gather { for ^$how-many {
take @!cards.pop }
}
return @draw.Slip; }
}

你有两种可能的例外情况,一种例外情况是你将使用 AdHoc,另一种情况则是你的卡片已用完,另一种情况是当请求的卡片数超过卡组中的卡片数时。 你返回是为了限制返回值,因为万一发生异常,你将不通过返回值来处理它,而是通过捕获它来处理它。

主程序将更改为:

use Deckeroo;
my Deckeroo $this-deck .= new; CATCH {
when X::AdHoc { .Str.say }
when X::OutOfRange { say "We don't have that many cards: ", $_.Str; }
default { say "Something has happened: $_"; }
}
for ^(@*ARGS[0] // 1) {
say "Cards ", $this-deck.draw(( @*ARGS[1] // 2 ).UInt ).
join(" ❦ "); }

CATCH 块与给定的块相似:它将具有不同的选项,并在 when 之前加上一个默认选项,如果没有其他选项匹配,则将运行该默认选项。 在这种情况下,两个例外中的每一个都有一个子句。 由于 when 是一个主题分析器,因此实际的异常将包含在主题变量 $_ 中; 你可以直接打印(使用 .Str.saysay $_.Str),也可以简单地将其插入到自己的消息中,从而进行包装。 在这种情况下,你要使用两个参数来调用程序,一个用于绘制次数,另一个用于卡片数量:

> perl6 catch.p6 7 4
Cards 9♦ ❦ 7♠ ❦ 4♦ ❦ 6♦
Cards 8♥ ❦ 5♥ ❦ 10♥ ❦ 5♠
Cards 2♥ ❦ 3♣ ❦ 4♥ ❦ 7♣
Cards 6♣ ❦ 10♣ ❦ 3♠ ❦ 8♠
Cards 1♣ ❦ 2♦ ❦ 5♦ ❦ 3♥
Cards 8♦ ❦ 1♦ ❦ 7♦ ❦ 6♥
Cards 10♦ ❦ 7♥ ❦ 1♠ ❦ 8♣

此输出正确,但是不同的组合将触发 CATCH 块捕获的异常。 例如,

perl6 catch.p6 11 4
Cards 10♥ ❦ 7♣ ❦ 3♠ ❦ 4♣
#...
No more cards

这是第一个例外,在你呼叫牌组且没有剩余牌时触发。

>perl6 catch.p6 1 54
We don't have that many cards: Argument out of range. Is: 54, should be in 1..40

在这种情况下,它指示第二个参数超出范围。 也可能会发生一些意外错误:

>perl6 catch.p6 one 2

发生了什么事情:无法将字符串转换为数字:以10为基数的数字必须以有效数字或“。”开头 在“⏏one”中(用 dic 表示) 在这种情况下,异常发生在与 CATCH 块相同的级别。 由于未预见到这一点,因此默认子句被激活。 你可能还想检查是否有东西在运行,如果没问题就让它通过,但如果没用就跳过它。 try 块正是这样做的:它们在其中运行代码,如果发生异常,则将其删除。 将前面的代码包装在 try 块中将实现此效果:

try {
    for ^(@*ARGS[0] // 1) {
say "Cards ", $this-deck.draw(( @*ARGS[1] // 2 ).UInt ).
join(" ❦ "); }
}

如果两个参数都正确,则使用与以前相同的选项运行此代码将正确运行,如果两个参数都没有,则只会执行任何操作。 在上面的代码和以下情况下,try 也可以用作语句前缀:

try for ^(@*ARGS[0] // 1) {
    say "Cards ", $this-deck.draw(( @*ARGS[1] // 2 ).UInt ).join(" ❦ ");
}

由于该块仅包含 for 语句,因此其前面的 try 将影响其中发生的任何事情。

在大多数情况下,使用 X::AdHoc 作为特定应用程序的异常就足够了,但是更好的做法是创建自己的异常类,以便你可以专门处理它们。 你的类将必须继承 Exception

unit class X::Cards::NoMore is Exception;
method message() {
"No more cards left, sorry";
}

仅需要覆盖消息方法即可。 你会将其包含在你的模块中(这次称为 Deckie):

use X::Cards::NoMore;
# @.cards, _shuffle and TWEAK defined as usual method draw ( UInt $how-many = 1 --> Slip ) {
if ( ! @!cards.elems ) { X::Cards::NoMore.new.throw
}
    # Rest will remain the same
}

你可以像以前一样从程序中使用它。 当反复调用 draw 且纸牌已用尽时,所键入的消息将被抛出,如果未被捕获,则将打印特定消息并退出。

11.4. 结束语

在本章中,在以几种可能的方式显示了使用参数调用程序的语法后,你已经了解了异常和失败的产生方式,以及如何处理它们,包括如何创建自己的异常类以在程序中使用。 避免在另一个级别上的错误包括广泛地测试你的代码。 你将在下一章中看到如何做。

12. 与系统交互

使用文件系统,网络以及所有可用的内容 如今,与系统交互并不像以前那么重要 之前。 十年前,大多数语言参考都以 "hello world" 和一个"打开文件"命令开头,而如今,大多数程序运行在通过环境变量(在第11章中已看到)或通过网络(在第10章中看到)进行交互的环境中。 。 尽管如此,知道如何与系统交互仍然是许多程序的组成部分,甚至在我们需要运行以其他语言编写的应用程序时更是如此。 下一个。

12.1. 运行外部程序

运行外部程序意味着知道它们的路径并处理运行使它们运行所需的参数的命令。有两种不同的方法可以做到这一点:

  • 通过内核调用:操作系统内核包括一组可以运行带有参数的外部程序的函数。

通过外壳程序:这些是运行外壳程序的高级调用,并为其提供程序以与其参数一起运行。 前者和后者之间有许多区别,但是你可以将后者视为运行程序的方式与从命令行执行时的方式相同:考虑到环境变量和 shell 设施,例如 shell 扩展,内部命令(例如 Linux shell 中的 echo 或 cd),以及文件系统导航。但是,内核调用不提供此类功能:例如,你需要包括正在运行的程序的完整路径,以及参数的完整列表和扩展的环境变量。 Raku 包括使用这些方法中的任何一种来运行外部程序的不同命令。请参阅表12-1。

命令

类型

动作

run

Kernel

根据需要运行具有输入和输出重定向的外部程序。

Proc.new

Kernel

创建一个捕获与外部程序交互的对象

shell

Shell

使用预定的外壳程序(包括重定向)运行外部程序

qx, qqx

Shell

引用运行外部程序的构造

你将使用我为此目的发布的一个小游戏 App::Game::Concentration。在此游戏中,一副纸牌随机排列成四行,每行13张纸牌。玩家选择2张纸牌,并且如果它们的点数(A,Jack,Queen,King 或数字)相同,则将其淘汰。如果不是,则再次将其转回。名称的集中来自以下事实:一旦获得一张特定的卡片,你就必须记住这一点,即可能出现的另一对卡片出现在另一个选择中。

一旦在游戏中安装了 Zef App::Game::Concentration,就可以通过在命令行中输入浓度来运行它。它显示一个提示,提示格式为应输入两个卡位:一个接一个,另一个由单个空格分隔,行和列由逗号分隔。空白行将结束游戏。请参见图12-1。

你也可以从 https://github.com/JJ/p6-app-concentration 源下载游戏,然后直接从那里运行它,以防万一你想自己破解它。

最简单的播放方法是通过外壳运行它,因为既然已安装,它将位于路径中:

shell "concentration";

这将启动游戏,其行为与从命令行运行游戏时的行为完全相同(如图12-1所示):它将显示提示并等待你的答案。 你可以从命令行运行任何操作,因此这也是有效的:

shell "echo '1,1 2,2' | concentration";

这将在 Linux 中起作用,因为 echo 是一个内部 Shell 命令。 该命令使用管道(|),该管道将命令左侧的标准输出连接到右侧命令的标准输入。 也就是说,好像回声是在集中提示时键入的。 输出将等同于键入一行,然后键入空白行,这将退出程序。 像这样:

row-1,column-1 row-2,column-2 » 6 ♥-J ♥
row-1,column-1 row-2,column-2 »

你提供的输入内容不会显示在命令行中。 提示后显示的两张卡片将是在提示下以交互模式显示的两张卡片。 这也显示了一种通过获取程序输入和输出来控制程序的方法。 可以使用许多命名参数来调用 shell 命令,这些参数必须考虑到这一点。 表12-2中显示了最重要的内容。 表12-2。 调用外部程序的函数的命名参数

参数

默认值

描述

:$in

'-'

捕获输入

:$out

'-'

捕获输出

:$err

'-'

捕获错误输出

:$bin

二进制(非文本)格式

:$chomp

True

消除回车

:$enc

编码

:$cwd

$*CWD

命令会在哪个目录下运行

:$env

环境变量

你可以使用此程序复制上述程序的结果,该程序将获取程序的输入流:

my $concentration = shell "concentration", :in;
$concentration.in.put("1,1 2,2");

shell 命令返回一个 Proc。 这种类型具有有关外部过程的信息。 实际上,你可以通过构造函数与系统进行交互。 由于你要创建一个从程序到正在运行的外部程序的管道,因此该变量将包括该管道。 使用 :in 作为 shell 的参数表示你将建立从你到程序的输入流。

你可以使用变量的 in 属性来访问该流,该属性的作用类似于输入/输出流。 你可以使用 put(也可以说并打印)对其进行写操作。 在这种情况下,该程序的输出与上一个程序非常相似,除了你会注意到它在打印输出之前退出(包括提示和第一个选择的结果)。

Proc 对象具有表12-3中所示的属性和方法。

方法

类型

描述

in, out, err

IO::Pipe

连接到过程的标准输入,输出和错误输出

exitcode

流程退出代码完成后。 0表示成功退出; 默认值为-1

pid

进程 ID

command

它已经运行的实际命令

shell

启动一个使用 shell 的命令

创建 Proc 会使用几个命令可能需要的一系列属性来初始化对象,但是它还会为你提供与程序进行交互的上述方法:

my $concentration = Proc.new: :in :out;
$concentration.shell: "concentration";
$concentration.in.say("1,1 2,2");
$concentration.in.close;
say "Output is \n\t", $concentration.out.lines().join("\n\t");

在第一行中创建一个 Proc 对象,在这种情况下,你将获取输入和输出。 这是一个空的过程,但实际上你是通过调用 shell 方法来附加游戏的。 和以前一样,你可以将数据发送到标准输入,但是由于你还将与标准输出进行交互,因此你需要关闭该流。 $concentration.out 是另一个 IO::Pipe,在这种情况下,你使用行来读取。 这样,你就可以稍微重新格式化输出,以这种方式打印:

Output is
row-1,column-1 row-2,column-2 » K ♠-9 ♣
row-1,column-1 row-2,column-2 »

但是有趣的是,你可以通过这种方式控制与输入/输出绑定的控制台程序,并与之交互。 使用 run 与使用 shell 相似,不同之处在于,不涉及任何 shell:

my $cal = run "cal", "1965", :out;
say "The whole calendar for 1965\n\n", $cal.out.lines().join("\n\t");

你以该程序的名称和所需的参数调用 run,在本例中为 1965。 如果仅以略微不同的方式构架输出,则将捕获输出。 以与以前相同的方式,你将从管道中读取命令输出的行,并在它们之前打印一个制表位。 如果你需要以较低的开销运行程序并且不使用 Shell 功能,则运行可能是一个更好的选择。

12.2. 输入和输出

处理文件的最简单方法是让 Raku 为你打开文件。 如果在命令行中传递多个文件的名称(例如,通过 glob),则动态变量 $*ARGFILES 可让你直接访问文件的内容:

if $*ARGFILES.path ~~ IO::Special {
    say "No input"
} else {
    $*ARGFILES.lines.elems.say
}

你可以通过以下方式使用此程序:

perl6 argfiles.p6 *.p6

如果没有参数,它将仅打印“无输入" $*ARGFILES 是一个魔术变量,它包含作为参数提供的所有文件的整理内容。 没有它们,$*ARGFILES 将改为从标准输入中读取,它将变成 IO::Special 句柄,指向标准输入STDIN。 你对该行为不感兴趣,因此该程序将简单地退出并显示一条消息。

如果不是这种情况,.lines 将返回一个列表,其中包含打开文件中的所有行; .elems 将对它们进行计数,然后说将它们打印出来。 $*ARGFILES 十分简单明了,可为你提供有关 Raku 中输入输出如何工作的提示。其类位于IO名称空间中。 表12-4列出了最重要的那些。

Is a

描述

IO::ArgFiles

IO::CatHandle

$*ARGFILES 是一个实例

IO::Cathandle

IO::Handle

多个文件的单个句柄。

IO::Handle

单个输入或输出流的对象。

IO::Pipe

IO::Handle

用于程序与外部程序之间的通信。

IO::Special

用于标准输入,输出和错误输出。

IO::Path

Cool

用于以与操作系统无关的方式存储路径。

正如往常一样,你需要先指定一个文件能够与它的工作之前被打开。 该文件通常用字符串描述,但是你需要先将该字符串转换为 Path 才能使用它。IO::Path 包括一系列文件测试以及其他功能。 表12-5中列出了文件测试。

测试

描述

返回或抛出

d

目录?

True/False

e

存在?

True/False

f

文件?

True/X::IO::DoesNotExist

l

符号链接?

True/X::IO::DoesNotExist

r

可读?

True/X::IO::DoesNotExist

rw

可读和可写?

True/X::IO::DoesNotExist

rwx

可读可写可执行?

True/X::IO::DoesNotExist

s

多小字节的大小

True/X::IO::DoesNotExist

w

可写?

True/X::IO::DoesNotExist

x

可执行?

True/X::IO::DoesNotExist

z

大小为 0?

True/X::IO::DoesNotExist

例如,你可以使用此脚本来检查文件本身所在的目录:

say <. Chapter12>.map: { $_ => "$_/x.p6".IO.e };

这将打印类似

(. => True Chapter12 => False)

如果从同一目录运行它。 .IO 是一个很酷的函数,它返回一个 IO::Path,该 IO::Path 是动态创建的,将被检查是否存在。 在上述情况下,它是从同一目录运行的; 如果从上方运行,它将返回相反的结果。

你也可以链接多个 IO 操作:

say <. Chapter12>
    .map( * ~ "/x.p6")
    .grep( { .IO.e } )
    .map( { $_ => .IO.s } )

首先使用 map 创建可能的文件名,然后仅过滤现有的文件名,最后使用 .smethod 计算大小。这将在同一目录中打印(./x.p6 ⇒ 104)。

你已经可以使用文件句柄从中提取信息或对其进行写入。 表12-6显示了可直接在手柄上使用的主要方法。

方法

描述

comb

搜索文件中字符串的出现

lines

返回文件中的行序列

words

返回文件中的单词序列

split

按给定的字符串拆分文件并返回结果列表

spurt

将变量或变量集的内容写入文件

print, say, put

与例程相同的行为,作用于句柄

slurp

读取整个文件

这些方法不需要显式打开或关闭文件。

"/tmp/cards.txt".IO.spurt: 1..10 X~ <♠ ♦ ♣ ♥>;

将一副纸牌打印到指定的文件并关闭。 另一方面,以下代码将打开,读取它并执行梳理操作,以检索文件中存在的所有心形:

say "/tmp/cards.txt".IO.comb: "♥";

如果要使用更复杂的操作或指定特殊的打开模式,则需要打开并最终关闭文件。 表12-7显示了如何以不同方式指定打开模式。

模式

描述

r

默认模式,仅可读开放

w

只写

x

如果文件存在将失败

a

添加到文件末尾

update

读写模式

rw

同上

rx

读/写和独占模式

ra

读/写和附加模式

create

如果不存在则创建文件

append

将追加到文件末尾

truncate

如果存在则将覆盖文件

exclusive

与x相同

bin

用于打开二进制文件

mode

ro==只读,wo==只写,rw==读/写

单字母和双字母模式可以相互组合,也可以与 molong-word 选项结合使用。 但是,结果可能无法预测,因此最好坚持使用不同的模式之一。

my $open-first = "/tmp/cards.txt".IO.open: :mode<wo>;
$open-first.put: 1..10 X~ <♠♦♣♥>;
$open-first.close;

这将重新创建以前的文件,因此在大多数情况下,所有数据都可以通过一次处理或写入,最好是突冲/灌浆。 但是,打开具有更多选项,除了打开模式外,整个列表如表12-8所示。

参数

默认

描述

chomp

True

移除换行符

nl-out, nl-in

操作系统相关

定义将用于换行符的字符

out-buffer

True

创建具有定义字节数的输出缓冲区,或使用False禁用输出缓冲区。 True将使用默认实现

enc

utf8

非二进制文件的编码

例如,你可以使用如下脚本所示的二进制文件:

use LWP::Simple;
constant $filename = "/tmp/camelia.ico";
my $camelia = LWP::Simple.get( 'https://docs.perl6.org/ favicon.ico' );
my $binary-file = $filename.IO.open: :bin, :w; $binary-file.write( $camelia );
$binary-file.close;
say "Written $filename";

此代码将从文档页面下载 Camelia 图标,并将其写入 /tmp 目录中的文件(在这种情况下,请将其更改为 Windows 等效文件)。 你使用的 LWP::Simple.get 不会导出,因此你需要使用其完全限定名称下载文件,然后以二进制和只写模式打开文件,然后使用 .write 方法写入文件。 在二进制内容的情况下,优选打印。 在 Bufs 上写作品(在第5章中已经看到过); 对于二进制内容,get 会方便地返回一个 Buf

关闭文件句柄后,你可以使用 eog 或任何其他实用程序可视化文件。 你还可以使用 read 以二进制模式读取它:

constant $filename = "/tmp/camelia.ico";
my $binary-file = $filename.IO.open: :bin, :r;
my $camelia = $binary-file.read; $binary-file.close;
say "Read ", $camelia.elems, " bytes";

读取方法可以使用多个字节进行限定。 默认情况下,它将读取 65536 字节或文件的大小。 由于 $camelia 是一个 Buf,至少在 Linux 中,其中的元素数量将与文件大小相对应。 最后一行将有效打印与 ls 出现的字节数相同的字节数。

12.3. 结束语

本章介绍了与系统中其他程序进行交互的语法,以及如何写入和读取不同类型的文件,包括图像等二进制文件。 但是,除非经过充分的测试,否则不能说一个好的程序会起作用。 Raku 包含一个 Test 模块作为核心库的一部分。 你将在下一章中对其进行了解。

13. 测试你的模块和脚本

如果没有测试,那就坏了 对于当今的软件开发而言,测试是如此重要,以至于大多数语言在其标准库中至少包括一些基本的测试功能。 这通常以单元测试库的形式出现:单元测试检查单个函数的行为,返回的函数以及它们如何更改全局或局部状态。 对于 Raku ,标准测试库简称为Test。

13.1. 标准测试模块:Test

测试是一个标准模块,但不是核心模块的一部分,因此你需要使用它将其合并到测试中。 它通常仅在测试程序中使用,并且通常以 .t 为扩展名,而不是通常的 .raku

use Test;
sub returns-forty-two( --> 42 ) {};
is( returns-forty-two, 42, "Returns 42");

在这里,你正在测试始终返回42的子例程,并检查是否每次都返回。 你使用的是典型的测试命令。 大多数测试命令采用以下形式

type of test( obtained-result, required-result, test-description)

上面的代码将返回

ok 1 - Returns 42

也是这样的形式

(ok or not ok) (test number ) - (Test message)

从现在起,为简洁起见,我将省略 use Test 行。 如果失败,则输出将采用其他形式。

sub returns-forty-two( --> 42 ) {};
ok( returns-forty-two() == 66, "Returns 42");

在这种情况下,它会打印

not ok 1 - Returns 42
# Failed test 'Returns 42'
# at ../Chapter13-notest/not-ok.p6 line 9

在这里,你使用的是 ok 而不是 is。 在检查是否相等的同时,ok 仅检查作为第一个参数的表达式是否为真,即可以将其简化为 True。 如表13-1所示,还有其他测试可用。

测试

描述

ok, nok

第一个论点是真的或假的

is, isnt

第一个参数是 eq(对于对象)或 ===(对于类型对象); 在第二种情况下被否定。

is-approx

数值差小于预设阈值。

is-deeply

使用 eqv 比较时,结果为 True。

cmp-ok

使用作为第二个参数的运算符比较第一个和第三个参数。

isa-ok

检查第一个参数是否属于作为第二个参数传递的类。

can-ok

检查对象是否具有作为第二个参数的方法。 这作为字符串传递。

does-ok

检查变量是否起作用。

like, unlike

使用正则表达式进行比较。

use-ok

可以使用模块而不会引发异常。

dies-ok, lives-ok

检查代码是否抛出异常。

eval-dies-ok, eval-lives-ok

检查要评估的字符串是否引发异常。

throws-like, fails-like

检查它是否引发异常或返回预期的失败。

在运行任何其他测试之前,应该对每个模块进行尝试和使用:

plan 1;
use-ok( "Deckie", "Can use library");

这会返回

1..1
ok 1 - Can use library

在一开始就添加了概述的测试“计划”。 在这种情况下,你已经表示将通过 Test 的 plan 命令运行单个文件。 在程序结束时,如果你以这种方式运行了不同数量的测试,则 Test 将会抱怨:

1..2
ok 1 - Can use library
# Looks like you planned 2 tests, but ran 1

说出要进行的测试很方便,但是如果该数字是可变的,或者你只想表明测试已经完成,则还有另一种选择:

use Deckie;
my $deck = Deckie.new;
for 1..($deck.cards.elems/2) {
    my $draw = $deck.draw( 2 );
    is( $draw.elems, 2, "Correct number of elems" );
    cmp-ok( +$draw.comb[0], ">", 0, "Figure OK");
}
throws-like { $deck.draw( 2 ) }, X::Cards::NoMore, "No more cards" ;
done-testing;

这将测试你在第11章中创建的类的所有功能。你正在测试绘图是否返回正确数量的元素,以及 如果第一个是数字。 但是该模块会引发异常,因此也必须对其进行测试。 你可以使用类似 throws 的方法,但是在这种情况下,你必须使用一个块,而不是一个调用; 类抛出将捕获异常并进行检查。 该测试将返回类似

ok 1 - Correct number of elems ok 2 - Figure OK
[...]
ok 40 - Figure OK
    1..2
    ok 1 - code dies
    ok 2 - right exception type (X::Cards::NoMore)
ok 41 - No more cards 1..41

首先是一个子测试,该子测试检查代码是否确实引发了异常以及它是否是正确的类型,如果两个都正确,它将显示消息 "No more cards"。 在这种情况下,如果你使用完成测试来指示测试结束,那么测试数量将显示在测试脚本的末尾,而不是像以前那样显示在开始处。 你可以在 plan * 之前进行测试,但效果大致相同。 另外,当两个测试相关或应用于同一对象时,可以将它们分组为子测试:

plan *;
my $deck = Deckie.new;
for 1..($deck.cards.elems/2) {
    my $draw = $deck.draw( 2 );
    subtest {
        is( $draw.elems, 2, "Correct number of elems" );
        cmp-ok( +$draw.comb[0], ">", 0, "Figure OK");
    }, "Testing card hand"
}

这主要具有使子测试作为单个测试出现的效果,因此,如果其中一个测试失败,则整个子测试也将失败。 这个模块中的几个例程并不是真正的测试功能,但是可以帮助描述它们或提供选择性应用测试的功能。 它们在表13-2中描述。

例程

描述 todo, skip, skip-rest

将 $reason 和 $count 作为位置参数; 接下来的 $count 测试(或一项)将被跳过 $reason。 第一种情况将打印 TODO。 skip-rest 跳过剩下的任何测试。

pass, flunk bail-out

通过消息说测试已通过或未通过。 跳过测试并以状态1退出。

diag

13.2. 其它测试模块

测试模块包含基本测试所需的所有内容。 但是,它错过了针对复杂事件或特定需求的测试。 这就是为什么生态系统中有几个模块可以为你提供帮助的原因,其中大多数使用 Test 名称空间。 请参阅表13-3。

模块

描述

Test::Output

测试写入 STDOUT 和 STDERR 的内容

Test::When

仅运行用户请求的测试组,或根据环境变量的值有选择地运行它们。

Test::Mock

创建类的模型,可以检查其调用数量以及如何进行这些调用。

Testo

测试“正确完成”,这是 Raku 测试功能的替代实现。

例如,该程序使用 Testo 测试另一个程序的输出(本章前面介绍的 ok.raku):

plan 1;
my $path = "./ok.p6".IO.e??"./ok.p6"!!"Chapter13/ok.p6";
is-run $path, :out("ok 1 - Returns 42\n"), "Runs test";

这会返回

1..1
    1..3
    ok 1 - STDOUT
    ok 2 - STDERR
    ok 3 - Status
ok 1 - Runs test

这表示它正在运行三个子测试,以检查标准输出和错误以及退出状态,如果三个条件都正确,则通过一次测试。

如果这些是你要测试的东西,或者你要使用它添加到标准 Test 中的任何其他功能(例如 is-eqv ),则可以使用 Testo。

13.3. 测试最佳实践

Raku 中的测试只是使用 Test 模块的程序。关于测试的一系列约定通常得到遵守:

  • 测试位于 t/ 目录中,扩展名为 .t。可选测试或作者测试放置在 xt/ 目录中。它们通常采用一个数字,第一个是 00-load.t,它只是测试模块实际上已经编译,其余的在那之后。你应该按照复杂性的顺序对它们进行编号,以便更简单的方法首先失败,然后在矿井中充当金丝雀,尽早发现错误。

  • 单一测试应使用参数组合来测试每个面向用户(即未隐藏)的功能和方法,以便对所有可能的分支进行测试,并对所有可能的输出(包括异常)进行测试。

  • 测试所有内容,包括独立脚本。为此,将它们划分为一个模块+调用它的脚本,并测试脚本将接收的所有可能参数组合,包括变量值等等。

由于测试只是 Raku 程序,因此你可以像其他任何程序一样运行它们。但是,当你有一组测试时,通常需要一个脚本来运行它们;该脚本通常将隐藏测试的单个输出,生成一份报告,其中包含运行的测试数量以及哪些测试失败。例如,让我们将此程序 deckie-subtests.t 放在主目录之外的 t/ 目录中:

plan 20;
my $deck = Deckie.new;
for 1..($deck.cards.elems/2) {
    given $deck.draw( 2 ) {
        isnt( @_[0], @_[1], "Cards from pair are different");
    }
}

该程序会预先计划你要进行的测试数量,以便将提前停止表示为失败。 你将需要安装 App::Prove6 才能运行测试并获取 此报告。 该模块实现了“测试任何协议”(TAP)的解释器,TAP是一种简单的基于文本的格式,用于报告测试的符合性或失败。 它将安装证明6脚本,你只需键入证明6即可从 t 挂起的目录运行该脚本,获取以下输出:

t/deckie-subtests.t .. ok
All tests successful.
Files=1, Tests=20, 0 wallclock secs
Result: PASS

当你压入母版时,应该自动运行测试。 为了做到这一点,最好的方法是使用 Travis CI 等开放源代码注册一些具有免费层的连续集成系统。 有一个正式的 Raku Travis 配置文件,但是我非常喜欢我的文件,它基于 Docker 容器并且运行速度更快,因为它不需要在运行测试之前下载和编译 Raku :

language:
  - minimal
services:
  - docker
install:
  - docker pull jjmerelo/test-perl6

script: docker run -t -v $TRAVIS_BUILD_DIR:/test jjmerelo/
test-perl6

这是一个 YAML 文件,在概念上类似于 JSON,但语法略有不同。 它是一系列键值对,以语言,服务,安装和脚本键以及数组作为值,每个数组元素前都有一个制表符和一个破折号。 从而,

services:
    – docker

正在声明一对以“服务”为键,并使用 [docker] 这样的值。 这些键的解释顺序是预定义的,并且与文件中的显示方式无关。 “最小”语言表示你将不下载任何特定语言,服务的“ docker”命令其启动docker服务。 完成后,“安装”阶段将下载 jjmerelo/test-perl6 docker 窗映像。 实际上,将通过运行 "script" 键的值进行测试,该脚本通过 -v 在映像内的当前目录中挂载,并仅运行默认测试。

你将需要在主模块目录中将该文件另存为 .travis.yml。 你显然需要注册 Travis 并在你正在运行的存储库中启用它才能继续进行。 当结果的状态(通过或失败)改变时,Travis 将为你运行测试并向你发送消息。

该模块需要存在 META.json 文件。 稍后将看到此文件的整个语法。 目前,只需使用一个虚拟文件,例如:

{
  "description" : "Test",
  "name" : " Raku  Quick Reference examples",
  "version" : "0.0.1"
}

13.4. 结束语

测试是检查程序是否符合规格并免受基础语言或下游库漂移影响的最有效方法。 Raku 将 Test 作为核心库包含在内,还有其他库(例如 Testo )可用于测试程序。 更多的库特定于Web或并发应用程序。

除了知道如何使用 Test 和 Testo 之外,还可以通过证明6自动运行这些测试并收集其结果。 你还可以配置 Travis 通过 YAML 文件为你运行其测试。 大多数其他持续集成服务都运行同一文件或 YAML 文件,因此了解其语法将始终有帮助。

通过测试,你几乎可以准备要在 Raku 生态系统中发布的整个模块。 你将在下一章中了解如何执行此操作。

14. 构建工程

如何设计和创建一个库以将其发布到全世界 尽管在 Raku 世界中总是有很多方法可以执行任何操作,但是如果你要向世界发布包含多个模块,测试和示例脚本的发行版,则有一些约定可以帮助标准安装命令(例如 zef)执行。 正确,并且其他开发人员可以在希望将其分叉用于个人用途或对其进行改进时浏览你的源代码。 但是,每个项目中最重要的是对其进行记录。 Raku 包含用于记录其代码的迷你语言。 接下来,你将了解其工作原理。 本章中的示例将位于其自己的存储库中。 有时它们太长而无法包含在页面中,因此最好在 Apress 网站(www.apress.com/9781484249550)上进行检查。

14.1. 用 Pod6 给代码添加文档

Pod6是普通旧文档版本6的首字母缩写。它是一种特定于域的语言,用于在代码中添加注释和添加文本信息。 在大多数语言中,注释是惰性的,或者如果它们使用某种特定格式,则由外部程序解释。 Pod6是该语言不可或缺的一部分,因此具有独特性。注释将被解析和解释,以便你可以像使用语言语法一样使用内部结构。

Raku 用于嵌入在 Raku 本身中的这些迷你语言的名称是辫子。除文档/注释外,它们还包括引号和正则表达式。 通常,文档迷你语言将在以下情况下进行解释:

  • 文本以 # 开头。如果后面有字母,则将其解释为不属于文档的简单文本。如果后面有另一个符号,它将包含在文档中。

  • 该行中的第一个字符为=或文本被使用该字符的块标记括起来。这始终是文档的一部分,但其实际呈现方式将取决于命令。 此示例包括两种文档:

unit class X::Cards::NoMore is Exception;
=begin pod
=head1 NAME
X::Cards::NoMore - Exception thrown when there are no more cards
=head1 SYNOPSIS
=for code
use X::Cards::NoMore;
throw X::Cards::NoMore;

=head1 DESCRIPTION
Thrown when there are no more cards in the deck.

=head1 METHODS
=end pod

#| Returns the exception message
method message() {
    "No more cards left, sorry";
}

=begin pod

=head1 AUTHOR
JJ Merelo <jjmerelo@gmail.com>

=head1 COPYRIGHT AND LICENSE
Copyright 2018,2019 JJ Merelo
This library is free software; you can redistribute it and/or modify
it under the Artistic License 2.0.
=end pod

这段代码将文档添加到在第11章中创建的 X::Cards::NoMore 异常类。之所以选择该类是因为它是到目前为止你所见过的最简单的类,但是其文档包括你需要了解的所有内容。 默认情况下会分析文档,但是为了使其可视化,你需要编写:

perl6 --doc lib/X/Cards/NoMore.pm6

此命令会将文档呈现为纯文本格式,但也可以用于以其他几种格式呈现文档。 这些格式可以作为生态系统中的模块使用。 请参阅表14-1。

Table 4. 主要的 Pod 转换模块

Pod 渲染器

描述

Pod::To::HTML

生成 HTML 输出

Pod::To::Markdown

生成 markdown 文件

Pod::To::Latex

转换为 LaTeX 文档处理框架

Pod::To::Man

生成能被 Unix 的 man 命令处理的框架

这些模块在安装后即被插入,因此

perl6 --doc=HTML lib/X/Cards/NoMore.pm6

将以 HTML 呈现标准输出。通常,如果已安装模块的名称为 Pod::To::XXX,则将使用 --doc=XXX。默认情况下,它使用系统随附的 Pod::To::Text

让我们再次观察两种文档:Pod 命令的前缀为 =,智能注释的前缀为 | 注释在方法声明之前,并将与方法签名一起显示在输出中。这样,很容易将诸如 Pod6 之类的 markdown 方法与生成的文档中的注释结合在一起。

Pod 命令有四种类型:

  • 块命令:它们使用 =begin=end 指示其跨度。例如,pod 块以 =begin pod 开始,以 =end pod 结尾。

  • 段落命令:它们使用 =for 并在第一个空行结束,或者使用独立的 = 命令(例如 =head1)。例如,代码可以使用 =for 代码,代码从下一行开始。

  • 内联命令:它们对应用的代码使用单个字母和圆括号。例如,I<this> 用于斜体。

  • 元数据:它们使用 ALL CAPS,并且如何使用是特定于实现的。例如,=TITLE 可用于为生成的网页命名(如果以其他方式呈现,则将其忽略)。

为了记录你的模块,上面的示例中显示了基本部分。你可以在同一存储库中查看 Deckie 模块的文档。

14.2. 工程部件和以及如何构建、测试和发布

在创建目录之前,你可能应该花一些时间给它起一个适当的名称。如果要用于生态系统,那么最重要的一个问题就是确保所选名称尚未被使用。主题是否已经在生态系统中并不重要,因为你可能采用不同的方法,或者可能以不同的方式演变。

实际上,只要在 auth 元数据字段中表示的作者不同, Raku 生态系统就可以支持具有相同名称的模块。但是,你应该有充分的理由这样做。

类名和模块名遵循扩展语法,标识符用双冒号分隔。这些名称是任意的,但通常冒号之前的第一个标识符是通用字段(例如 Math 或 Test,或 Web),后跟一些特定名称(例如 Web::Cache)。可能有两个以上的部分。例如,Web 缓存的特定实现可以称为 Web::Cache::DB;例如,Web::Cache::DB。异常通常以X为前缀,并且它们具有与它们所使用的类或字段,例如 X::Cards,这就是为什么在第12章(你将在此处使用)中给出的异常称为 X::Cards::NoMore 的原因。这些名称将帮助所有人找到你的模块。但是,你可以选择一个简单而原始的名称,例如将在此处使用的名称 Deckie。

通常,项目中包含四组文件:

  • 源文件

  • 可安装文件,包括脚本和/或文档

  • 元数据,其中包括模块说明以及其他文件,例如许可证和文档

  • 测试文件

让我们从元数据开始,如果回购托管允许,通常使用存储库创建元数据。存储库中托管的开源文件共有几个文件:

  • 许可证:这是一个通常称为 LICENSE 的文件,位于主目录中。许可证是开发者转让给软件用户的所有权利的写照。有许多开源许可证,但是 Perl 世界更喜欢一个称为 Artistic 2.0 许可证的许可证。它包含在回购中。此许可证的指针通常也包含在源文件中。这就是为什么在上面模块的内联文档中有对其的引用。

  • README.md 文件:该文件包含安装模块的简单说明和少量参考。引用从 Pod 文档自动生成的模块方法的一种方法是使用 Pod::To::Markdown

  • .gitignore:此文件包含 git 通常应忽略的模式和 glob。除了你喜欢的编辑器生成的任何文件外, Raku 还会生成一个 .precomp 目录。包括在一行中。

所有这些文件都很方便并且按常规使用,但是对于每种项目都是通用的。 Raku 模块中的元数据包含在一个名为 META6.json 的 JSON 文件中,并放置在根目录中,例如用于你模块的根目录,简称为 Deckie:

{
  "authors" : [ "JJ Merelo" ],
  "build-depends" : [ ],
  "depends" : [ ],
  "description" : "A card deck",
  "license" : "Artistic-2.0",
  "name" : "Deckie",
  "perl" : "6.*",
  "provides" : {
    "Deckie" : "lib/Deckie.pm6",
    "X::Cards::NoMore" : "lib/X/Cards/NoMore"
  },
  "resources" : [ ],
  "source-url" : "https://github.com/JJ/perl6-quick-reference-chapter14.git",
  "tags" : [ "apress", "games" ],
  "test-depends" : [ ],
  "version" : "0.0.1"
}

此处遵循在 JSON 文件中用作配置的常规键值结构。它必须是有效的 JSON,并且有一些强制性密钥。它们是名称,描述,提供和 perl。前两个用于命名和描述该模块,最后一个表示该模块使用的 Raku 版本。当前只有 6.c 和 6.d 两个; 6.* 表示它将与任何版本一起使用。

Provides 键包含一个数组,该数组具有要包含在库中的模块的名称,在本例中为两个,还包含可以在其中找到它们的文件。 你可以在此处阐明这些路径的事实表明,放置这些文件实际上是任意的。实际上,习惯上将它们放到 lib 目录中,名称的片段变成子目录,如 X::Cards::NoMore 情况所示。 其余键不是强制性的,但对于源 URL 和版本,则非常方便。安装程序 zef 将使用 version 来决定是否安装新版本,并将其与你系统中已经存在的一个来源网址将用于托管位置,通常是 GitHub URL。在这种情况下,不使用依赖,构建依赖和测试依赖,因为此模块不依赖于另一个模块,而应包括它需要工作(或运行测试或构建)的模块。许可证也是如此,许可证应与你包含在存储库中的文件匹配。此字符串遵循 SPDX(软件包数据交换)约定,该约定提供了一种识别每个许可证的独特方法,并且通常在所有现代语言中使用。你可以从 https://spdx.org/licenses 检索整个列表。

此元数据文件在 Raku 模块分发中显示了目录布局中的约定。我已经显示了哪些文件放置在主目录中。除了他们

  • 软件包放在 lib 目录中,因此模块 X::Y::Z.pm6lib/X/Y 目录中。

  • 示例和其他文件(例如数据或文档)放置在资源目录中。这些资源将通过 %?RESOURCES 哈希可用于库(和其他)文件,你可以通过以下方式将它们添加到资源密钥中:

"resources" : [ "examples/deckie.p6" ],

在这种情况下,%?RESOURCES<examples/deckie.p6> 将为你提供该文件的有效安装位置。

  • 你想要的可执行文件需要安装在 bin/ 目录中;同样,它们通常没有扩展。例如,你将此文件另存为随机播放,并且在安装模块后便可以从任何地方访问该文件:

use Deckie;
say Deckie.new.draw( 40 ).join(" · " );
  • 测试文件进入 t/ 目录,如第13章所示。其他测试(不必每次都运行)也称为“作者测试”,放置在 xt/ 目录中。

第13章中详细介绍了如何测试。你将在那里看到的测试合并到一个文件中,但是你需要为模块本身添加一个新测试。 这个测试

use Test;
use Test::META; plan 1;
meta-ok;

将通过运行 meta-o 测试来检查 META6.json 文件是否遵循正确的语法。它还将检查它是否包含必填字段,以及某些字段(如许可证)的格式是否正确。由于测试是按顺序运行的,并且这是应该检查的第一件事,因此该测试将命名为 00-meta.t。Test::META 不属于标准发行版,因此你需要将依赖于测试的键更改为 "test-depends": ["Test::META"],,这表明你仅需要该模块进行测试,而实际上不需要使该模块永久运行。某些模块安装应用程序可能会选择临时添加它或询问它。完成所有这些之后,就可以对模块进行测试了。用于安装模块的相同程序 zef 也用于测试你自己的模块,因为通过测试是下载或从源代码安装它们的必要条件。运行

zef test .

在这种情况下,将运行你的两个测试并产生类似

===> Testing: Deckie:ver<0.0.4>
===> Testing [OK] for Deckie:ver<0.0.4>

zef test . 已经负责将-Ilib添加到正在运行测试的实用程序,并将使用任何可用程序。 如果未安装证明(Perl 5版本),它将使用后备内部 TAP 解析器来编译测试结果。 每次按下主菜单时,自动运行这些测试都很方便; 如果对你的模块提出了一些拉取请求,它们也将运行。 如第13章所述,只需注册 Travis(或其他CI服务)并将 .travis.yml 配置文件添加到你的根目录中即可。 对于每个推或拉请求,你都将获得一个类似于图14-1的报告。

最后,当一切准备就绪时,你可以将其发布到生态系统中,使其(通过 zef)可用于所有 Raku 社区。 根据最新调查, Raku 用户中几乎有一半已发布 生态系统中的一个或多个模块,其主题从数学到胶合到其他程序(例如 git)。 如果对你有用,那么它可能对其他人也有用,并且通过发布它,你可能会自发地进行协作。 像现在这样,你的库已经可以被任何人使用。 只需下载然后运行

zef install .

这将下载所有需要的依赖项,并使它可用于所有 Raku 程序。

通常, Raku 安装在用户空间中,这就是为什么此步骤未在特权模式下运行的原因。我强烈建议你遵循这种做法;但是,如果从某个存储库安装了解释器,则必须以超级用户身份运行该解释器(通过在 sudo 之前或操作系统使用的任何机制进行操作)。 显然,你可能希望将其发布在生态系统中,以便可以按名称查找并下载它作为任何其他库的依赖项。有两种方法可以做到这一点。一种是成为 CPAN 作者,然后通过名为 PAUSE 的界面上载 tar 或 zip 文件。但是,实际上是从 CPAN 团体获得许可的中间步骤,因此让我们使用第二种方法,简称为 Raku 生态系统。请分步执行以下操作:

1.右键单击按钮并选择“获取URL”,如图14-2所示,获取你的 META6.json 的原始URL。这类似于 https://github.com/JJ/perl6-quick-reference-chapter14/raw/master/META6.json,这是存储库名称`+/raw/master/META6.json`。

为了将其添加到生态系统中,你需要在生态系统存储库上创建拉取请求。转到 https://github.com/perl6/ecosystem/blob/master/META.list,然后单击铅笔图标进行编辑。将其添加到你希望的任何位置;这真的无所谓。 3.完成后,请使用“将我的::模块::名称添加到生态系统”主题。无需使用主体。这将向负责接受拉取请求的人(像我一样)发出信号,告知你要将其添加到生态系统中。 4.选择“此提交的新分支并启动拉取请求。”这将创建一个拉取请求(要求我们拉出你刚才创建的文件的该版本),最后单击“建议更改文件”。

基本上就是这样。测试将在你的 META6.json 文件上运行(但是,如果你遵循以上建议,那很好),再加上一些其他检查。请稍等片刻,看看是否有错误,如果发现错误,请予以纠正案子;你需要添加评论,以通知我们你已完成此操作。如果测试通过,通常会有人接受 PR,并且你的模块将对整个生态系统可用。仍然需要将更改传播到生态系统指数,但是它们每两个小时进行一次更新,因此在接下来的两个小时之内,你将能够看到你的模块。键入

zef search My::Awesome::Module

它将显示在那里。 发生这种情况时,请向全世界宣布! 你对社区的贡献将不胜感激。

14.3. 结束语

本章是对将模块发布到 Raku 生态系统中所涉及的命令,配置文件和约定的快速参考。 如果你浏览生态系统,你会发现它们从异想天开变为关键任务,因此随时添加它们。 在下一章中,你将学习如何使用 Raku 最强大的工具之一:Grammar 来分析半结构化文本。 在语法学习中,你将使用正则表达式,它们是构建基块。

15. Grammars

使 Raku 变得与众不同的功能之一,也是处理文本的最强大方法之一

打开文件或文件组,从文件的常规结构中提取信息,以及执行其他操作是典型的脚本编制任务。 每一种现代语言都存在正则表达式,可帮助你匹配字符串中的结构(有时将文档视为一个很长的字符串)。 Raku 使这一概念更进一步:Grammar 收集正则表达式,并且可以应用于诸如协议甚至编程语言之类的复杂结构。

regexes 的语法(有时称为 regexen)在许多语言中都是通用的,尽管 Raku 对其进行了调整以适应其 Unicode 功能并引入了其他优点。你将在接下来的内容中看到。

15.1. 使用正则表达式处理文本

正则表达式是 Raku 中的对象,但是与其他原生对象相同,可以使用字面值创建正则表达式:

say /foo/.^name; # Regex

// 语法声明了一个正则表达式字面值,在这个例子中,它将与该字符串中任何以确切顺序包含字母 f, o, o 的字符串匹配。 这也可以使用引用构造 rx 表示:

say rx/foo/.^name;
say rx«foo».^name;

在这种情况下,如上所示,任何成对的引号或大括号/括号都可以用于使清晰易读。

此外,语法 m// 不会创建对象。 而是直接用于计算主题变量或与之智能匹配的变量是否与正则表达式匹配,

for <foo bar baz> {
    say "$_ matches" if m/foo/;
}

这完全等效于:

for <foo bar baz> -> $m {
    say "$m matches" if $m ~~ m/foo/;
}

如果没有匹配项,智能匹配将返回一个布尔值; 但是,如果实际存在匹配项,它将返回 Match 对象。 如上所示,Match 对象将被布尔化为 True。 正则表达式和 grammar 实际上是 Raku 中不可或缺的一部分,因此,成功匹配的结果还将获得其自己的特殊变量 $/, 该变量存储从上一个正则表达式得到的 Match 对象,即使该对象没有存储在任何地方。 你显然可以将其存储在变量中:

for <foo bar baz> -> $m {
    my $match = $m ~~ m/foo/;
    say $match.perl;
}

这会打印:

Match.new(pos => 3, made => Any, from => 0, hash => Map. new(()), orig => "foo", list => ())

后面跟着两个 Bool::False,显示了 Match 对象的内部结构,它们非常强大。 它们是捕获, 这意味着它们将为匹配结果提供位置以及关联接口。 请参阅表15-1。

Table 5. Match 属性

方法

描述

pos

匹配时的上一个光标位置

from, to

字符开始和结束的匹配

orig

匹配到的原始对象

prematch, postmatch

匹配前后的字符串

但是,正则表达式比只是检查一个字符串是否包含在另一个字符串中更强大。 与其他情况一样(例如,Postionals),可以通过副词来修改其匹配行为。 表15-2显示了可以在匹配表达式中使用的一些副词。

Table 6. 在匹配表达式中使用选定的副词

副词

描述

nth(位置列表)

仅在它们位于指示的位置时匹配。

1st, 2nd, 3rd

匹配字符串的第一,第二或第三次出现。

g, global

捕获所有匹配项,而不仅仅是第一个。

pos(position)

匹配应该发生在特定位置。

例如, 这里:

my $match = "stafoostatic" ~~ m:g:2nd/sta/;
say "From {$match.prematch} to {$match.postmatch}";

你使用了两个副词, 其中一个副词考虑了所有匹配项,而不仅仅是第一个副词 :g,然后你通过 :2nd 副词(实际上等效于 :nd(2))表明你感兴趣的匹配项是第二个。 这将打印:

From stafoo to tic

表明这实际上匹配的是第二个位置。 请注意,你可以按任何顺序使用副词。

你只能匹配单个字符串。 但是,正则表达式是表达其结构的完整语言。 这些指令与文本重复有关。 请参阅表15-3。

Table 7. 量词和其它正则符号

指令

描述

.

代表任何字符。

?

指示它可以出现或不出现的 Postfix。

*

Postfix的“将会出现很多次,或者可能不会出现。”

+

Postfix的“肯定会出现很多次。”

""

引用将再次表示实际的字符串,包括空格和符号,因为在正则表达式中空格无意义(除非另有说明)。

让我们看看这是如何工作的:

say "foo baz bar" ~~ m/ fo+" ba".?" ba"r* /; # ⌈foo baz bar⌋

这种正则表达式开始显示出强大功能,但当然也使正则表达式声名狼藉。 乍一看,它们看起来像是线路噪声。 但实际上,仅需几个指令,你现在就可以使用各种字符串结构。 为了清楚起见, Raku 添加了不重要的空格,在这个例子中,你可以将斜杠与实际正则表达式的开头和结尾分开。 上面的其他量词在此表达式中使用,甚至与 .? 一起使用。 只是表示在字面值 " ba" 之后(或在前面有空格)是否可以存在单个字符。 结果由选择的日式引号引起来,因为这是 .gist 方法在 Match 情况下的行为。

另一组符号允许你匹配替代项或连词,也可以使匹配停止而不是贪婪。 请参阅表15-4。

Table 8. 备选,节俭量词和表达式连接

符号

描述

?

节俭量词; *或+之后的字符将停在第一个使真实的字符上。

[]

在视觉上或语法上将替代项或正则表达式集分组。

|

表达替代方案。 将与第一个匹配。

|

, <>

最长的选择:将匹配最长的。 引号之间的表达式将充当此运算符。

&, &&

在这些例子中:

say "foo baz bar" ~~ m:g/ [fo+ | ba.] /;          # (⌈foo⌋ ⌈baz⌋ ⌈bar⌋)
say "foo bar baz" ~~ m/ fo.+?b /;                 # ⌈foo b⌋
say "10 ♥" ~~ m/ ..? " " "♥" | "♠" | "♣" | "♦" /; # ⌈10 ♥⌋

你使用其中一些。 请注意,第一个使用了包围的方括号,但最后一个使用了方括号。 如果第二场比赛不算节俭,那它本该贪婪地匹配“ foo bar b”。 但是,大多数时候,你对匹配的兴趣不大,而是对字符串的特定部分的提取很感兴趣。 你需要通过括号指出你对正则表达式的意图:

my$card= "10♥"~~m/(..?)"" ("♥"|"♠"|"♣"|"♦") /;
say $card;
.say for $card.list;

在这种情况下,你成功的匹配项已分配给 Math 变量。但是,此匹配将包含比以前更多的信息,因为它将有效地托管匹配操作的结果。当说出要点时,它将打印匹配的字符串以及单个捕获的每一个。捕获列表也可以通过 .list 方法获得;并且这些捕获中的每个捕获也将是一个 Match,如打印它们的时髦方式所示。

还记得特殊的 Match 变量 $/ 吗?该结果还将托管结果并将其作为列表提供,因此说 $/[0] 将打印第一个捕获结果。此外,这些捕获将创建一组以 $0 开头的特殊变量。 $0 将再次打印相同的结果。

但是,当捕获数量很多时,最好为它们命名,以便可以按名称而不是按顺序引用它们。这就是为什么 Match 用作捕获来利用其位置和关联质量的原因。

my $card = "10 ♥"
~~ m/ $<value> = (..?) " " $<suit> = ("♥" | "♠" | "♣" | "♦") /;
say $_.key => $_.value for $card.hash;
say "$<value> of $<suit>";

命名捕获具有自己的语法,其中包括尖括号; 它们被直接插入正则表达式中,位于要捕获的子正则表达式之前。 虽然 Position.captures 将在 Match.list 中可用,但它们将在 Match 中可用。 哈希。 但是请记住, $card 是 Match,因此是 Capture,因此捕获的字符串将按名称提供:例如 $card<value>$/<suit>

有一些经常重复的模式,甚至是经常分组的某些字符。 正则表达式还包括一个转义字母来代表字母组。 表15-5列出了一些。

Table 9. 转义字符类

字符类

描述

\s, \S

空格,以及任何非空格。

\d, \D

数字和非数字。

\w, \W

你可以在单词中找到的字符,反之亦然。 字母数字字符被认为是“单词”。

\t, \T, \v, \V, \n, \N, \h, \H

制表符,垂直空格,回车符,水平空格(以及相反的空格)。

让我们使用以下字符类重写前面的表达式:

say "3 ♣" ~~ / (\d+) \s+ (\W) /;

现在更加清晰了,而且比它是。 在正则表达式中,你不仅需要匹配并提取正确的子字符串,还需要避免误报。 在这种情况下 将避免匹配 " AA♣" 之类的内容。 但是,现在有了新的不精确之处:\W 可以匹配任何非字母数字字符。 你可以使用 Unicode 字符属性(使用 <:property> 语法)更精确地表达字符类。 它们有两种风格:字符属性和类别。 字符属性指的是脚本和块的类型,这两个大类是可用于检查字母。 你可以将 \W 替换为 <:Block('Miscellaneous Symbols')>。这意味着你要学习很多,因为它实际上比枚举这四种西装要长。 使用 <:So>(对于Other_Symbol常规类别)将更短,但是在该类别中仍然有更多的符号。 最终,你为

say "10 ♦" ~~ / (\d+) \s+ (<[♥ ♠ ♣ ♦]>) /;

<[]> 允许你直接包含字符类别; 像往常一样,不考虑空格。 可以使用任何字符文字,包括 \x\c 规范,甚至可以使用算术来添加或减去类别。 例如,这将包括除拉丁数字外的所有数字:

my $non-latin-digits = rx/ <[\d] - [0..9]>+ /;
for <٨٦ 33> {
    say $_ ~~ $non-latin-digits;
}

这将打印 ⌈٨٦⌋ 第一次迭代,而第二次打印 Nil,因为两个拉丁数字将不匹配表达式。 另请注意,你已通过引用构造 rx 重用了表达式。 你要进行算术运算的类别都放在方括号中。 –的使用与数字表达式的含义相同:(所有字母的)所有数字,排除或减去10个拉丁数字。 整个构造的末尾都有一个+,表示它可能出现一次或多次。

正则表达式的最后一个功能是替换字符串的一部分,这是一种数据整理操作,通常由脚本语言(例如 Raku )执行。替换使用 s 作为命令和以下语法:

$_ = "A ♥,A ♣";
s/","/█/;
.say;

这只是用逗号代替了块字符。 由于字符串是不可变的(它们是文字),因此s命令需要对变量进行操作。 你只需将字符串作为主题变量的别名,默认情况下,s 将对其起作用。 .say 也将作为方法应用于主题变量,这样将打印 A♥█A♣。

到目前为止,你一直在使用空格作为分隔符,但是有很多不同的方式来结束一个单词。 单词有多种可能的分隔方式(例如标点符号),但有时你只想知道单词的开始或结束位置。 正则表达式也可以解决此问题,表15-6中显示了边界运算符。

Table 10. 边界运算符

边界运算符

描述

w>, <?wb>, <!wb>

任何单词边界; (最后)不是单词边界。

<<, «, >>, »

左右单词边界。

^, $

字符串的开始,结束。

^^, $$

你可以在匹配,提取和替换操作中使用以下运算符:

$_ = "3♠";
s/^«(.+)»(<[♥ ♠ ♣ ♦]>)$/$1$0/;
.say;

如果要提取部分匹配内容以用于替换,则使用前面提到的用于捕获的相同语法。 在这种情况下,你捕获 在字符串 ^«(.+)» 的开头形成单词的几个字符,在字符串的末尾 (<[♥♠♦]>)$ 加上西服符号。 你将它们倒置,将后者放在前者的前面。 这将打印 ♠3。

正则表达式是代码,但在生成匹配项时它们也可以触发代码。 在语句后面插入一个块,每次匹配时都会运行:

"<zipi><zape>" ~~ m:g/ '<' ~ '>' (\w+) { say $/[0] } /;

你可以在此处看到封闭字符串的新语法,该语法在将用于封闭字符串的两个符号(在本例中为尖括号)之间使用波浪号(~)。 将要匹配的实际正则表达式位于此表达式的后面; 因为你也想捕获,所以你写 (\w+)。 在此之后,你将打开一个块,该块将使整个字符串与主题变量匹配,而且与通常的 $/ 匹配。 由于在匹配中,捕获的字符串将是第一个匹配的字符串,因此 $/[0] 将包含刚刚匹配的字符串,并将其打印出来

⌈zipi⌋
⌈zape⌋

15.1.1. 正则表达式作为函数

Raku 是一种功能语言。 这意味着函数是一流的对象,但也可能意味着我们认为是静态数据或复杂数据结构的部分实际上也是函数,并且可以如此运行。 恰当的例子:正则表达式实际上是函数。

my $r = rx/foo/;
say $r.^mro

会打印

((Regex) (Method) (Routine) (Block) (Code) (Any) (Mu))

令人惊讶的是,这使正则表达式成为方法,因而成为可调用的。 目前,这种可调用类型没有参数,但是如果你在智能匹配中使用它(如你所见),或者将其包含在另一个表达式中,它将被调用:

my $suits = rx/<[♥ ♠ ♣ ♦]>/;
say "Q♠" ~~ /^«(.+)»($suits)$/;
# ⌈Q♠⌋
# 0 => ⌈Q⌋
# 1 => ⌈♠⌋

只需将正则表达式的名称插入另一个正则表达式中,即可对其进行调用,就好像它所代表的内容确实存在。 这种表示正则表达式的方式类似于动态声明一个块。 但是例程还有另一种直接声明它们的形式化方法,直接给它们命名:sub,method,或者在这种情况下还包括 regex:

my regex suits { <[♥ ♠ ♣ ♦]> };
say "Q♠" ~~ /^«(.+)»<suits>$/;
# ⌈Q♠⌋
# 0 => ⌈Q⌋
# suits => ⌈♠⌋

语法与将它们声明为 "block"-正则表达式非常相似。 但是,你使用大括号内的正则表达式语法来分隔正则代码。 另外,你可以使用 my 声明其词法范围。 不这样做 会由于不适合某个方法的声明而产生一些隐秘的错误,因此你可以使用my(或我们的,如果要声明它) 与包范围)。 你还使用一种特殊的语法将正则表达式作为另一个子正则表达式包含在内。 <> 已在正则表达式中广泛使用,大多数情况下与其他符号一起使用。 如果以这种方式使用它们,则将假定它所包含的标识符是另一个正则表达式,并将其包含在其中。 你也可以单独使用类似于例程的正则表达式,但是由于将其用作对象而不是调用它,因此请在前面加上 & 符号。

say "♣ ♦" ~~ &suits; # ⌈♣⌋

既然是例行程序,你可以这样称呼吗? 奇怪的是,你可以。 但是请记住,这是一种方法,你需要在对象上调用方法,或者使用它们将用作自我的任何东西作为参数。 正则表达式将用作自我的什么? 毫不奇怪,$ /或任何 Match 对象。 如果你在上述声明后立即致电,你将获得

say suits( $/ ); # ⌈♦⌋

也就是说,它将继续检查它离开的位置上方右上方的字符串,并返回下一个匹配项。 这就是为什么 Match 对象存储上次搜索停止的位置,然后在开始的位置再次将其拾取的原因。 这些正则表达式方法中的一些是预定义的,并且包括常规模式。 其中一些如表15-7所示。

Table 11. 表15-7. 主要字符类

字符类

描述

<punct>

ASCII 中不包括的标点和符号

<graph>

<alnum>(\w)和 <punct>

<same>

仅在两个相同的字符之间匹配

<print>

<graph>和 \s(又称为<space>)

<ident>

匹配 Raku 标识符

<xdigit>

十六进制数字

<upper>, <lower>

大写和小写字符

正则表达式使用一种称为回溯的机制,这基本上意味着它们从左到右解释正则表达式,扫描字符串并尝试尽可能匹配每个正则表达式运算符。 与运算符合作后,他们将继续进行下一个操作,但是如果该新运算符引入了新的可能的匹配项,则他们会回溯,从第一个删除一些字符,然后将它们与第二个匹配。 你可以在以下构建正则表达式的示例中的操作中看到这一点:

$_ = "foostastic";

say m/(\w+)/;
# ⌈foostastic⌋
# 0 => ⌈foostastic⌋
say m/(\w+)s/;
# ⌈foostas⌋
# 0 => ⌈foosta⌋

say m/(\w+) s \w ** 4 /;
# ⌈foostast⌋
# 0 => ⌈foo⌋

最完整的第三个表达式实际上将逐步显示,如图所示。 此代码还引入了数量运算符 **,它表示前面的表达式将发生一定次数或一个 Range。 因此,它将首先找到 \w+ 部分,然后贪婪地吞咽整个字符串,如第一步所示。 但随后它将继续进行正则表达式的第二部分,其中包括s。 它将回溯到那里的第一个,即tic之前的一个。 但随后,当它进入 表达式,它将意识到再次花费太多,并且将再次回溯,仅匹配 foo。

如你所料,此过程非常昂贵,对于复杂的正则表达式和长字符串来说更是如此,因为它可能需要回溯多次,最终使字符串多次运行 那需要。 这就是 Raku 引入标记的原因:标记是不回溯的正则表达式。 一旦他们匹配了某个东西,它就会保持匹配:

my token any-letter { <alpha>+ }
my token any-letter-plus-s { <alpha>+ s }
say "foostastic" ~~ &any-letter;
say "foostastic" ~~ &any-letter-plus-s;

第一个标记将匹配字符串,但不匹配第二个标记,因为它将吞噬整个字符串,并且没有比该匹配的s。 第二个 smartmatch 将显示 Nil。 令牌可以与上述正则表达式相同的方式使用,也可以安装在其他正则表达式(或令牌)中。 它们还将捕获到具有相同名称的 Match 键,但是你可以使用以下语法更改键的名称:

my token any-letter { <alpha>+ }
say "foo,bar" ~~ m/<first=any-letter><punct><second=any- letter>/;

你正像其他正则表达式一样使用令牌。 regex 本身将回溯,但是那部分不会,尽管在这种特殊情况下 regex 的行为方式相同。 在这种情况下,输出将非常详细:

⌈foo,bar⌋
 any-letter => ⌈foo⌋
  alpha => ⌈f⌋
  alpha => ⌈o⌋
  alpha => ⌈o⌋
first => ⌈foo⌋
 alpha => ⌈f⌋
 alpha => ⌈o⌋
 alpha => ⌈o⌋
punct => ⌈,⌋
any-letter => ⌈bar⌋
 alpha => ⌈b⌋
 alpha => ⌈a⌋
 alpha => ⌈r⌋
second => ⌈bar⌋
 alpha => ⌈b⌋
 alpha => ⌈a⌋
 alpha => ⌈r⌋

但是将其完整呈现以显示递归性质很有趣 这个正则表达式。 由于将捕获每个例程语法字符类,因此它将在三个级别上生成匹配项:在顶层,在任意字母(和点)级别上,以及在较低级别 <alpha> 上,因为使用了该字符类 从任何字母。 它看起来像一棵解析树,的确确实像一棵树,但是已经表明 Raku 正则表达式具有将字符串分解为其中描述的结构的能力。 此外,还有任意字母和你给它指定的名称的键,第一和第二。 但是,拥有这些别名使你可以直接使用它们,例如打印 $<second> 或在替换中使用它们。 或将它们提取为令牌。 功能正则表达式的第三种类型(规则)是令牌,其中的空间很大。 可以使用 :sigspace 副词在正则表达式中启用此功能,但是它很常见,以至于拥有自己的名称:

my token word { <alpha>+ }
my rule comma-separated {<word><punct> <word>}
say "foo, bar" ~~ &comma-separated;

单词 token 用于建立逗号分隔的规则,然后用于解析字符串。 结果将与预期的一样,该匹配将提取键为单词的两个单词,类似于在编程语言中提取两个标记。 这就是为什么它们被称为令牌的原因。 但是,此规则是在相同的词法范围内使用已定义的正则表达式构建的,你正在使用它来解析字符串,就好像它是语法一样。 你可以在接下来将要看到的语法中更系统地进行此操作。

15.2. 使用 Grammar 构建文本处理器

语法是解析器,在相同的词法范围内包括多个相互依赖的正则表达式。 与将方法组成类的方式相同,将正则表达式(它们是方法,并包括标记和规则)组成语法,以创建处理字符串的单一,复杂且有状态的方式。 语法的声明与类的声明类似(这是它们的意思):

grammar Game {
    token TOP    { <player> \s+ <action> \s+ <card> }
    token card   { [ <[1..9]> | "10" | <[AJQK]> ] ["♥" | "♠" | "♣" | "♦"] }
    token action { <alpha>+ }
    token player { <upper><alpha>+ }
}
say Game.parse("Alice plays K♠");

Game.parse 将返回一个 Match,它将如下所示:

⌈Alice plays K♠⌋
 player => ⌈Alice⌋
  upper => ⌈A⌋
  alpha => ⌈l⌋
  alpha => ⌈i⌋
  alpha => ⌈c⌋
  alpha => ⌈e⌋
 action => ⌈plays⌋
  alpha => ⌈p⌋
  alpha => ⌈l⌋
  alpha => ⌈a⌋
  alpha => ⌈y⌋
  alpha => ⌈s⌋
 card => ⌈K♠⌋

语法是使用关键字语法声明的,并且由于它们是类,因此通常在声明中使用大写。 它们由一系列正则表达式组成,这是它们的方法,其中最重要的一个是 TOP,即提交的字符串将与之匹配的正则表达式。 由于令牌在处理字符串时比正则表达式函数更有效(因为它们不会回溯),因此传统上语法是由令牌和规则(也属于regex-as-method)填充的,但是如果你愿意或自己使用,也可以使用正则表达式 语法需要它。 在这种情况下,你不需要使用作用域声明符 my,因为实际上,这些标记在 Game 类中得到了隐式的 "has"。

语法除了解析外,还有两种其他方法:parsefile和subparse。 后者用于将语法应用于字符串的一部分,而不是整个字符串:

grammar G {
    token TOP { <alpha>+ }
}
say G.parse( "abcd" );
say G.parse( "abcd3" );
say G.subparse( "abcd3" );

constant filename = "/tmp/letters.txt";
spurt( filename, "foostastic");
say G.parsefile( filename );
unlink filename;

这是一个简单的语法,只有一个标记。 它需要解析整个字符串,如第一句话所示。 第二个将失败,但可以使用 subparse 进行工作。 第二块显示了 parsefile 方法如何直接在文件上工作并匹配其内容。 由于语法是类,而标记是方法,因此你应该能够对其进行参数化,并以与其他任何类相同的方式使用它们。 主要区别在于你将仅使用类,而不使用实例方法。 作为类的文法永远不会被实例化。 因此,你可以使用 unit 作为声明将它们放在自己的文件中。 称为 Game.pm6。

unit grammar Game;
token TOP ($separator) { <hand>+ % $separator }
token hand { <player> \s+ <action> \s+ <card> }
token card { [ <[1..9]> | "10" | <[AJQK]> ] ["♥" | "♠" | "♣" | "♦"] }
token action { <alpha>+ }
token player { <upper><alpha>+ }

它是先前语法的演变,但是你在顶部添加了另一个标记,该标记用于分析要由分隔符分隔的游戏动作。 你可以通过以下方式在任何脚本中使用它:

use Game;
my $game-desc1 = "Alice plays 7♥,Bob plays 8♠";
say Game.parse( $game-desc1, :args(( ",",)) );
$game-desc1 ~~ s/","/|/;
say Game.parse( $game-desc1, :args(( "|",)) );

第一句话是你要解析的字符串。 当你在使用字符串作为参数进行解析之前进行调用时,你现在需要告诉它TOP的参数将是什么。 它们必定是一个列表,这就是为什么要在“,”上添加,创建一个单元素列表并将其处理为命名参数 args 的原因。 此参数将绑定到语法中的 $separator。 你可以使用替换为字符串赋予另一个分隔符,然后使用此新的分隔符再次调用语法。 在两种情况下,结果都是相同的:会有一个匹配项。 你可以如上所述直接打印,也可以这样渲染:

my $match = Game.parse( $game-desc1, :args(( "|",)) );
for $match<hand>.list -> $hand {
    say "Playing→ $hand";
}

每个语法标记都将在Match对象中获得一个键,并且你可以使用Match作为哈希值(实际上是Capture)来获取它们的列表。 这将打印

Playing→ Alice plays 7♥
Playing→ Bob plays 8♠

Raku 的其他机制(例如多个时间表)在语法中也派上用场。 它们用于使不同的字符串触发相同的令牌。 你可以在此模块中使用一个名为 Cards.pm6 的模块:

token TOP ($separator = ",") { <hand>+ % $separator }
token hand { <player> \s+ <action> \s+ <card> }
token card { [ <[1..9]> | "10" | <[AJQK]> ] ["♥" | "♠" | "♣" | "♦"] }
proto token action {*}
token action:sym<plays> { <sym> }
token action:sym<draws> { <sym> }
token action:sym<wins> { <sym> }
token player { <upper><alpha>+ }

它与上一个类似,不同之处在于你使用了多个时间表进行操作,并且还为 $separator 指定了默认值,因此你不必每次都用args调用它。 你需要像往常一样先通过 proto 声明令牌的名称。 但是,然后,语法不使用相同的名称和不同的签名来标识将要调用的代码,而是使用:sym<string> 来选择将要使用的特定标记。 该字符串由 multi 中的 <sym> 表示。 因此,第一个动作将在找到“播放”时触发,第二个动作将在找到“绘制”时触发,还有 <*>,它将在其他情况下触发。 这样使用

my $match = Cards.parse( "Alice plays 7♥,Bob draws 8♠, Cara wins A♦" );
for $match<hand> -> $h {
    say "Action→ $h<action>";
}

你将为每只手提取将要打印的动作部分。 令牌可以独立使用,这在你需要 分别调试它们: 假设 use Cards; 从现在开始,如果程序中使用 Cards 类。

say Cards.parse( "Alice plays 7♥", :rule<hand> );
say Cards.parse( "wins", :rule<action> );

请记住,规则,令牌和正则表达式有时在此处可互换使用。 即使语法是由标记组成的 甚至正则表达式),在这种情况下,我将按规则引用它们。 在第一种情况下,手令牌将应用于字符串。 在第二种情况下,它将是多令牌操作。 除其他事项外,然后显示的语法是正则表达式的集合,你还可以使用它们来检查子字符串或独立字符串。 语法的真正威力在于,除了检查和解析复杂的结构外,你还可以根据匹配的内容采取措施。 对于编程语言, 该操作将运行该语句; 在其他情况下,它将解释已解析的文档并创建另一个文档。 可能的动作仅受语言本身的约束。 绑定到语法的类称为动作,它们只是包含原始语法中标记方法的类。

这是最简单的语法操作之一:

unit class Cards-Action;
method TOP ($/) { make ~$/ }

语法操作中的所有方法都将$ /作为匹配对象作为参数。 你可以使用任何其他参数,但其行为方式会稍有不同,唯一的区别是你不能再将make用作子例程,因为它以这种形式直接作用于$ /:

method TOP ($match) { $match.make: ~$match }

你要做的就是打印正确的匹配项; 这个 将包含在 $/ 中,并在其前面加上 ~ 对其进行字符串化。 但是重要的是 make 命令。 此命令将有效地产生输出,并将其插入到匹配的产生属性中。 其余令牌默认情况下会进行处理,也就是说,它们只产生一个 $/ 并将传递到下一个级别。 你现在可以通过以下方式使用此操作:

use Cards;
use Cards-Action;
my $to-parse = "Alice plays 7♥,Bob draws 8♠,Cara wins A♦";
my $match = Cards.parse( $to-parse, actions => Cards-Action.new );
say $match.made;

$match 变量将包含你的常规匹配项; 但是调用 .made 方法将返回语法动作产生的结果。 在这种情况下,它将简单地重新打印成功匹配的字符串。 通常,make 会生成一个字符串,可以使用 .made 在更高级别上对其进行检索。

你可以(并且应该)为每个令牌创建动作。 如果需要,这可以让你忽略其中的一些。 例如,

unit class Cards-Who;
method TOP ($/) { make join("", $/<hand>».made ) }
method hand( $/ ) {
    make ~$/<player> ~ ' → ' ~ ~$/.<card> ~ "\n"
}

在此脚本中使用时,

my $to-parse = "Alice plays 7♥,Bob draws 8♠,Cara wins A♦";
my $match = Cards.parse( $to-parse, actions => Cards-Who );
say $match.made;

会打印

Alice → 7♥
Bob → 8♠
Cara → A♦

你将从下至上阅读语法操作,因为这是字符串解析的方式。你只提供了手动和 TOP 操作,但这足以从初始字符串生成报告而无需太多代码。 手动操作相对简单。你将玩家和纸牌的结果,它们是每只手的一部分,并添加了一些格式设置,以便将它们打印到一行上。但是,你可以使用 make 将生产的产品添加到每只手的比赛中,也就是说,每场比赛现在除了其通常的字段外,还将包含将生产该手牌的那串产品。

晋升为 TOP,该比赛将包含以下所有已匹配的内容,包括手动比赛的列表。但是现在,列表中的每个元素都将包含先前操作中生成的内容。 $/<hand>».made 将通过 » 运算符在via的每个元素上调用 .made。你加入结果行,并调用 make 使其在返回的匹配中顺利进行。

你对该脚本所做的唯一更改是,你正在使用该类而不是实例来调用操作。实际上,在这种情况下使用一个或另一个都不重要,因为该类实际上没有任何实例变量。有了它们,显然会有区别。

15.3. 结束语

正则表达式及其母亲(语法)是非常强大的机制,实际上将特定语言嵌入 Raku 中,使其具有能够处理半结构化文档的功能,并可以从中生成新的字符串或你在其中描述的任何活动。此外,它们很好地集成在整个 Raku概念中:正则表达式是例程,语法是类。因此,它们是一流的公民,可以模块化地集成到大型应用程序中。此外,语法以与任何其他模块或类相同的方式在生态系统中发布,并带有诸如 Grammar::Common 之类的发行版,其中包括许多有用的功能。此外,此分发实际上包含的是角色,可以轻松地将其混入自己的语法中,从而省去了为诸如 infix 或 prefix 表达式之类的简单事情编写自己的语法的工作。

语法通常被称为 Raku 的杀手级功能。也许是这种情况(以及对Unicode的全面支持),但实际上,它们确实是 Raku 的坩埚,因为使它独特的大多数概念来自正则表达式的角色,在其中起作用。这就是为什么将它们留在书的最后一章。

下一章将由你编写,使用 Raku 用户可用的所有资源,包括其精彩的社区,我们将热情地欢迎你。