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

时间:2016-03-20 21:01:56

标签: 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 

因此我们有两个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

两者的工作方式相同。

我不想学习不正确的习语,因为我开始了Elixir的旅程,所以澄清以这种方式使用案例陈述的缺点,正是我在寻找的。

3 个答案:

答案 0 :(得分:15)

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

第1部分:关注点分离。

首先,一般惯例是管道应该以“raw”变量开头,如下所示:

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

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

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

现在,您的计数功能有两个选项。当文件出现问题或处理错误时,您可以让它崩溃。在这个例子中只返回字符串“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

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

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

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

多态性

假设您想使lines_num更通用以处理字符表示列表:

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

实施可能会有所不同,但最终结果将是相同的:不同类型的行数:"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)]

(警告:示例不是尾递归的)对这些函数使用case会更不易读/惯用。

结论:

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

答案 1 :(得分:1)

你可以在这里指出,这里的三体版本更擅长制作具有单一责任的功能。我认为这是软件设计中最重要的原则之一,适用于OO中的类(和类似结构)以及FP中的函数。 case-statement版本更短更简洁,但可以一次性结合文件读取和行计数。尝试为它编写测试,然后为分裂的版本编写测试。

当然,这是一个设计问题。因人而异。但我认为一本书需要在安全方面犯错,而且在应用SRP和简洁之间有一个不错的权衡。

答案 2 :(得分:1)

在这个特定的例子中,它可能没有什么区别你这样做。没有任何东西可以说你"必须"使用模式匹配函数子句。

case语句版本与其他语言的版本更相似,因此作者可能会引入Elixir特定概念,以期在以后更多地使用它。

我绝对更喜欢多功能子句版本,但也许是因为我已经看了一段时间的Erlang和Elixir代码并且已经习惯了它。

我在Elixir Slack频道上询问了选择case语句中的功能的原因,建议观看此视频:https://www.youtube.com/watch?v=CQyt9Vlkbis

case语句中使用函数子句的主要理由是,您可以为您正在做出的决定命名。这个问题中给出的例子在这一点上并不具有吸引力,但视频非常清楚。