了解带有多个子句的Elixir函数

时间:2018-12-09 03:24:06

标签: pattern-matching elixir

我最近开始学习Elixir。来自面向对象的编程背景,我在理解Elixir函数方面遇到困难。

我正在遵循Dave Thomas的书 Programming Elixir > = 1.6,但是我不太了解函数的工作原理。

在书中,他有以下示例:

handle_open = fn
  {:ok, file} -> "Read data: #{IO.read(file, :line)}"
  {_,  error} -> "Error: #{:file.format_error(error)}"
end

handle_open.(File.open(​"​​code/intro/hello.exs"​))   ​# this file exists​
-> "Read data: IO.puts \"Hello, World!\"\n"

 handle_open.(File.open(​"​​nonexistent"​))           ​# this one doesn't​
 -> Error: no such file or directory"

我不了解参数的工作方式。是否有隐式if,else语句隐藏在某处?

3 个答案:

答案 0 :(得分:4)

这里发生了几件事情,我将尝试涵盖所有这些内容。对于初学者,这里使用了两种不同的功能。一个是命名函数(File.open),另一个是您创建的匿名函数,已分配给变量handle_open。略有difference in the way both are called

当您在File.open函数内部调用handle_open函数时,基本上意味着您正在对其结果调用handle_open。 但是File.open/2函数本身可以返回两个值:

  1. {:ok, file}(如果文件存在)
  2. {:error, reason}(如果没有)(或出现其他错误)

handle_open函数使用pattern matchingmultiple function clauses来检查响应内容并返回适当的消息。如果给定值“匹配指定的模式”,则执行该语句,否则将检查下一个模式。 虽然从某种意义上说,它与if-else语句相似,但更像是case关键字:

result = File.open("/some/path")

case result do
  {:ok, file} ->
    "The file exists"

  {:error, reason} ->
    "There was an error"
end

答案 1 :(得分:3)

在elixir中,您可以定义具有多个子句的函数,并且elixir使用所谓的 模式匹配 来确定要执行的子句。如您所怀疑的那样,当您定义多个函数子句时,可以有效地创建一个隐式if-else if,例如 如果 在函数调用中指定的函数参数与第一个函数子句的参数匹配,然后执行第一个函数子句否则,如果在函数调用中指定的函数参数与第二个函数子句的参数匹配,则执行第二个函数子句,依此类推。。这是一个示例:

my.exs:

defmodule My do

  def calc(x, 1) do
    x * 3
  end
  def calc(x, 2) do
    x - 4
  end

  def test do
    IO.puts calc(5, 1)
    IO.puts calc(5, 2)
  end

end

(我通常不会在函数定义的多个子句之间留空行以表明它们都是相同的函数。)

在iex中:

$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> My.test
15
1
:ok

iex(2)>

调用calc(5, 1)时,elixir进入calc()的定义,并且elixir从第一个函数子句开始,并尝试将参数5, 1与参数x, 1匹配。因为参数x是变量,所以它可以匹配任何内容,而参数1仅可以匹配1。因此,存在匹配项,并且x被分配了值5。在其他语言中,不能将诸如1"hello"%{a: 1}:error之类的值指定为函数定义中的函数参数-而是所有函数参数必须是变量。

类似地,当您调用calc(5, 2)时,elixir进入calc()的定义,并且elixir从第一个函数子句开始,并尝试将函数调用中的参数5, 2与参数x, 1x参数可以匹配任何内容,但是1参数不匹配2参数,因此elixir转到下一个函数子句,并尝试将参数5, 2与参数x, 2。这样就产生了一个匹配项,并且x被分配了5。

Elixir有一些非常有趣的匹配规则。在某些时候您会遇到以下几条规则,这些规则可能很难破译:

  1. def func(%{] = x)将匹配作为映射的任何参数(而不仅仅是空映射),并且该映射将被分配给变量x。结构也是如此,例如def func(%Dog{} = x)将匹配任何Dog结构。

  2. def func("hello " <> subject)将匹配任何以"hello "开头的字符串参数,其余的字符串将分配给subject变量。

Elixir还允许您定义具有多个子句的 匿名函数 。如果您将test()更改为:

  def test do
    func = fn 
              (:ok, x)    -> IO.puts ":ok branch, x = #{x}"
              (y, :error) -> IO.puts ":error branch, y = #{y}"
           end

    func.("hello", :error)
    func.(:ok, 10)
  end

然后在iex中,您将看到:

~/elixir_programs$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> My.test()  
:error branch, y = hello
:ok branch, x = 10
:ok

iex(2)> 

匹配与命名函数的工作原理相同:elixir依次查看函数子句,并尝试将函数调用中的参数与函数子句中的参数进行匹配。请注意,在代码中定义函数子句的顺序可能很重要。定义一个永远不会执行的函数子句很容易,例如:

func = fn 
          (x)    ->  ...
          (:ok)  ->  ...
       end

由于第一个函数子句中的参数x将与任何参数匹配,因此第二个函数子句将永远无法执行。幸运的是,如果您这样做,长生不老药会警告您。

而且,由于e剂是一种功能语言,因此如果不显示递归示例,将被忽略。将以下count()定义添加到“我的模块”中:

  def count(0) do
    :ok
  end
  def count(n) when n > 0 do
    Process.sleep 1_000  #sleep for 1 second
    IO.puts n
    count(n-1)
  end

然后在iex中:

 ~/elixir_programs$ iex my.exs 
 Erlang/OTP 20 [erts-9.3] [source]
 [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe]
 [kernel-poll:false] Interactive Elixir (1.6.6) - press Ctrl+C to exit
 (type h() ENTER for help)

iex(1)> My.count(10)
10
9
8
7
6
5
4
3
2
1
:ok

iex(2)>

当代码调用count(n-1)时,elixir转到count()的第一个函数子句,然后尝试将参数n-1的值与参数{{1}进行匹配在函数子句中。如果没有匹配项,则elixir尝试第二个函数子句。结果,第二个函数子句一直与函数调用0匹配,直到输出包括1的所有数字为止,于是count(n-1)在函数调用中等于0,从而导致函数调用{{1 }},它最终与第一个function子句匹配,并且first function子句不输出任何内容,也不调用自身,因此递归结束。

在长生不老药中学习的关键是:

  1. 模式匹配
  2. 递归
  3. 生成其他进程

祝你好运!

答案 2 :(得分:0)

本书joyElixir是处理文件的一个很好的解释。 这是一个很好的起点,是一本小书。