在Elixir中,为什么不使用case语句而不是多个函数重载?

Tho*_*wne 7 elixir

我正在学习Elixir并且有点困惑为什么我们必须使用相同函数的多个定义进行分支,而不是使用case语句.以下是来自Elixir in Action的第一版第81页的示例,用于计算文件中的行:

defmodule LinesCounter do
  def count(path) do
    File.read(path)
    |> lines_num
  end

  defp lines_num({:ok, contents}) do
    contents
    |> String.split("\n")
    |> length
  end

  defp lines_num({:error, _}), do: "error"
 end 
Run Code Online (Sandbox Code Playgroud)

所以我们有两个defp lines_num实例来处理以下情况:ok和:error.但是,以下是不是做同样的事情,可以说是更清洁,更简洁,只使用一个函数而不是三个?

defmodule LinesCounterCase do
  def count(file) do
    case File.read(file) do
      {:ok, contents} -> contents |> String.split("\n") |> length
      {:error, _} -> "error"
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

两者的工作方式相同.

我不想学习不正确的习语,因为我开始了Elixir的旅程,所以澄清以这种方式使用case语句的缺点,就是我在寻找的东西.

tko*_*wal 15

书中的代码不是很惯用,它试图在不是最好的例子上显示多个函数子句和管道.

第1部分:关注点分离.

首先,一般约定说管道应该以"raw"变量开头,如下所示:

def count(path) do
  path
  |> File.read
  |> lines_num
end
Run Code Online (Sandbox Code Playgroud)

第二件事是这个代码真的混合了责任.有时对函数返回的类型也有好处.如果我看到,lines_num返回整数或字符串,我真的会抓住我的头.lines_num阅读文件时为什么要关心错误?答案是:它不应该.它应该采用一个字符串并返回它计算的内容:

defp lines_num(contents) do #skipping the tuple here
  contents
  |> String.split("\n")
  |> length
end
Run Code Online (Sandbox Code Playgroud)

现在,您的计数功能有两个选项.当文件出现问题或处理错误时,您可以让它崩溃.在这个例子中只返回字符串"error",所以最惯用的方法是完全跳过它:

def count(path) do
  path
  |> File.read! #note the "!" it means it will return just content instead {:ok, content} or rise an error
  |> lines_num
  end
end
Run Code Online (Sandbox Code Playgroud)

Elixir几乎总是提供func!版本,正是出于这个原因 - 使管道更容易.

如果要处理错误,case语句是最好的.Unix管道也不鼓励分支.

def count(path) do
  case File.read(path) do
    {:ok, contents} -> lines_num(contents)
    {:error, reason} -> do_something_on_error(reason)
  end
end
Run Code Online (Sandbox Code Playgroud)

第2部分:多个函数子句有意义吗?

有两种主要情况,其中多个函数子句优于case语句:递归和多态.还有其他一些,但对初学者来说应该足够了.

多态性

假设你想使lines_num更通用的也处理chars表示列表:

defp lines_num(contents) when is_binary(contents) do
  ...
end
defp lines_num(contents) when is_list(contents) do
  contents
  |> :binary.list_to_bin #not the most efficient way!
  |> lines_num
end
Run Code Online (Sandbox Code Playgroud)

实现可能会有所不同,但最终结果将是相同的:不同类型的行数:"foo \n bar"'foo \n bar'.

递归

def factorial(0), do: 0
def factorial(n), do: n * factorial(n-1)

def map([], _func), do: []
def map([head, tail], func), do: [func.(head), map(tail)]
Run Code Online (Sandbox Code Playgroud)

(警告:示例不是尾递归的)使用这种函数的情况将更不易读/惯用.

结论:

  1. 除非您知道自己在做什么,否则不要将功能头用于分支逻辑.
  2. 如果你有分支逻辑,最好拆分管道.
  3. 使用函数子句进行多态和递归.

  • 可能需要添加的东西:如果你有一个嵌套的case语句,你需要将它分成单独的函数.嵌套的案例陈述很快就变成了混乱的泥球. (3认同)
  • 这是事实,但我认为这是一个单独的问题,并没有回答何时使用案例与功能条款的问题. (3认同)