第十章. 使用模块

Using Modules

声明

本章翻译仅用于 Raku 学习和研究, 请支持电子版或纸质版

第十章. 使用模块

Modules allow you to compartmentalize, distribute, and reuse code. Someone creates a general solution to something, then packages it so you can reuse it in your programs. Sometimes people make these modules available to everyone. You can find some Raku modules at https://modules.raku.org

You don’t have to understand the code inside a module to benefit from its features. You can usually follow the examples in its documentation even if it uses syntax that you haven’t already seen.

模块允许你划分,分发和重用代码。有人创建了一个通用的解决方案,然后将其打包,以便你可以在程序中重用它。有时人们会向所有人提供这些模块。你可以在 https://modules.raku.org 找到一些 Raku 模块。

你无需了解模块内部的代码即可从其功能中受益。你通常可以按照其文档中的示例进行操作,即使它使用你尚未看到的语法。

Installing Modules

zef is one of the Raku module managers. It can install, update, and uninstall modules. It comes with Rakudo Star but you can install it yourself:

zef 是 Raku 的模块管理器之一。它可以安装,更新和卸载模块。它附带在 Rakudo Star 中,但你可以自行安装:

% git clone https://github.com/ugexe/zef.git
% cd zef
% raku -Ilib bin/zef install .

Once you have zef you can install modules. The Task::Popular module installs those most used by other modules:

一旦你有了 zef,你就可以安装模块了。 Task::Popular 模块安装其他模块最常用的模块:

% zef install Task::Popular

You can install a module by name if the author has registered it in the module ecosystem:

如果作者已在模块生态系统中注册了模块,你可以按名称安装模块:

% zef install HTTP::Tiny

You can also tell it to install the code directly from a Git repository:

你也可以告诉它直接从 Git 仓库安装代码:

% zef install https://github.com/sergot/http-useragent.git

% zef install git://github.com/sergot/http-useragent.git

Ensure that you are using the clone URL and not the project page URL.

You can install from a local directory if the module infrastructure is there and there’s a META6.json file. You have to make the argument to zef not look like a module name. This one looks for a module directory in the current directory:

确保你使用的是克隆 URL,而不是项目页面 URL。

如果存在模块基础结构且存在 META6.json 文件,则可以从本地目录进行安装。你必须使 zef 的参数看起来不像模块名。这个在当前目录中查找模块目录:

% zef install ./json-tiny

You can install the modules from the current directory by using the . as the current working directory:

你可以使用 . 作为当前工作目录从当前目录安装模块:

% zef install .
注意

You may find references to panda, an early module installation tool. It’s the old, unsupported tool. zefis the new hotness. Double-check the documentation, though, since the favored tool may change by the time you read this book.

EXERCISE 10.1Install the Inline::Perl5 module by its name. You’ll use that module later in this chapter. Install the Grammar::Debugger module by its repository URL. You’ll use that module in Chapter 17. Find and clone the Grammar::Tracer module repository, change into its local directory, and install it from that directory.

你可以找到对 panda 的参考,这是一个早期的模块安装工具。这是旧的,不受支持的工具。 zef 是新秀。但请仔细检查文档,因为在你阅读本书时,受欢迎的工具可能会发生变化。

练习10.1 按名称安装 Inline::Perl5 模块。你将在本章后面使用该模块。通过其仓库 URL 安装 Grammar::Debugger 模块。你将在第 17 章中使用该模块。查找并克隆 Grammar::Tracer 模块仓库,切换到其本地目录,然后从该目录安装它。

加载模块

You load a module into your program with need. This searches through the module repository looking for a match. If you’ve installed modules with zef they should be in the repository (in the next section I show you how to tell your program to look in other places too):

你使用 need 将模块加载到程序中。这将搜索模块存储库以查找匹配项。如果你已经使用 zef 安装了模块,那么它们应该在存储库中(在下一节中,我将向你展示如何告诉你的程序在其他地方查找):

need Number::Bytes::Human;
my $human = Number::Bytes::Human.new;

put $human.format(123435653); # '118M'
put $human.parse('3GB');      # 3221225472

Loading a module with use does the same thing but also automatically imports anything the module has set to export. This allows modules to define thingys in your current scope as if you’d defined the code there yourself:

使用 use 加载模块会做同样的事情,但也会自动导入模块设置为导出的任何内容。这允许模块在当前作用域内定义东西,就像你自己定义了代码一样:

use Number::Bytes::Human;

This is the same as doing a need and then an import:

这与执行 need 然后 import 相同:

need Number::Bytes::Human;
import Number::Bytes::Human;

Some modules import things automatically and others wait until you ask for them. You can specify a list after the module name that asks for specific imports. The Number::Bytes::Human module uses an adverb for that:

有些模块自动导入东西,有些模块会等到你要求它们。你可以在要求特定导入的模块名称后指定一个列表。 Number::Bytes::Human 模块为此使用一个副词:

use Number::Bytes::Human :functions;

put format-bytes(123435653); # '118M'
put parse-bytes('3GB');     # 3221225472

No matter which way you load it, follow the examples in the documentation (or maybe look in the module tests).

无论你以哪种方式加载,请按照文档中的示例(或者查看模块测试)。

查找模块

When you install a module with zef the module’s filename becomes a digest of that file and is saved in one of the module repositories—this allows several versions and sources of the file to be simultaneously installed. You can see this path with the command zef locate:

当你使用 zef 安装模块时,模块的文件名将成为该文件的摘要并保存在其中一个模块存储库中 - 这允许同时安装该文件的多个版本和源。你可以使用命令 zef locate 查看此路径:

% zef locate Number::Bytes::Human
===> From Distribution: Number::Bytes::Human:ver<0.0.3>:auth<>:api<>
Number::Bytes::Human => /opt/raku/site/sources/A5EA...

As you can see in the output, the module shows up in a cryptically named file. Raku uses several methods to store and retrieve compunits in repositories. This is very flexible but also much more than I have space to explain here. For the most part you don’t need to worry about that.

正如你在输出中看到的那样,该模块显示在一个文件名加密过的文件中。 Raku 使用几种方法来存储和检索存储库中的 compunits。这非常灵活,但也比我在这里解释的要多。在大多数情况下,你无需担心这一点。

注意

The repository system is complicated because it can manage the same module name with different versions or authors. This means it’s possible to store or load old and new module versions simultaneously.

存储库系统很复杂,因为它可以使用不同的版本或作者管理相同的模块名称。这意味着可以同时存储或加载新旧模块版本。

LIB 指令

No matter where the module is, you need to tell your program where to find it. zef uses the repositories that rakuhas configured by default. The lib pragma can add a directory as a repository. You can store plain files in there (that is, unmanaged by Raku). The module name is translated to a path by replacing :: with a / and adding a .pm or .pm6 extension:

无论模块在哪里,你都需要告诉你的程序在哪里找到它。 zef 使用 raku 默认配置的存储库。 lib 指令可以将目录添加为存储库。你可以在其中存储普通文件(即,不受 Raku 管理)。通过将 :: 替换为 / 并添加 .pm.pm6 扩展名将模块名称转换为路径:

use lib </path/to/module/directory>;
use Number::Bytes::Human

This looks for Number/Bytes/Human.pm or Number/Bytes/Human.pm6 in /path/to/module/directory.

You can specify multiple directories:

这将在 /path/to/module/directory 中查找 Number/Bytes/Human.pmNumber/Bytes/Human.pm6

你可以指定多个目录:

use lib </path/to/module/directory /other/path>;

Or specify lib multiple times:

或者多次指定 lib

use lib '/path/to/module/directory';
use lib '/other/path';

Relative paths resolve themselves according to the current working directory:

相对路径根据当前工作目录自行解析:

use lib <module/directory>;  # looks for module/ in current dir

The . works as the current working directory. People tend to do this if the module file and the program are in the same directory:

这个 . 作为当前工作目录。如果模块文件和程序在同一目录中,人们倾向于这样做:

use lib <.>:

You should carefully consider using the current working directory in your library search path. Since it’s a relative location you’re never quite sure where it’s looking. Running your program from a different directory (with a command like the following) means your program looks in a different directory and probably won’t find the module:

你应该仔细考虑在库搜索路径中使用当前工作目录。由于它是一个相对位置,你永远不能确定它在哪里。从不同的目录运行程序(使用如下命令)意味着你的程序在不同的目录中查找,可能找不到该模块:

% raku bin/my-program

It takes a bit more work to figure out the relative directory. Your program’s path is in the special variable $*PROGRAM. You can turn that into an IO::Path object with .IO and use .parent to get its directory. You can use that to add a lib directory at the same level as your program:

找出相对目录需要更多的工作。你的程序路径位于特殊变量 $*PROGRAM 中。你可以使用 .IO 将其转换为 IO::Path 对象,并使用 .parent 来获取其目录。你可以使用它来添加与程序相同级别的 lib 目录:

# random-between.p6
use lib $*PROGRAM.IO.parent;
use lib $*PROGRAM.IO.parent.add: 'lib';

There’s also the $?FILE compile-time variable:

还有 $?FILE 编译时变量:

use lib $?FILE.IO.parent;
use lib $?FILE.IO.parent.add: 'lib';

For this to work you must add the paths to search before you try to load the library. It does no good to tell it where to look after it has already looked!

为此,你必须在尝试加载库之前添加要搜索的路径。告诉它已经看过它在哪里照顾它没有用!

环境

The PERL6LIB environment variable applies to every program you run in the current session. Separate the directories with commas (no matter which system you are using). Here it is in bash syntax:

PERL6LIB 环境变量适用于你在当前会话中运行的每个程序。用逗号分隔目录(无论你使用哪个系统)。这是 bash 语法:

% export PERL6LIB=/path/to/module/directory,/other/path

And in Windows syntax:

在 Windows 语法中:

 C:\ set PERL6LIB=C:/module/directory,C:/other/path 

-I 开关

The -I switch to raku works for a single run of a program. This is handy inside a project repository (a different sort of repository!) that you haven’t installed. You can use the development version of the module from the project repository instead of a previous one you might have installed:

-I 切换到 raku 适用于单个程序运行。这在你尚未安装的项目存储库(不同类型的存储库!)中很方便。你可以使用项目存储库中的模块开发版本,而不是之前安装的模块:

% raku -Ilib bin/my_program.p6

Specify more than one extra directory with multiple -I switches or by separating them with commas:

使用多个 -I 开关指定多个额外目录,或者用逗号分隔它们:

% raku -Ilib -I../lib bin/my_program.p6
% raku -Ilib,../lib bin/my_program.p6

You can see the -I at work if you want to use prove to run Raku module tests. The argument to -e is the interpreter to use (with Perl 5 being the default). You want a raku that looks for the development modules in the current repository:

如果要使用 prove 来运行 Raku 模块测试,可以看到 -I 在工作。 -e 参数是要使用的解释器(Perl 5 是默认值)。你需要一个在当前存储库中查找开发模块的 raku

% prove -e "raku -Ilib"

The $*REPO variable can tell you where Raku will look for modules. These aren’t just directories. The repositories could be almost anything—including other code:

$*REPO 变量可以告诉你 Raku 在哪里寻找模块。这些不仅仅是目录。存储库几乎可以是任何东西 - 包括其他代码:

for $*REPO.repo-chain -> $item {
    say $item;
}

EXERCISE 10.2Create a program to show the repository chain. Run it in several situations using PERL6LIB, -I, and use lib.

EXERCISE 10.2 创建一个程序来显示存储库链。使用 PERL6LIB-Iuse lib 在几种情况下运行它。

Lexical Effect

Loading a module only affects the current scope. If you load a module in a Block it’s only available in thatBlock, and anything it imports is only available in that Block. Outside of the Block the program doesn’t know about the module:

加载模块仅影响当前作用域。如果在 中加载模块,它只在该中可用,并且它导入的任何内容仅在该中可用。在 之外,程序不知道该模块:

{
use Number::Bytes::Human;

my $human = Number::Bytes::Human.new; # works in here

put $human.format(123435653); # '118M'
}

my $human = Number::Bytes::Human.new; # ERROR: not defined here

You’ll get an odd error where your program is looking for a subroutine that has a name that’s the same as the last part of the module name:

你的程序正在寻找一个名称与模块名称的最后部分相同的子程序,你会得到一个奇怪的错误:

Could not find symbol '&Human'

You can limit module imports to just where you need them. If you only need it in a subroutine you can load it there:

你可以将模块导入限制在你需要的位置。如果你只需要在子程序中,你可以在那里加载它:

sub translate-it ( Int $bytes ) {
    use Number::Bytes::Human;
    state $human = Number::Bytes::Human.new;
    $human.format( $bytes );
}

This means that you could load different versions of the same module for different parts of your program. The lib declaration is lexically scoped as well:

这意味着你可以为程序的不同部分加载同一模块的不同版本。 lib 声明也是词法作用域的:

sub stable-version {
    use Number::Bytes::Human;
    ...
}

sub experimental-version {
    use lib </home/hamadryas/dev-module/lib>;
    use Number::Bytes::Human;
    ...
}

This sort of thing is often handy to convert data formats when they change between module versions:

当模块版本之间的数据格式发生变化时,转换数据格式通常很方便:

sub translate-to-new-format ( Str $file ) {
    my $data = do {
        use lib </path/to/legacy/lib>;
        use Module::Format;
        Module::Format.new.load: $file;
    };

    use Module::Format; # new version
    Module::Format.new.save: $data, $file;
}

在运行时加载模块

need and use load a module as the program compiles. Sometimes you don’t know which module you want until you want to use it, or you know that you might use one of several modules but only want to load the one you’ll actually use. You can wait until runtime to load it with require. If the module isn’t there the requirethrows an exception:

needuse 在程序编译时加载模块。有时你不知道你想要使用哪个模块直到你要使用它,或者你知道你可能使用几个模块中的一个但只想加载你实际使用的模块。你可以等到运行时使用 require 加载它。如果模块不在那里,则 require 抛出异常:

try require Data::Dump::Tree;
if $! { die "Could not load module!" }
警告

Even if the module fails to load, the require still creates the type. You can’t rely on the type being defined as a signal of successful loading.

Perhaps you want to check that a module is installed before you try to load it. The $*REPO object has a.resolve method that can find a module from its dependency specification:

即使模块无法加载,require 仍会创建类型。你不能依赖被定义的类型作为成功加载的信号。

也许你想在尝试加载模块之前检查模块是否已安装。 $*REPO 对象有一个 .resolve 方法,可以从其依赖项规范中找到一个模块:

my $dependency-spec =
    CompUnit::DependencySpecification.new: :short-name($module);

if $*REPO.resolve: $dependency-spec {
    put "Found $module";
}

EXERCISE 10.3Write a program that reports whether a module is installed. Try this with Number::Bytes::Human (assuming you installed it so it is present) and Does::Not::Exist (or any other name that isn’t a module).

练习10.3 编写一个报告模块是否已安装的程序。尝试使用 Number::Bytes::Human(假设你安装它以便它存在)和 Does::Not::Exist(或任何其他不是模块的名称)。

插值模块名

You can interpolate a Str where you’d normally have a literal class name by putting the Str inside ::():

你可以通过将字符串放入 ::() 来插入一个通常具有字面类名称的字符串

require ::($module);

Anywhere you’d use the literal class name you can use that ::($module). When you want to create an object but you don’t know the literal module name, you interpolate it just as you did in the require:

在你使用字面类名称的任何地方,你都可以使用 ::($module)。如果要创建对象但不知道字面模块名称,则可以像在 require 中一样进行插值:

my $new-object = ::($module).new;

Not only that, but you can use a method name from a Str by putting it in double quotes. You must use the parentheses for the argument list when you do this:

不仅如此,你还可以通过将其放在双引号中来使用字符串中的方法名称。执行此操作时,必须使用圆括号作为参数列表:

$new-object."$method-name"( @args );

You can use the return value of require. If it was able to load the module that value is the type:

你可以使用 require 的返回值。如果它能够加载模块的值是类型:

my $class-i-loaded = (require ::($module));
my $object = $class-i-loaded.new;

This might work better with a literal name that you don’t want to repeatedly type:

对于你不想重复输入的字面名称,这可能会更好:

my $class-i-loaded = (require Digest::MD5);
my $object = $class-i-loaded.new;

Checking this is a bit tricky. You can’t simply check for the type because that will be defined no matter which way it goes. Check that it’s not a Failure:

检查这个有点棘手。你无法简单地检查类型,因为无论它采用何种方式,都将对其进行定义。检查它不是 Failure

my $module = 'Hamadryas';

try require ::($module);
put ::($module).^name; # Failure
say ::($module).^mro;  # ((Failure) Nil (Cool) (Any) (Mu))
if ::($module) ~~ Failure {
    put "Couldn't load $module!"; # Couldn't load Hamadryas!
}

These aren’t tricks to use frequently, but they are there as a last resort should you need them. Here’s a program that lets you choose which dumper class to use. It uses a Hash to translate the class name to the method name it uses. At the end it merely dumps the only Hash defined in the program:

这些不是经常使用的技巧,但如果你需要它们,它们是最后的手段。这是一个程序,可以让你选择要使用的转储程序类。它使用 Hash 将类名转换为它使用的方法名。最后它只转储程序中定义的唯一 Hash

sub MAIN ( Str $class = 'PrettyDump' ) {
    my %dumper-adapters = %(
        'Data::Dump::Tree' => 'ddt',
        'PrettyDump'       => 'dump',
        'Pretty::Printer'  => 'pp',
        );

    CATCH {
        when X::CompUnit::UnsatisfiedDependency {
            note "Could not find $class";
            exit 1;
        }
        default {
            note "Some other problem with $class: {.message}";
            exit 2;
        }
    }
    require ::($class);

    my $method = %dumper-adapters{$class};
    unless $method {
        note "Do not know how to dump with $class";
        exit 2;
    }

    put ::($class).new."$method"( %dumper-adapters );
}

EXERCISE 10.4Modify the dumping program. Create another subroutine that takes a list of modules and returns the ones that are installed. Use that subroutine to provide the default for MAIN.

练习10.4 修改转储程序。创建另一个子例程,该子例程获取模块列表并返回已安装的模块。使用该子例程为 MAIN 提供默认值。

从 Web 抓取数据

HTTP::UserAgent is a handy module to fetch data from the web. Install it with zef and follow the example:

HTTP::UserAgent 是一个方便的模块,用于从 Web 抓取数据。用 zef 安装它并按照示例:

use HTTP::UserAgent;
my $ua = HTTP::UserAgent.new;
$ua.timeout = 10;

my $url = ...;
my $response = $ua.get( $url );
my $data = do with $response {
    .is-success ?? .content !! die .status-line
}

Once you have the data you can do whatever you like, including reading some lines from it:

获得数据后,你可以随意执行任何操作,包括从中读取一些行:

for $data.lines(5) -> $line {
    put ++$, ': ', $line;
}

EXERCISE 10.5Write a program that fetches the URL you provide on the command line, then outputs its contents to standard output.

练习10.5 编写一个程序,用于抓取你在命令行上提供的 URL,然后将其内容输出到标准输出。

在 Raku 中运行 Perl 5

One of Raku’s original goals was Perl 5 interoperability. Larry Wall said that if the new Raku could run “with 95-percent accuracy 95 percent of the [Perl 5] scripts, and 100-percent accuracy 80 percent of the [Perl 5] scripts, then that’s getting into the ballpark.” This meant, as a goal, that a lot of the current Perl 5 contents of the Comprehensive Perl Archive Network (CPAN) would be available in Raku.

The Inline::Perl5 module allows you to load Perl 5 modules or evaluate Perl 5 snippets from a Raku program. Add the source :from<Perl5> after the module you want to load, then translate the syntax to Raku (so, . for a method call and so on). You don’t have to load Inline::Perl5 explicitly in this case:

Raku 最初的目标之一是 Perl 5 的互操作性。 Larry Wall 表示,如果新的 Raku 能够“以 95% 的[Perl 5]脚本 95% 的准确率运行,并且 80% 的[Perl 5]脚本能够 100% 准确地运行,那么它就会进入可变通范围。”这意味着,作为一个目标,Raku 中将提供 Comprehensive Perl Archive Network(CPAN)的许多当前 Perl 5 内容。

Inline::Perl5 模块允许你加载 Perl 5 模块或计算 Raku 程序中的 Perl 5 片段。在要加载的模块之后添加源::from<Perl5>,然后将语法转换为 Raku(因此,. 用于方法调用等等。)。在这种情况下,你不必显式加载 Inline::Perl5

use Business::ISBN:from<Perl5>;
my $isbn = Business::ISBN.new( '9781491977682' );
say $isbn.as_isbn10.as_string;

You can have Perl 5 code in your program and call it when you need it, dropping in and out of it as you like. Create an object that will handle the Perl 5 code for you:

你可以在程序中使用 Perl 5 代码,并在需要时调用它,根据需要加入和退出。创建一个将为你处理 Perl 5 代码的对象:

use Inline::Perl5;
my $p5 = Inline::Perl5.new;

$p5.run: q:to/END/;
    sub p5_test { return 'Hello from Perl 5!' }
END

put 'Hello from Raku!';

$p5.run: 'print p5_test()';

EXERCISE 10.6Compare the results of the Perl 5 and 6 versions of Digest::MD5 by loading them into the same program. Get the digest of the program itself. You can use slurp to read the entire contents of a file.

练习10.6将 Digest::MD5 的 Perl 5 和 Raku 版本的结果加载到同一程序中。获取程序本身的摘要。你可以使用 slurp 来读取文件的全部内容。

总结

You’ve learned how to find and install modules with zef. You can often simply follow the example in the module’s documentation to get what you want. Before you set out to program something, see if someone else has already done it.

You’re not limited to modules from Raku either. The Inline modules allow you to use code from other languages. If you have a favorite module you might not have to give it up.

你已经学会了如何使用 zef 查找和安装模块。你通常可以按照模块文档中的示例来获得所需内容。在你开始编程之前,看看其他人是否已经做过了。

你不仅限于 Raku 的模块。Inline 模块允许你使用其他语言的代码。如果你有一个喜欢的模块,你可能不必放弃它。

comments powered by Disqus