我正在学习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的旅程,所以澄清以这种方式使用案例陈述的缺点,正是我在寻找的。
答案 0 :(得分:15)
书中的代码不是很惯用,它试图在不是最好的例子上显示多个函数子句和管道。
首先,一般惯例是管道应该以“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
有两种主要情况,其中多个函数子句优于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 :(得分:1)
你可以在这里指出,这里的三体版本更擅长制作具有单一责任的功能。我认为这是软件设计中最重要的原则之一,适用于OO中的类(和类似结构)以及FP中的函数。 case-statement版本更短更简洁,但可以一次性结合文件读取和行计数。尝试为它编写测试,然后为分裂的版本编写测试。
当然,这是一个设计问题。因人而异。但我认为一本书需要在安全方面犯错,而且在应用SRP和简洁之间有一个不错的权衡。
答案 2 :(得分:1)
在这个特定的例子中,它可能没有什么区别你这样做。没有任何东西可以说你"必须"使用模式匹配函数子句。
case
语句版本与其他语言的版本更相似,因此作者可能会引入Elixir特定概念,以期在以后更多地使用它。
我绝对更喜欢多功能子句版本,但也许是因为我已经看了一段时间的Erlang和Elixir代码并且已经习惯了它。
我在Elixir Slack频道上询问了选择case
语句中的功能的原因,建议观看此视频:https://www.youtube.com/watch?v=CQyt9Vlkbis
在case
语句中使用函数子句的主要理由是,您可以为您正在做出的决定命名。这个问题中给出的例子在这一点上并不具有吸引力,但视频非常清楚。