【问题标题】:Understanding Elixir functions with multiple clauses了解具有多个子句的 Elixir 函数
【发布时间】:2018-12-17 06:28:18
【问题描述】:

我最近开始学习 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 语句隐藏在某处?

【问题讨论】:

  • 我想和你分享的一个重要的一点是:在匿名函数中你不能混合使用不同arities的子句,例如fun = fn x,y -> "TEST" ; x,y,z -> "TEST2"; end 你会得到一个错误:** (CompileError) iex:1: cannot mix clauses with different arities in anonymous functions
  • 如果您想知道函数返回什么值,请使用IO.inspect (functionx()) 示例:handle_open.(IO.inspect (File.open(​"​​code/intro/hello.exs"​))) 或使用管道运算符,例如:File.open(​"​​code/intro/hello.exs"​) |> IO.inspect |> handle_open.()
  • 查看匿名函数链接:blog.distortedthinking.agency/articles/…

标签: pattern-matching elixir


【解决方案1】:

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

当您在handle_open 函数中调用File.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

【讨论】:

    【解决方案2】:

    在 elixir 中,您可以定义具有多个子句的函数,并且 elixir 使用所谓的 模式匹配 来确定要执行哪个子句。正如您所怀疑的,当您定义多个函数子句时,您有效地创建了一个隐式if-else if,例如如果函数调用中指定的函数参数与第一个函数子句的参数匹配,执行第一个函数子句,else if 函数调用中指定的函数参数匹配第二个函数子句的参数,然后执行第二个函数子句,等等。下面是一个例子:

    我的.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, 1 进行匹配。 x 参数可以匹配任何东西,但 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 将匹配任何参数,所以第二个函数子句永远无法执行。幸运的是,如果你这样做,elixir 会警告你。

    而且,因为 elixir 是一种函数式语言,所以不显示递归示例将是失职。在我的模块中添加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 的值与函数子句中的参数0 的值。如果没有匹配,elixir 会尝试第二个函数子句。结果第二个函数子句一直匹配函数调用count(n-1),直到输出包括1在内的所有数字,于是函数调用中n-1等于0,导致函数调用count(0),最终匹配第一个函数子句,第一个函数子句不输出任何东西,也不调用自己,所以递归结束。

    在 Elixir 中要学习的关键内容是:

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

    祝你好运!

    【讨论】:

      【解决方案3】:

      这本书很好地解释了使用文件:joyElixir。 这是一个很好的起点,而且是一本小书。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-11-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-11-07
        相关资源
        最近更新 更多