我想在 Perl 6 中定义两种数据类型,它们派生自Int
,但同时Int
或彼此不兼容。
例如:
Distance
派生自范围为 0 到 32000 的Int
,以及Offset
派生自Int
,范围从 -32000 到 32000
我希望Distance
、Offset
和Int
的类型在默认情况下是可区分的和不兼容的。
所以(伪Perl 6):
my Distance $d = Distance(12); // ok
my Offset $o = Offset(-1); // ok
my Distance $d2 = $o; // BUMMER!
sub myprint(Int $i) { say $i }
say $d + $o; // BUMMER!
myprint $d; // BUMMER!
myprint Int($d); // ok
等等!我希望Perl 6编译器抱怨如果我试图隐式混合Distance
s和Offset
s。
在我迄今为止读过的书中,没有任何提示如何实现这一目标。问谷歌几天也没有给我任何答案,这是否可能,如果是,如何?
我找到了大约subset
,但这只对类型施加了一些限制,但不会使其与原始类型不兼容。此外,如果在原始类型及其子集中同时满足其限制,则无法将其与原始类型区分开来。
所以我想在这里问一下,是否有人知道这在 Perl 6 中是否可行?如果是,我该怎么做?
好吧,如果你真的希望它们在默认情况下是可区分的和不兼容的,只需将它们完全分开。 您可以定义所需的任何功能。 如果对整数使用"具有"关系(而不是"is a"关系),则很容易将功能委托给该值(在此示例中,我委托.Int
以便您的示例有效):
class Distance
{
has Int $!value handles<Int>;
method new($value where 0 <= * <= 32000) { self.bless(:$value) }
submethod BUILD(:$!value) {}
}
class Offset
{
has Int $!value handles<Int>;
method new($value where -32000 <= * <= 32000) { self.bless(:$value) }
submethod BUILD(:$!value) {}
}
my Distance $d = Distance.new(12); # ok
my Offset $o = Offset.new(-1); # ok
my Distance $d2 = $o; # Bummer! Type check fail
sub myprint(Int $i) { say $i }
say $d + $o; # Bummer!, can't add those objects
myprint $d; # Bummer!, $d isn't an Int, can't print
myprint Int($d); # ok, prints 12, converting with Int
无论您希望Distance
和Offset
具有什么功能,您都必须构建到这些类中,可能委托给$!value
以使其变得简单。
编辑:如果你真的想要你想要的语法my Distance $d = Distance(12);
你可以向Int
类添加一个方法来调用你的构造函数:
Int.^add_method('Distance', method () { Distance.new(self) });
Int.^compose;
我实际上不会建议这样做 - 可能比帮助更令人困惑。 最好鼓励使用标准构造函数。 @raiph还指出了惯用的Perl:
my Distance $d .= new(12);
更新,2021现在有 RakuAST![1]
柯特的回答抓住了池想抓住的错误。这个补充答案是我对池的后续提问的初步探索:
为什么在编译/运行 Curt 的代码时,Rakudo 要等到运行时才报告三个错误中的两个?
在最初的探索结束时,我得出了将 Curt 的代码包装在一个BEGIN
块中。这段代码似乎在编译时报告所有错误(当然,在注释掉每个先前的错误之后,一个接一个)。(单击查看可运行的代码段。[2]
这是一个稻草人答案,是为 Perl 6 核心开发人员设置的。
Curt 代码的初始运行结果为:
===对不起!=== ...调用我的指纹(距离)将永远不起作用...
前导===SORRY!===
表示"编译时">[3]错误。
但是,如果我们注释掉失败的行并重试,我们会得到:
分配给 $d 2 时类型检查失败...
此错误消息是"运行时">[3]消息 - 它不以===SORRY!===
开头。
为什么编译器等到"运行时"才抱怨?
答案似乎在于 Perl 6 默认动态特性和失败代码的组合:
my Distance $d2 = $o; # Bummer! Type check fail
当编译器在"编译时"首次遇到此声明时,此行的my Distance $d2
部分将被完全处理(引入新的词法范围符号$d2
)。但是这条线的=
部分是"运行时"运算符;初始化赋值和相应的类型检查在"运行时"进行。
但是开发人员可能希望在编译时强制发生类型检查,从而发生类型检查错误。现在怎么办?
BEGIN
时间
Perl 6 支持通过相位器执行程序的空间/时间旅行。第一个相位器是"在编译时尽快运行"的BEGIN
相位器,可以像这样使用:
BEGIN my Distance $d2 = $o;
如果使用上述更改重新编译,错误现在会出现在编译时[3]:
===SORRY!=== Error while compiling...
An exception occurred while evaluating a BEGIN...
Type check failed in assignment to $d2...
如果我们现在注释掉最新的故障行并重试,我们会得到:
无法解析呼叫者数字(距离:)...
没有前导===SORRY!===
所以这又是一个"运行时"错误。
重新运行代码,并将失败行更改为:
BEGIN say $d + $o;
收益 率:
0
12
在 Stdout 和 Stderr 上,我们得到:
Use of uninitialized value of type Distance in numeric context...
Use of uninitialized value of type Offset in numeric context...
呵呵。不仅没有编译时错误,也没有运行时错误!(运行时警告可能会泄露游戏有关0
的信息。因为声明$d
和$o
的my...
行没有前缀BEGIN
,这些符号在编译时还没有初始化,也就是BEGIN say $d + $o;
行运行的时候。但所有这些都没有实际意义;我们显然倒退了一步。
如果我们将 Curt 的所有代码包装在一个BEGIN
块中会发生什么?
BEGIN { ... Curt's code goes here ... }
BEGIN {
class Distance...
class Offset...
my Distance $d = Distance.new(12)...
sub myprint(Int $i) { say $i }
say $d + $o;...
}
宾果游戏!这些错误现在都像 Curt 的原始代码一样显示出来,但报告似乎在编译时发生(在注释掉每个先前的错误之后,一个接一个)。
脚注
[1] 在我刚刚在柯特的回答下面写的评论中,我刚刚写了[1a]:
嗨@chi,我刚刚注意到您的评论![1a](迟到总比没有好?Raku编译器可以随心所欲地进行静态分析,但是,在您编写此SO 4 年后,目前唯一具有实用意义的 Raku 编译器 Rakudo 目前做得相对较少,因为分析使编译速度变慢,而 Rakudo 的首要任务是快速开始运行代码。也就是说,Rakudo正在发展,在RakuAST工作完成后,可以合理地预期人们会开发更深入的静态分析模块。
[1a]当然,我在看答案之前就写了我的评论!这反映了我的过程;当我意识到有人对我的一个旧答案投了赞成票(或反对票)时,我通常会去检查这个问题,就好像我没有回答它一样——忽略我的答案——作为一种快速恢复速度的方式问/说,而我仍然有一个相对新鲜的观点,不受我最初回答时开发的观点的限制。我完全忘记了我的答案是什么,所以自然而然地被我似乎没有注意到@chi的评论,也没有回复的事实所震惊。此外,正如 jnthn 解释的那样,我的直接反应是/是新的 RakuAST 工作是相关的,因此我的评论,现在我将此信息添加到此答案中。
[2]此处复制的代码,以防 glot.io 消失:
# See https://stackoverflow.com/a/44360950/1077672
BEGIN { # to end of file
class Distance
{
has Int $!value handles<Int>;
method new($value where 0 <= * <= 32000) { self.bless(:$value) }
submethod BUILD(:$!value) {}
}
class Offset
{
has Int $!value handles<Int>;
method new($value where -32000 <= * <= 32000) { self.bless(:$value) }
submethod BUILD(:$!value) {}
}
my Distance $d = Distance.new(12); # ok
my Offset $o = Offset.new(-1); # ok
my Distance $d2 = $o; # Bummer! Type check fail
sub myprint(Int $i) { say $i }
say $d + $o; # Bummer!, can't add those objects
myprint $d; # Bummer!, $d isn't an Int, can't print
myprint Int($d); # ok, prints 12, converting with Int
}
[3]我吓唬引用了许多关于"编译时"和"运行时"的引用,因为它们在 Perl 6 中的含义模棱两可。Perl 6允许用户代码在运行时做任何事情,包括运行编译器,并允许用户代码在编译时做任何事情,包括运行时的事情。因此,从一个角度来看,编译时阶段中可以有一个或多个运行时阶段,反之亦然。但从第二个角度来看,存在编译时阶段,即当您在开发会话期间坐在那里并且刚刚运行编译器时。同样,还有运行时阶段,即当您的代码在"生产中"运行时。我不吓唬引用运行时/编译时,我的意思是参考第二种观点。