如何从 Int 派生自己的区分类型



我想在 Perl 6 中定义两种数据类型,它们派生自Int,但同时Int或彼此不兼容。

例如:

  • Distance派生自范围为 0 到 32000 的Int,以及
  • Offset派生自Int,范围从 -32000 到 32000

我希望DistanceOffsetInt的类型在默认情况下是可区分的和不兼容的。

所以(伪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编译器抱怨如果我试图隐式混合Distances和Offsets。

在我迄今为止读过的书中,没有任何提示如何实现这一目标。问谷歌几天也没有给我任何答案,这是否可能,如果是,如何?

我找到了大约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

无论您希望DistanceOffset具有什么功能,您都必须构建到这些类中,可能委托给$!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$omy...行没有前缀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允许用户代码在运行时做任何事情,包括运行编译器,并允许用户代码在编译时做任何事情,包括运行时的事情。因此,从一个角度来看,编译时阶段中可以有一个或多个运行时阶段,反之亦然。但从第二个角度来看,存在编译时阶段,即当您在开发会话期间坐在那里并且刚刚运行编译器时。同样,还有运行时阶段,即当您的代码在"生产中"运行时。我不吓唬引用运行时/编译时,我的意思是参考第二种观点。

最新更新