如何防止 Nil 将容器恢复为默认值?

How to keep Nil from reverting container to its default value?

我正在实现一个简单的链表,为了表示没有下一个节点这一事实,我使用了值 Nil。问题是,当分配给容器时,Nil 将尝试将容器恢复为默认值,这意味着我需要使用容器的默认值或 Any 来确定链表的末尾是否已到达。但是,我仍然想使用 Nil(如果只是为了它的明确意图)并且没有其他值来测试代码中下一个节点的不存在(例如,until $current === Anylast if $current-node === Any;).对于某些上下文:

class Node {
    has $.data is rw is required;
    has $.next is rw = Nil;
}

class UnorderedList {
    has $!head = Nil;
    
    method add(\item --> Nil) {
        my $temp = Node.new: data => item;
        $temp.next = $!head;
        $!head = $temp;
    }

    method size(--> UInt) {
        my $current = $!head;
        my UInt $count = 0;
        
        until $current === Any {          # <=== HERE
            $count += 1;
            $current .= next;
        }
        
        return $count;
    }
   
    method gist(--> Str) {
        my $current-node = $!head;
        my $items := gather loop {
            last if $current-node === Any; # <=== HERE
            take $current-node.data;
            $current-node .= next;
        }
        
        $items.join(', ')
    }
}

答案是:Nil可以是默认值。所以与其说:

has $.next is rw = Nil;

您所要做的就是:

has $.next is rw is default(Nil);

Nil 分配给这样的 rw 属性,将为您提供 Nil

一些选择,其中我认为最吸引人的是顶部,而我最反对的是底部:

  • 使用Node(你的Nodeclass的类型对象)。 这是 jnthn 推荐的选项。 这对所有熟悉 Raku 对已定义值和未定义值的区别的 rakuns 来说都是有意义的,如果您将 Node 类型约束添加到节点变量。 Imo 这将是惯用的解决方案。

  • 使用Empty。可以说,它具有与 Nil 相同的大部分积极因素,但没有我在本答案后面看到和讨论的大部分消极因素。如果你使用 Empty (或 End 或其他),你可以创建一个 subset MaybeNode where Node | Empty 或任何允许向节点变量添加类型约束的东西。 (虽然这样的样板主要用于突出使用 Node 选项的简单优雅的人体工程学。)

  • 使用is default(Nil)设置节点属性的默认值。 这是 Liz 建议的选项。 同样,可以使用 is default(Empty) 作为使用 Node 以外的任何其他选项的一部分。

  • 选择一个新名称。 None 可能是一个糟糕的选择,因为它在许多具有其他语义的语言中具有既定含义。也许 End? (声明为 role End {}。)

  • 声明你自己的 Nil 并在词法上隐藏 Raku 的内置 Nil。我会说那是一件疯狂的事情;如果我要对你的代码进行同行评审,我会反对它;并且会预料到大多数 rakuns 都会同意我的观点;但是你可以.

选项的进一步讨论

根据Nil's doc,它的定义是:

Absence of a value or a benign failure

我觉得这个重载是 Larry 的天才。但最好尊重它的本来面目。这不仅仅是一个超载的语义,而且 明确地 如此。继续文档:

Failure is derived from Nil, so smartmatching Nil will also match Failure. ... Along with Failure, Nil and its subclasses may always be returned from a routine even when the routine specifies a particular return type [even] regardless of the definedness of the return type.

所有 这些语义将在思想上和实际上应用于您的代码,即使您不想要它们。您绝对确定您想要它们吗?请参阅我在此答案下方的评论中对@ElizabethMattijsen 的第一个回复,以简要讨论我认为这可能导致的问题。另一个是人们 阅读 您的代码可能 思考 同样的麻烦。这可以说是不必要的开销。

另一方面,即使在相当复杂的代码中,这样的麻烦(实际的而不是想象的)也不太可能发生。而且您的代码非常简单。因此,对于您提供的场景,我对这个失败方面的关注被夸大了。 Liz 的解决方案简单又好。

在抓手方面,即使对于最复杂的代码,采用最普遍适用的简单解决方案也很好,并让新手尽早接触这些简单和通用的解决方案。您也可能有兴趣了解选项范围。当然,我写的东西可能是错误的,并得到很好的反馈。因此这个答案。

I'd still like to use Nil (if only for its clear intent)

如果代码只是给你看的,那么你看清楚了就由你来决定了。

我认为 Nil 的明确意图对于那些了解 Raku 的人来说是象征着 Raku 的 Nil

Raku 的 Nil 具有我上面引用的特定语义。

如果有人将注意力集中在“缺乏价值”上,您的解决方案将显得非常笨拙。他们关注“良性失败”的程度,并考虑这种语义在整个 Raku 中的使用方式,他们可能会大吃一惊。显然,到达没有下一个节点的节点非常适合“良性故障”和“无价值”。事实上,这就是 Larry 使它们超载的原因。但是 其他代码 也使用 Nil 来表示这些东西,它们可能并不表示“link 列表的结尾”。

类似的混合故事适用于 NodeEmpty,但风格不同。

Node 很有意义,因为它在 Raku 中是惯用的,类型对象是未定义的,你可以键入约束节点变量,它会接受 Node。但它也是一种表示未初始化的东西的惯用方式。你真的想要那个超载的语义吗?也许还好,也许不好。

Empty 是有道理的,因为它读起来很好,像 NilNode 一样是未定义的(但没有其他不需要的语义),并且错误地遇到它的机会正在消失小的。它的缺点是它错过了 Node 对于已定义/未定义值的良好惯用二元性;它不像新的本地定义名称(例如 End)那么安全;有人可能想知道为什么您的代码使用 Slip;根据您的偏好,它的定义不会像 Nil 那样吸引您。

这有点像在问“打着伞我头上怎么还有雨”。 :-) Nil 存在于 Raku 中的主要原因是提供一个函数可以 return 在软故障产生结果时的值,如果分配到任何支持未定义的容器中,这将是安全的(或默认)。

因此,可以编写一个可以 return Nil 的函数 func,它只适用于调用者 my SomeType $foo = func() 或调用者 [=22] =].

虽然有 is default(Nil) 技巧,但我强烈建议您使用其他值作为标记。在您不希望它提供的主要行为的情况下尝试使用语言功能通常不会顺利进行。

Node 类型对象本身就是一个合理的选择。因此:

has $!head = Nil;

变为:

has Node $!head;

测试变为:

until $current === Node {

不过,我可能会这样写:

while $current.defined {

它还支持 Node 的子classing。另一方面,如果我知道 Node 是我的 class 的实现细节,我会觉得足够安全,可以使用依赖于默认的 boolification 语义来消除混乱:

while $current {

同时,这:

last if $current-node === Any;

可能会变成:

last without $current-node;

或者如果选择“依赖布尔化”方​​法,为了保持一致性:

last unless $current-node;

最后,如果 Node 只是一个实现细节,我会把它移到 UnorderedList 内并使其成为 my 范围。