Julia 函数:使可变类型不可变

Dar*_*osa 9 mutable immutability julia

来自 Wolfram Mathematica,我喜欢这样的想法,即每当我将变量传递给函数时,我实际上是在创建该变量的副本。另一方面,我了解到在 Julia 中有可变和不可变类型的概念,前者通过引用传递,后者通过值传递。有人可以向我解释这种区别的优势吗?为什么数组是通过引用传递的?我天真地认为这是一个不好的方面,因为它会产生副作用并破坏编写纯函数代码的可能性。我的推理哪里错了?有没有办法使数组不可变,这样当它传递给函数时,它实际上是按值传递的?

这里是代码示例

#x is an in INT and so is immutable: it is passed by value
x = 10
function change_value(x)
    x = 17
end

change_value(x)

println(x)

#arrays are mutable: they are passed by reference
arr = [1, 2, 3]

function change_array!(A)
    A[1] = 20
end

change_array!(arr)

println(arr)
Run Code Online (Sandbox Code Playgroud)

这确实修改了数组 arr

Col*_*ers 15

这里有一个公平的回应。

首先,Julia 不传递引用或传递值。相反,它采用了一种称为传递共享的范式。引用文档

函数参数本身充当新的变量绑定(可以引用值的新位置),但它们引用的值与传递的值相同。

其次,您似乎在问为什么 Julia 在将数组传递给函数时不复制它们。这是一个简单的答案:性能。Julia 是一种面向性能的语言。每次将数组传递给函数时都进行复制对性能不利。每个copy操作都需要时间。

这有一些有趣的副作用。例如,您会注意到许多成熟的 Julia 包(以及基础代码)由许多短函数组成。这种代码结构是函数调用接近零开销的直接结果。另一方面,像 Mathematica 和 MatLab 这样的语言倾向于长函数。我不想在这里开始一场激烈的战争,所以我只是说我个人更喜欢许多短函数的 Julia 风格。

第三,您想知道传递共享的潜在负面影响。从理论上讲,当用户不确定某个函数是否会修改其输入时,这可能会导致问题,这一点是正确的。在该语言的早期,对此进行了长时间的讨论,根据您的问题,您似乎已经确定约定是修改其参数!的函数在函数名称中带有尾随。有趣的是,这个标准不是强制性的,所以是的,理论上有可能最终出现一个狂野的西部类型的场景,其中用户生活在一个持续的不确定状态中。在实践中,这从来都不是问题(据我所知)。使用约定!在 Base Julia 中强制执行,实际上我从未遇到过不遵守此约定的包。综上所述,是的,pass-by-share 时可能会遇到问题,但在实践中从来没有出现过问题,而且性能收益远远超过成本。

第四(也是最后),您询问是否有办法使数组不可变。首先,我强烈建议不要尝试使本机数组不可变的黑客行为。例如,您可以尝试禁用setindex!数组的功能...但请不要这样做。它会破坏很多东西。

正如对该问题的评论中提到的,您可以使用StaticArrays。然而,正如 Simeon 在对这个答案的评论中指出的那样,对于真正大的数据集使用静态数组会导致性能下降。超过 100 个元素,您可能会遇到编译问题。静态数组的主要好处实际上是可以为较小的静态数组实现的优化。

phipsgabler 在下面的评论中建议的另一个基于包的选项是FunctionalCollections。这似乎可以满足您的需求,尽管它看起来只是偶尔维护。当然,这并不总是一件坏事。

一种更简单的方法是,只要您想实现传值,就在您自己的代码中复制数组。例如:

f!(copy(x))
Run Code Online (Sandbox Code Playgroud)

只要确保你明白之间的差别copydeepcopy,当你可能需要使用后者。如果您只处理数字数组,则永远不需要后者,实际上使用它可能会大大减慢您的代码速度。

如果您想做一些工作,那么您也可以本着静态数组的精神构建自己的数组类型,但没有静态数组所需的所有花里胡哨。例如:

struct MyImmutableArray{T,N}
    x::Array{T,N}
end
Base.getindex(y::MyImmutableArray, inds...) = getindex(y.x, inds...)
Run Code Online (Sandbox Code Playgroud)

同样,您可以向此类型添加您想要的任何其他函数,同时排除setindex!.

  • @AboAmmar 那是不正确的。数组是可变的,但它们的元素不一定如此。例如,对于“Int”的“Array”,您可以通过替换其元素来改变数组,但不能改变元素,因为“Int”是不可变的。您可以自己测试:创建一个整数数组:`a = [1,2,3]`,然后运行`isimmutable(a)`和`isimmutable(a[1])`。 (4认同)
  • (+1) 很好的答案。我想补充一点,数组**不**可变,但是它们的元素是可变的。看这个例子:`arr = [1, 2, 3]; 函数change_array!(A) A = [20, 2, 3] end; 更改数组!(arr);arr`,您将得到:“3 元素数组{Int64,1}:1 2 3”。这非常方便和有用,有时人们会要求逆;具有可变元素的静态数组。 (3认同)
  • 保持不可变风格的真正解决方案是使用[纯函数式数据结构](https://github.com/JuliaCollections/FunctionalCollections.jl)(但我相信这个包目前维护得不是很好)。 (3认同)
  • 您绝对不想对大于 100 个元素的数组使用 StaticArray,因为这对编译器来说确实是一个负担,而且生成的代码实际上不会那么高效。https://github.com/JuliaLang/julia/pull/31630 可能也很有趣,但这仍然是非常推测性的,可能仍然需要一些工作。 (2认同)