【问题标题】:How to derive own distinguish type from Int?如何从 Int 派生出自己的区分类型?
【发布时间】:2017-11-02 09:34:37
【问题描述】:

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

例如:

  • Distance 派生自 Int,范围为 0 到 32000,并且
  • 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

等等!如果我尝试隐式混合 Distances 和 Offsets,我希望 Perl 6 编译器抱怨。

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

我发现了subset,但这只是对一个类型施加了一些限制,但不会使其与原始类型不兼容。此外,如果原始类型及其子集都满足其限制,则无法与原始类型区分开来。

所以我想在这里问一下是否有人知道这在 Perl 6 中是否可行?如果是,我该怎么做?

【问题讨论】:

  • 注意。 SO聊天中有进一步的讨论。 imo 最重要的主题是:类型检查;编译时与运行时;开发人员可以做些什么来控制代码何时运行以及何时发生类型检查;理论上,Rakudo 编译器能做什么或不能做什么;以及它今天的作用。一个方便的起点链接是chat.stackoverflow.com/transcript/message/37424127#37424127

标签: types user-defined-types raku static-typing


【解决方案1】:

如果您真的希望它们在默认情况下可区分且不兼容,只需将它们设为完全独立的类即可。你可以定义你想要的任何能力。如果您使用整数的“具有”关系(而不是“是”关系),则很容易将功能委托给该值(在此示例中,我委托.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);

【讨论】:

  • 感谢您提供此示例!我曾希望有一种更简单的方式,比如在 Ada 中,我可以简单地写 type Distance is new Integer;。但你的答案是我暂时可以选择的解决方法。我可以问一个附录吗?是否也有可能创建我的例如DistanceInt 相同,即Distance(12) 而不是Distance.new(12)
  • @user2242237 仅供参考...我注意到您的问题上有一个static-typing 标签。 DistanceOffset 类型是静态类型。但是编译器会在 compile-time 抱怨您的三个“BUMMER”错误中的 last。 (您可以这样说,因为消息以===SORRY!=== 开头。)当您注释掉时,您会得到“类型检查在分配给$d2 时失败;”这是一个运行时错误。当您将 that 注释掉时,您会得到“无法解析呼叫者数字(距离:);”这也是一个运行时错误。
  • @raiph 关于静态类型检查:是的,我曾希望编译器能检测到我所有的“BUMMER”错误。现在您发现这发生在运行时,我有点失望。你能解释一下为什么编译器在编译时无法检测到这些违规行为吗?
  • 嗨@chi。我才注意到你的评论! (迟到总比没有好?)Raku 编译器允许 进行尽可能多的静态分析,但是,在您编写此 SO 4 年后,唯一的 Raku 编译器目前实用的 Rakudo 目前做的相对较少,因为分析会使编译速度变慢,而 Rakudo 的首要任务是快速启动运行代码。也就是说,Rakudo 正在发展,在 RakuAST 工作完成后,it's reasonable to anticipate folk developing deeper static analysis modules
  • “如果你真的想要你想要的语法my Distance $d = Distance(12);,你可以在Int类中添加一个方法来调用你的构造函数:“......我实际上不会建议这个”。对于任何人阅读此内容,请注意 Raku 的强制功能已得到改进,因为编写了可能相关的答案;重要的是,在大多数情况下不再需要修改 Int 类型 - 使用您现在通常的新功能只需将强制器添加到您的新类型。
【解决方案2】:

2021 年更新现在有了 RakuAST![1]

Curt's answer 捕获 chi 想要捕获的错误。这个补充答案是我对Chi's follow on question 问的初步探索:

为什么在编译/运行 Curt 的代码时,Rakudo 会等到运行时才报告三个错误中的两个?

在初步探索结束时,我将 Curt 的代码包装在 BEGIN 块中。此代码似乎在编译时报告所有错误(当然,在注释掉每个先前的错误之后)。 (Click for runnable snippet.)[2]

这是一个稻草人的答案,为 Perl 6 核心开发者准备的。


Curt 代码的初始运行导致:

===SORRY!=== ...调用 myprint(Distance) 将永远无法工作...

前导 ===SORRY!=== 表示“编译时”[3] 错误。

但如果我们注释掉失败的行并重试,我们会得到:

分配给 $d2 的类型检查失败...

此错误消息是“运行时”[3] 消息 -- 它不以 ===SORRY!=== 开头。

为什么编译器要等到“运行时”才抱怨?

答案似乎在于 Perl 6 的默认动态特性和失败的代码的结合:

my Distance $d2 = $o;              # Bummer! Type check fail

当编译器在“编译时”第一次遇到此声明时,此行的 my Distance $d2 部分将被完全处理(引入一个新的词法范围符号 $d2)。但是这一行的= 部分是“运行时”运算符;初始化分配和相应的类型检查发生在“运行时”。

但开发人员可能希望在编译时强制进行类型检查,从而导致类型检查错误。现在呢?

BEGIN时间

Perl 6 通过phasers 支持程序执行空间/时间旅行。第一个移相器是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

在标准输出上,在标准错误上我们得到:

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]a comment I just wrote below Curt's answer,我刚刚写了[1a]: p>

嗨@chi。我才刚刚注意到您的评论![1a](迟到总比没有好?)Raku 编译器允许做尽可能多的静态分析它喜欢,但是,4 年后你写了这个 SO,目前唯一实用的 Raku 编译器,Rakudo,目前做的相对较少,因为分析使编译速度变慢,而 Rakudo 的首要任务是快速开始运行代码。也就是说,Rakudo 正在不断发展,在 RakuAST 工作完成后,可以预期人们会开发更深入的静态分析模块。

[1a]当然,我写了我的评论之前我看了我的答案!这反映了我的过程;当我意识到有人赞成(或反对)我的一个旧答案时,我通常会检查问题,就好像我没有回答一样——忽略我的答案——作为一种快速恢复被问/说的速度,而我仍然有一个相对新鲜的观点,不受我最初回答时所发展的观点的限制。我完全忘记了我的答案是什么,所以我自然而然地被我似乎没有注意到@chi的评论并且没有回复的事实所震惊。此外,我的直接反应是/是新的 RakuAST 工作是相关的,as jnthn explains,因此是我的评论,现在我将此信息添加到此答案中。

[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 允许用户代码在运行时几乎可以做任何事情,包括运行编译器,并让用户代码在编译时做任何事情,包括运行时的事情。所以从一个角度来看,一个编译阶段可以有一个或多个运行阶段,反之亦然。但从第二个角度来看,有 编译时阶段,即当您在开发会话期间坐在那里并且您刚刚运行编译器时。同样,还有 运行时阶段,例如,当您的代码“在生产中”运行时。我在哪里吓唬引用运行时/编译时,我的意思是指第二个观点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-02
    • 2013-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多