【问题标题】:Elixir: Set variable in if statementElixir:在 if 语句中设置变量
【发布时间】:2019-06-28 04:30:50
【问题描述】:

我对 Elixir 很陌生,这个简单的问题让我发疯了。

a = 0
if true do
    a = 1 + 1
end 
a = a + 1

IO.puts (a)

有趣的是,这给出了正确的值,但也给出了警告:

warning: the variable "a" is unsafe as it has been set inside a case/cond/receive/if/&&/||. Please explicitly return the variable value instead. For example:

case int do
  1 -> atom = :one
  2 -> atom = :two
end

should be written as

atom =
  case int do
    1 -> :one
    2 -> :two
  end

Unsafe variable found at:
  Untitled:5

3

我不明白警告信息。在 Elixir 中执行此操作的最佳方法是什么?

更新:这种情况呢?

a = 0
b = 0
if true do
    a = 1 + 1
    b = 2 + 2
end 
a = a + 1
b = b + 2

IO.puts (a)
IO.puts (b)

【问题讨论】:

    标签: elixir


    【解决方案1】:

    在 Elixir 中,每个语句都返回值。您可以将整个 if 语句值分配给变量,而不是在 if 中分配变量。

    a = 0
    a = if true do
          1 + 1
        else
          a + 1
        end
    

    【讨论】:

    • 如果我想在同一个 if 语句中更新两个值怎么办?即a和b。嵌套 if 呢?
    • 那么你可以使用模式匹配{a, b} = if true do {a + 1, b + 2} else {a, b} end
    • 我完全理解你在说什么,这对我来说没有多大意义。我必须坚持使用更传统的编程技术。如果我有一个复杂的数学问题要解决,需要用几个嵌套的 if 改变 40 个变量,那么我必须为每个嵌套的 if 语句定义 {a,..,a40}?这将导致一些非常丑陋的代码。我敢肯定,我一定是在以一种老式的错误方式来思考这个问题。谢谢。
    • 考虑如果what正是你想用这个if实现的。 Elixir 和 Erlang 中的分支通过许多函数子句和模式匹配来解决,而不是 if 语句。
    • 我同意@PatNowak。如果您对 N 个变量执行类似的转换并且使用嵌套的 if,则最好使用递归和多个函数子句。
    【解决方案2】:

    警告是正确的,它试图阻止你做_可能危险_的事情。 Elixir 的 1.3 更新日志中对此进行了很好的解释。

    看看命令式赋值的弃用部分,这里有解释(用例子):

    http://elixir-lang.org/blog/2016/06/21/elixir-v1-3-0-released/

    希望有帮助!

    【讨论】:

    • 感谢您指出这一点。找了一段时间的官方解释。
    【解决方案3】:

    我对 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_1transform_function_2transform_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 模块中的 mapreduce 函数通常比函数式编程风格更清晰涉及命令式赋值的等效代码,您通常也不需要或不想为单个向量中包含的所有值维护 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。就我而言,这并不重要。

    而且,因为cond4test 实际上是在测试something_elsesomething_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 ++ xx ++ list 都与list 相同,当x 为空时。

    来自the official blog post announcing the release of Elixir version 1.3(引入了您观察到的警告以将相关行为标记为已弃用的版本):

    弃用命令式赋值

    Elixir 现在会在 ifcase 和朋友等结构分配给在外部范围内访问的变量时发出警告。例如,想象一个名为 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 将对这种情况发出警告,强制从ifcase 和其他构造显式返回两个变量。此外,此更改使我们有机会在未来版本中统一语言范围规则。

    【讨论】:

      猜你喜欢
      • 2014-05-01
      • 1970-01-01
      • 2021-09-14
      • 2017-04-26
      • 1970-01-01
      • 2016-08-17
      • 1970-01-01
      • 2018-12-15
      • 1970-01-01
      相关资源
      最近更新 更多