如何使用 Raku 类型系统强制执行不变性?

How to enforce immutability with the Raku typesystem?

我很欣赏不可变数据结构的价值,并且非常喜欢 Raku 内置的许多功能。我特别喜欢 compiler/typechecker 会为我强制执行不可变性——我可能会休息一天或者粗心大意一些东西,但编译器永远不会。

或者至少,我是这么想的。

然而,我很惊讶地看到下面的代码运行时没有类型检查器的窥视:

my Map $m = Hash.new('key', 'value');
say $m.WHAT # OUTPUT: «(Hash)»

查阅文档后,我看到 MapHash 的父 class(因此 Hash.isa('Map') returns True。所以我理解 how(在机械层面上)类型检查成功。但我有两个问题:首先 why 继承像这样工作,其次,如果我真的希望类型检查器保证我的不可变变量保持这种状态,我该怎么办。

关于“为什么”的问题 — 这样构建的地图有何不同? Raku 的其他不可变类型 None 是:Set.isa('SetHash')Mix.isa('MixHash')Bag.isa('BagHash')Blob.isa('Buf') 和(如果算的话)List.isa('Array') 所有 returnFalse。 [编辑:正如 jjmerelo 在下面指出的那样,我将所有这些都颠倒了。我应该说 SetHash.isa('Set')MixHash.isa('Mix')BagHash.isa('Bag')Buf.isa('Blob') 都是 return False。有趣的是,Array.isa('List') returns True,这为 Elizabeth Mattijsen 的说法提供了一些支持,即这是一个历史疏忽——Lists 和 Maps 肯定更多比大多数其他不可变类型更基本的数据类型。]

MapHash 有这种行为有何不同?

关于更实际的问题,我可以做些什么来让类型检查器在这里帮助我更多吗?我知道,在这种特定情况下,我可以写类似

my Map $m where { .WHAT === Map } = Hash.new('key', 0); # Throws the error I wanted

甚至

subset MapForRealThisTime of Map where { .WHAT === Map }

那些真的是最好的选择吗?他们都感觉有点笨拙(where 块可能有运行时成本?)但也许这是最好的方法?

更一般地说,我真正想要的是一种在严格模式下进行类型检查的方法,可以这么说。如果我显式声明变量的类型,我真的希望编译器能够保证该变量具有该确切类型——而不是恰好具有该类型作为父类型的其他类型。有没有我可以采取的更通用的方法,或者我只是要求 Raku 不会提供的严格程度?

What's different about Maps and Hashes that they have this behavior?

就我个人而言,我认为这是一个历史性的疏忽,需要在未来的某个时候修复。

Are those really the best alternatives?

我认为您正在寻找这种情况下的 is 特征:

my %m is Map = a => 42, b => 666;
dd %m;  # Map.new((:a(42),:b(666)))
%m<a> = 666; # Cannot change key 'a' in an immutable Map
%m<c> = 666; # Cannot add key 'c' to an immutable Map

am I just asking for a level of strictness that Raku isn't going to provide

恐怕你是。您可以在 where 子句中使用 =:= 运算符:

subset RealMap of Map where .WHAT =:= Map;
my RealMap $m = Map.new((a => 42)); # works
my RealMap $h = Hash.new((a => 42));
# Type check failed in assignment to $m; expected RealMap but got Hash ({:a(42)})

警告 这可能是 nanswer OP 的初衷。无论如何,在到达最终 nanswer 之前,我会尝试回答所有提出的问题。我猜这使它成为 部分 nanswer。


好的,让我们尝试按顺序回答所有问题。

first why does the inheritance work like that and

好吧,您要将一个值放入类型与该值兼容的容器中。这里没什么大不了的。这是 class hierarchy

A Hash is-a Map,所以在那里分配它没有问题,对吧?您可以声明它 Cool 并且它无论如何都不会抱怨。如果您只是使用 my $m,它也不会抱怨。但无论如何,容器 类型将是它声明的内容,而其内容仍将是 Hash;如果您使用 say $m.^name,它仍然会 return Hash。 我们来看第二个问题:

what can I do about it if I really want the typechecker to guarantee that my immutable variables stay that way

使用绑定,而不是赋值。在这种情况下,您只能在类型完全相同或存在简单强制转换的情况下进行绑定。在这种情况下,您需要绑定到 map

my $m := Map.new('key', 'value');

通过将 Hash 分配给 Map,您并不是真的将其强制转换为 Map;您只是为仍然是 Map 的变量使用兼容的容器。即使绑定:

my Map $m := Hash.new('key', 'value');

还是不强制,还是一个Hash。您需要明确强制(我敢说 map 吗?)

my Map $m := Hash.new('key', 'value').Map;

然后,好吧,它将是不可变的。让我们进入下一个:

Set.isa('SetHash'), Mix.isa('MixHash'), Bag.isa('BagHash'), Blob.isa('Buf'), and (if it counts) List.isa('Array') all return False

嗯,Map.isa("Hash") 也 returns False。这里的模式相同。假设 Hash 被称为 HashMap。一样。继承只能朝着一个方向发展。您仍然可以将 SetHash 分配或绑定到 Set 变量,您仍然需要将其设置为 SetHash 以使其不可变。

is there anything I can do to get the typechecker to help me out more here?

只需分配给 Map 并将分配给 Map 的任何内容转换,或者从头开始声明它。这样不会报错:

my Map $m where { .WHAT === Map } = Hash.new('key', 0).Map; 
# Map.new((key => 0))

但你可以简单地说

my Map $m = Hash.new('key', 0).Map;

More generally, what I'd really like is a way to typecheck in strict mode, so to speak. If I explicitly declare the type of a variable,

好的,我明白你的意思了。 Scala 确实有办法做到这一点,您可以声明值在层次结构中的上下位置;我想这包括完全严格并且只允许类型本身。我不知道这是否是一个错误,而是一个功能。无论如何,我想不出比你提到的更好的解决方案,除非你可能需要为你想要严格类型的每个变量执行此操作,检查,因为它不能被参数化(我认为)。