为什么Raku语言的列表赋值操作会扁平化其左操作数?
我理解列表赋值会扁平化其左半部分:
my ($a, $b, $c); ($a, ($b, $c)) = (0, (1.0, 1.1), 2); say "\$a: $a"; # 输出:«$a: 0» say "\$b: $b"; # 输出:«$b: 1 1.1» <-- $b 的值*不是*1 say "\$c: $c"; # 输出:«$c: 2» <-- $c 的值*不是*1.1
我也知道我们可以使用:($a, ($b, $c)) := (0, (1.0, 1.1))来得到非扁平化的行为。
我不理解的是为什么列表赋值过程中左半部分会被扁平化。这其实可以拆分为两个问题:第一,这种扁平化行为和Raku语言的其余语义是如何适配的?第二,如果左半部分不自动扁平化,是否有某些行为是无法实现的?
关于第一个问题,我知道Raku历史上存在很多自动扁平化行为。在大列表重构(Great List Refactor, GLR)之前,类似my @a = 1, (2, 3), 4这样的表达式会自动扁平化右半部分,最终得到数组[1, 2, 3, 4];同理,map和很多其他迭代结构也会扁平化它们的参数。但GLR之后,Raku基本不会在未被显式告知的情况下列表扁平化。实际上我想不到其他任何不需要flat、.flat、|或*@参与的Raku扁平化场景(@_会隐式创建*@)。是我遗漏了什么,还是列表赋值的左半部分行为确实和GLR后的语义不一致?这种行为是历史遗留问题,还是仍然有其合理性?
关于第二个问题,我猜测列表赋值的扁平化行为可能在某种程度上有助于支持惰性。例如我知道可以用列表赋值从惰性列表中取出特定值,而不需要生成/计算所有值——但对列表使用:=绑定时则需要计算右半部分的所有值。不过我不确定左半部分自动扁平化是否是支持该特性的必要条件,也不清楚其实现逻辑。
我还猜测自动扁平化可能和=可以用于元运算符有关——:=则不同,如果和元运算符一起使用会抛出“too fiddly(过于繁琐难以处理)”错误。但我不清楚自动扁平化是否让=没那么“难处理”,也不知道二者的关联逻辑。
[编辑:我找到了早在2015-05-02的IRC记录,其中提到了“列表赋值是扁平化的”是GLR中保留的决策,所以显然这个决定是经过深思熟虑的、有充分依据的。但到目前为止我还没找到相关的决策依据,猜测可能是在面对面会议中确定的,因此希望有人能解答。]
最后我还想知道从概念层面,左半部分是如何被扁平化的(我不是特指Rakudo的具体实现,而是指通用的心智模型)。以下是我之前对绑定和列表赋值的理解:
my ($a, :$b) := (4, :a(2)); # 概念上等价于对右值调用.Capture my ($c, $d, $e); ($c, ($d, $e)) = (0, 1, 2); # 我之前认为概念上等价于对左值调用flat
但实际对第一行的右半部分调用.Capture是可以正常运行的,而对第三行的左半部分调用flat则会抛出Cannot modify an immutable Seq(无法修改不可变序列)错误——考虑到我们经常会扁平化Seq,这让我非常困惑。所以有没有更合理的心智模型来理解这种自动扁平化行为?
提前感谢各位的帮助,我正在完善相关文档,因此想要更好地理解该特性,所有的见解都能对文档优化工作提供支持。
内容的提问来源于stack exchange,提问作者codesections

