我对 Elixir 还很陌生,但也非常熟悉该家族中的两种语言的函数式编程,更普遍的是作为一种风格的编程。根据宣布弃用此行为的博文 [见下文],其意图似乎是鼓励更惯用的编程(除其他外)。
避免这种行为的一个好处是,将这些代码块提取到单独的函数中变得更加简单。函数式编程风格鼓励将程序的行为视为数据的一系列或序列转换,而不是修改。典型的函数式编程语言至少在默认情况下还提供不可变数据,同时在内部,在程序生成的数据之间共享公共或共享值,鼓励您将数据转换实现为纯函数,即不修改现有数据状态的代码块。
在 Elixir 和其他函数式编程语言中,模式匹配和强大的标准“集合”类型的组合提供了一种从单个函数或代码块返回多个值的简洁方法。相反,例如,在面向对象的编程语言中,通常会返回一个对象,该对象具有多个可作为该对象类的成员访问的值。这也是 Elixir 或其他函数式编程语言中完全有效的模式 - 请参阅 Elixir 中的 structs - 但在返回相对较少数量的值时,它是不必要的,而且几乎总是不太clear。
所以你的第一个例子可以重写为:
a = 0
a = if true, do: 1 + 1, else: a
a = a + 1
IO.puts (a)
你的例子太做作了,没有任何明显的优势。您的问题暗示了批评,我认为这是有效的,因为else,即需要明确“无操作”更新a,是多余的。这是这种编程风格的真实的,尽管很小的“固定成本”。但是您可以轻松地将“可能转换”行为的想法封装为函数或宏:
def maybe_transform(x, cond, f) do
if cond, do: f.(x), else: x
end
通过多种可能的转换可以更好地看出这种风格的真正好处:
a = 0
a
|> maybe_transform(cond1, &transform_function_1/1)
|> maybe_transform(cond2, &transform_function_2/1)
|> maybe_transform(cond3, &transform_function_3/1)
其中函数transform_function_1、transform_function_2、transform_function_3 将根据相关条件可能在a 的(可能)和连续转换的值上被调用。请注意,|> 运算符将a 的(可能)转换后的值作为第一个参数传递给maybe_transform 的每个调用。
你的第二个例子可以重写为:
a = 0
b = 0
{a, b} = if true, do: { 1 + 1, 2 + 2 }, else: {a, b}
a = a + 1
b = b + 2
IO.puts (a)
IO.puts (b)
再一次,这个例子被最大限度地设计,使得弃用命令式赋值行为的好处不清楚。
在当前接受的答案a comment 中,您写道:
如果我要解决一个复杂的数学问题,需要用多个嵌套 if 更改 40 个变量,那么我必须为每个嵌套 if 语句定义 {a,..,a40}?
我想不出任何涉及 40 个“返回”变量的示例,其中计算或转换都取决于相同的复杂条件 某种方式使得命令式风格在某种程度上会更清晰或明显更好。一个详细的、具体的例子会很有帮助。导致数据(如 40 个值的向量)的数据通常是“结构化的”,因此 Elixir 标准 Enum 模块中的 map 或 reduce 函数通常比函数式编程风格更清晰涉及命令式赋值的等效代码,您通常也不需要或不想为单个向量中包含的所有值维护 40 个单独的变量。
当我遇到这个问题时,我正在处理的问题涉及从两个不同的可能数据集构建一个列表;我这样做的函数的初稿:
def build_list(x) do
new_list = []
if cond1 do
something = f1(x)
if cond2 do
new_list = [ f2(something) | new_list ]
end
end
if cond3 do
something_else = f3(x)
if cond4 do
something_completely_different = f4(something_else)
if test(something_completely_different) do
new_list = [ f5(something_completely_different) | new_list ]
end
end
end
new_list
end
我可以通过多种方式重写它,但我选择了这样的方式:
def build_list(x) do
list_1 =
case cond1 do
false -> []
true ->
something = f1(x)
if cond2, do: [f2(something], else: []
end
list_2 =
if cond3 do
something_else = f3(x)
if cond4 do
something_completely_different = f4(something_else)
if test(something_completely_different) do
something_completely_different
else
[]
end
else
[]
end
else
[]
end
list_1 ++ list_2
end
请注意,新版本的行为有所不同,因为[x | list] 返回一个带有x 的新列表前置 到list 的内容,而list_1 ++ list_2 有效地返回一个带有list_2 的新列表附加到list_1。就我而言,这并不重要。
而且,因为cond4 和test 实际上是在测试something_else 或something_completely_different,它们本身就是列表,是空的,而list ++ [] == list,我最终得到了更像这样的东西:
def build_list(x) do
list_1 =
case cond1 do
false -> []
true ->
something = f1(x)
if cond2, do: [f2(something], else: []
end
list_2 =
if cond3 do
f4(f3(x))
else
[]
end
list_1 ++ list_2
end
最终帮助我的部分原因是我在新版本中使用的标准函数和运算符处理了“退化”数据,例如一个空列表[] 值,明智的。我的cond4 正在检查f3(x) 不是一个空列表,但f4 本身在给定一个空列表参数的情况下工作得很好,在这种情况下它本身返回一个空列表。当使用语法[x | list] 生成一个带有x 的新列表时,我必须检查x 本身是否是一个空列表,否则它将附加一个空列表作为新的头元素列表,但list ++ x 和x ++ list 都与list 相同,当x 为空时。
来自the official blog post announcing the release of Elixir version 1.3(引入了您观察到的警告以将相关行为标记为已弃用的版本):
弃用命令式赋值
Elixir 现在会在 if、case 和朋友等结构分配给在外部范围内访问的变量时发出警告。例如,想象一个名为 format 的函数,它接收消息和一些选项,它必须在消息旁边返回一个路径:
def format(message, opts) do
path =
if (file = opts[:file]) && (line = opts[:line]) do
relative = Path.relative_to_cwd(file)
message = Exception.format_file_line(relative, line) <> " " <> message
relative
end
{path, message}
end
上面的if 块隐式更改了message 中的值。现在假设我们想将if 块移动到它自己的函数中以清理实现:
def format(message, opts) do
path = with_file_and_line(message, opts)
{path, message}
end
defp with_file_and_line(message, opts) do
if (file = opts[:file]) && (line = opts[:line]) do
relative = Path.relative_to_cwd(file)
message = Exception.format_file_line(relative, line) <> " " <> message
relative
end
end
重构的版本被破坏了,因为if 块实际上返回了两个值,相对路径和新消息。 Elixir v1.3 将对这种情况发出警告,强制从if、case 和其他构造显式返回两个变量。此外,此更改使我们有机会在未来版本中统一语言范围规则。