Raku 中的 require

有时你必须不择手段地得到一个东西。 如果它是一个文件,你可以使用 Jonathan Stowes 的 URI::FetchFile。 所述模块检查四个模块中的任何一个是否可用,并且采用第一个将 URI 保存为磁盘上的文件。 在他的代码中有一个有趣的触发了 ENODOC 的

$type = try require :: ($class-name);

这里 require 返回一个类型对象,该类型对象由模块声明,并且具有与该模块相同的名字。

检查 roast 这个巧妙的花招并把玩整个动态模块的魔法让我意识到,我们没有真正地在文档中覆盖这一点。 当我尝试处理一个 ENODOC 时我喜欢从一个可编译的例子开始。 这一次,我们需要两个文件

# M.pm6
unit module M;
class C is export { method m { 'method C::m' } };
class D is export { method m { 'method D::m' } };

# dynamic-modules.p6
use v6;
use lib '.';

subset C where ::('M::C');

my C $context = try {
    CATCH { default { .note } };
    require ::('M');
    ::('M::C')
};

dd $context.HOW.^methods.elems; # 246
dd $context.HOW.WHAT; # Raku::Metamodel::ClassHOW
dd $context.WHAT; # M::C
dd ('M::C').^methods.elems; # 57;
dd $context.HOW.shortname($context); # "C"

通过 require 加载的任何符号在运行时将不可用。因此,我们不能进行静态类型检查。使用 subset 和动态查找,我们可以得到一个类型对象来检查。 where 从句将与类型对象进行智能匹配。由于动态查找很慢,所以像下面这样缓存类型对象可能是明智的:

subset C where $ //= ::('M::C');

where 会智能匹配给定的表达式,除非你用 *$_ 手动匹配。 给定的表达式本身不进行智能匹配,因此它将被求值。 由于 $ 是一个状态变量,它的初始化将只被执行一次(对于给定的thunk)。 所以我们最终得到一个状态变量 $,它填充了第一次使用subset时 ::('M::C') 后面的类型对象。 然后,针对该subset的每个约束检查,完成对该类型对象的智能匹配。

// 是 Defined-or 运算符。它返回第一个有定义的的操作数, 否则就返回最后的那个操作数。是短路运算符。

say Any // 0 // 42;   # 0
say Int // Mu // 42;  # 42
say Int // Mu // Str; # (Str)

Any 是类型对象(Type Object), 类型对象是未定义的(unfined)。如果你在类型对象上调用 .defined 方法, 它会返回 False。 你可以用这种方法来找出一个对象到底是不是类型对象:

my $obj = Int; # Int 是类型对象
if $obj.defined {
    say "普通对象, 有定义的对象";
} else {
    say "类型对象";
}

而对于普通对象:

0.defined     # True
False.defined # True
True.defined  # True
"".defined    # True
[].defined    # True
().defined    # True

//= 是多余的,可以被一个简单的 = 替换,但它告诉读者,我期望 $ 是未定义的。 我使用定义或赋值语法,希望读者熟悉它。

> $a = Int  # $a 初始值是未定义的类型对象
(Int)
> $a //= 3  # 等价于 $a = $a // 3
3
> $a        # // 返回第一个有定义的操作数
3

现在我们得到一个类型约束来防止 require 不返回一个匹配我们期望的名称的类型。请注意,我们会检查名称,而不是类型或接口。如果您有机会设计动态加载的模块,您可能需要定义一个角色(甚至可能为空),这些角色必须由动态加载的类实现,以确保您可以真正调用所期望的方法。不只是具有相同名称的方法。

现在实际上我们可以按名字加载模块,并动态解析其中一个类,并从 try 块返回它。因为 M.pm6 定义了一个模块(如在 Raku::Metamodel::ModuleHOW中)作为它的顶层包,我们不能简单地接受 require 的返回值,因为 Module 不是我们在 Raku 中最内省的东西。请注意,require 所加载的符号可在 try-block 外部通过动态查找获得。我不知道会发生什么如果你 go wild 并加载具有相同完全限定名的符号的模块。可能会有龙。

加载任何一组可能安装过也可能没有安装过的模块是一种相当普遍的情况,对我有限的知识,我们的生态系统中还没有那样一个模块。因此,我想挑战一下,写一个运行以下接口的模块。

sub load-any-module(*%module-name-to-adapter);
load-any-module({'Module::Name' => &Callable-adapter});

其中 Callable-adapter 提供了一个通用接口,将模块的 sub 或方法调用转换为用户代码需要的任何内容。有了这样的模块,Jonathan 可以将 URI::FetchFile 减少到 50 行代码。

comments powered by Disqus