Elixir 简明笔记(九)--- 匿名函数

Elixir 是函数式语言,函数是 Elixir 的核心,也是一种基本类型。前面我们已经知道一些内建的函数,例如div,rem,is_list等。与大多数语言语言,函数可以调用,通常使用一对圆括号,与大多数语言不一样,Elixir 的函数是可以省略圆括号。

当然这个特性 Ruby也有。Elixr的函数其实分为两种,一种是匿名函数,另外一种则是命名函数。它们都是函数,却有着不一样的地方。我们先了解匿名函数。

定义

一般的编程语言,函数的定义都是通过关键字和函数的名字,或者通过返回类型和函数名字,有的还有参数来定义,例如 js和 python

先看js

function func_name(){
    // func_body
}

python的函数定义

def func_name():
    # func_body
    pass

当然有的时候,js和python也可以这样定义函数,即函数表达式

sayHello = function (){
    return  "hello";
}
调用
sayHello();

say_hello = lambda : 'hello'
say_hello()

对于js和python,第一种即函数的定义,第二种可以称之为匿名函数,因为函数都没有名字,但是函数表达式可以赋值给一个变量,通过变量调用函数。

好了,说了这么多,都是js和python。我们的重点是Elixir啊。幸运的,Elixir的匿名函数和上述两个语言极其相识。

其语法字面大概如下:

fn 
parameter-list -> body
parameter-list -> body
...

end

Elixir的fn...end包裹的函数体,类似别的js的花括号包裹的函数体。定义匿名函数的时候,要把函数表达式绑定给一个变量,通过变量调用。

iex(1)> sum = fn -> "hello" end
#Function<20.90072148/0 in :erl_eval.expr/5>
iex(2)> sum
#Function<20.90072148/0 in :erl_eval.expr/5>
iex(3)> sum.()
"hello"

代码和js很相似吧,不过elixir的匿名函数调用的时候,不是通过括号直接调用,而是在绑定的变量和括号之间,有一个.操作符。并且括号不能省略。当然匿名函数的参数列表的括号可以省略。

iex(5)> two_number_sum = fn a, b -> a + b end
#Function<12.90072148/2 in :erl_eval.expr/5>
iex(6)> two_number_sum
#Function<12.90072148/2 in :erl_eval.expr/5>
iex(7)> two_number_sum.(1, 2)
3
iex(8)> two_number_sum.1, 2
** (SyntaxError) iex:8: syntax error before: 1

匿名函数的模式匹配

我们不仅一次说过,Elixir中模式匹配至关重要。elixir的类型几乎都可以进行模式匹配,函数也存在模式匹配。

上面的two_number_sum匿名函数中,a和b两个参数,通常称之为形参,1,2为传入的参数,通常称之为实参。基于以前的经验,不管a和b是传值还是传引用,都可以看出1,2对a,b进行赋值。

实际上,elixir中并不存在赋值的概念。实际上是模式匹配。two_number_sum匿名函数调用的时候,参数可以进行模式匹配,当传入1,2的时候,Elixir会尝试用参数a,b进行模式匹配,例如:


{a, b} = {1, 2}

举一反三,我们会想到,当调用函数的时候,甚至可以使用更复杂的模式匹配。下面的匿名函数接受一个元组参数,然后把元组的元素对调位置返回。

iex(1)> swap = fn {a, b} -> {b, a} end
#Function<6.90072148/1 in :erl_eval.expr/5>
iex(2)> swap.({6, 8})
{8, 6}

匿名函数的参数列表的括号是可以省略的,不雅被上面的元组花括号迷惑哦。

多函数体

函数传参调用是模式匹配的过程,那么如果不匹配,显然就无法绑定参数变量,进而执行函数了。因此匹配可以作为匿名函数执行函数体的一个条件。实际上很多情况模式匹配都可以做逻辑的分支处理,即匹配的时候该怎样,不匹配的时候又怎样。匿名函数可以模式匹配,那么设置多个匹配么?

答案是肯定的,Elixir的匿名函数可以设置多个参数列表,已经参数列表对应的函数体。

iex(1)> handle_open = fn
...(1)>   {:ok, file} -> "Read data: # 返回 {IO.read(file, :line)}"
...(1)>   {_, error} -> "Error: # 返回 {:file.format_error(error)}"
...(1)> end
#Function<6.90072148/1 in :erl_eval.expr/5>
iex(2)> handle_open.(File.open("./elixir.txt"))
"Read data: hello elixir"
iex(3)> handle_open.(File.open("./elixir.txts"))
"Error: no such file or directory"

介绍元组的时候,曾经说过使用元组的模式匹配来打开文件。这里面把元组的模式匹配过程分别作为匿名函数的参数和函数体。

仔细看一下函数的定义。第二和第三行定义了两个不同的函数体。每一个函数体都接受一个单独的元组。第一个函数体的参数元组第一个元素是:ok。而另外一个函数体则是_。这两个元组正好可以匹配文件打开成功或者失败的返回值。

第六行我们将File.open打开存在的文件,并把结果作为参数调用handle_open函数。这里函数将会传入一个元组{:ok,file},这个参数正好喝第二行的函数子句模式匹配,匹配的代码中IO.read将会读出文件内容。

再一次调用handle_open。这一串我们尝试读取一个不存在的文件。此时读取文件的函数返回元组是({:error,:enoent}) 。这个参数和函数的第二个字句匹配。因此会执行第二个函数字句的内容。

注意上面那段代码。第三行我们调用:file.format_error。:file是一个Erlang文件模块的引用,也就是调用了erlang模块的format_error函数。这与第六行的File.open调用不一样。后者使用了Elixir内建的模块。

该例子正好展示了在Elixir环境中与Erlang的无缝对接。你可以在Elixir中使用任何Erlang的库。Erlang中又很丰富的库足够你选择。当然需要注意,毕竟两个语言的调用方式不一样。后面将会介绍更多在elixir中使用erlang的库的内容。

上述例子的最后展示了elixir的字符串插入操作。#{变量}定义了字符串格式化占位方式。这里面的占位变量会被真实的字符串替换。

可以预见,匿名函数的参数的多个参数和函数体的功能很强大,类似其他语言的函数重载,即函数签名和参数列表重新定义函数。但是Elixir通过参数模式匹配来执行对应的函数体,实际上类似一个逻辑分支,实现Elixir的控制结构。为啥不用ifelse等结构呢?if else确实是标准的流程控制,可是Elixir中,很少使用它们。模式匹配能够比它们更好的工作,也更简洁。后面再单独介绍ifelse等控制结构。

函数返回函数

有用动态语言编程经验的开发者也行不会模式,函数除了返回值,也可以返回一个函数。Elixir的匿名函数也可以返另外一个匿名函数。

iex(1)> func = fn -> fn -> "hello" end end
#Function<20.90072148/0 in :erl_eval.expr/5>
iex(2)> func.()
#Function<20.90072148/0 in :erl_eval.expr/5>
iex(3)> func.().()
"hello"

写成一行比较难懂,更好的格式化如下:

func = fn ->
  fn ->
    "Hello"
  end
end

变量fun1绑定了一个函数。这个函数没有参数,函数体又定义了一个新的匿名函数,内函也没有参数,但是返回一个字符串“Hello”。

调用func.()调用外函数的时候,返回内函数。然后使用func.().()调用内函数结果。

写func.().()看起来不正常。可以调用外函数再绑定一个新的变量。再通过新的变量去调用内函数:

iex(4)> other = func.()
#Function<20.90072148/0 in :erl_eval.expr/5>
iex(5)> other.()
"hello"

函数存储的原始环境

再看匿名函数的特性:

iex> greeter = fn name -> (fn -> "Hello #{name}" end) end
#Function<12.17052888 in :erl_eval.expr/5>
iex> dave_greeter = greeter.("Dave")
#Function<12.17052888 in :erl_eval.expr/5>
iex> dave_greeter.()
"Hello Dave"

上面定义了带有name参数的外函数。与所有参数一样,函数体name都是可用的。内函数把name与格式化字符串返回。

当我们调用外函数的时候,返回内函数。此时,还没有把name传递到内涵上中。但是当我再次嗲用内涵上的时候(dave_greeter.())。此时name将会替换内函数的格式化占位符。

但是有一些奇怪的事情发生。内函数使用了外还是的name参数。在greeter.("Dave")调用的时候返回了,此时外函数其实已经执行完毕了,参数应该离开其作用域了。但是,当我们返回内涵上的时候,实际上还是可以愉快的使用外函数的参数。

这样是可行的,因为Elixir的函数在定义的时候,就会自动的与作用域的变量进行绑定。例子中,当我们的定义了内函数,它继承了外函数作用域的变量绑定。这就是闭包作用域绑定了变量,然后保持这个状态知道被使用。

参数化函数

在上一个例子。外函数接受一个参数,而内函数没有参数。让我们尝试一下不同的例子,即内函数也有参数的情况:

iex> add_n = fn n -> (fn other -> n + other end) end
#Function<12.17052888 in :erl_eval.expr/5>
iex> add_two = add_n.(2)
#Function<12.17052888 in :erl_eval.expr/5>
iex> add_five = add_n.(5)
#Function<12.17052888 in :erl_eval.expr/5>
iex> add_two.(3)
5
iex> add_five.(7)
12

例子中,内函数使用外函数的变量n,并和其自身的参数other相加。每一次我们调用外函数,就传递了一个n,然后返货一个把n当成其自己参数的函数。

把函数当成参数传递

函数也是值,因此我们可以把其传给别的函数。

iex> times_2 = fn n -> n * 2 end
#Function<12.17052888 in :erl_eval.expr/5>
iex> apply = fn (fun, value) -> fun.(value) end
#Function<12.17052888 in :erl_eval.expr/5>
iex> apply.(times_2, 6)
12

这个例子中,apply绑定匿名函数,其中第一个参数是一个匿名函数。它返回第一个参数(匿名函数)使用第二个参数作为其自身的参数调用的结果。

传递函数将会让Elixir代码无所不能。例如,内建的Enum模块有一个map函数。这个函数接受两个参数,一个容器集合和一个参数函数。该函数将容器里的每一个元素传入参数函数进行调用,并将调用的结果返回的值放入新容器,并返回新容器。看下面的例子:

iex> list = [1, 3, 5, 7, 9]
[1, 3, 5, 7, 9]
iex> Enum.map list, fn elem -> elem * 2 end
[2, 6, 10, 14, 18]
iex> Enum.map list, fn elem -> elem * elem end
[1, 9, 25, 49, 81]
iex> Enum.map list, fn elem -> elem > 6 end
[false, false, false, true, true]

&符号

创建简短的函数十分普遍,因此Elixir提供了一个缩写的语法糖。下看看例子我们再继续:

iex> add_one = &(&1 + 1)        # 等价于 add_one = fn (n) -> n + 1 end
#Function<6.17052888 in :erl_eval.expr/5>
iex> add_one.(44)
45
iex> square = &(&1 * &1)
#Function<6.17052888 in :erl_eval.expr/5>
iex> square.(8)
64
iex> speak = &(IO.puts(&1))
&IO.puts/1
iex> speak.("Hello")
Hello
:ok

&可以将表达式转换成函数。在表达式中,&1, &2是函数参数占位符,分别表示第一个和第二个参数。&(&1 + &2) 可以把表达式 p1 + p2 转换成 fn p1, p2 -> p1 + p2 end

Elixir很智能。仔细看前一个例子speak。Elixir生成了一个匿名函数,也就是 &(IO.puts(&1))将会转换成 fn x -> IO.puts(x) end。但是Elixir可以识别函数体重有一个命名函数---IO模块的puts函数,其中匿名函数的第一个参数也正是这个函数的参数。Elixir可以使用&符号做为函数IO.puts/1的引用来作进一步优化。

如下的例子,函数的参数必须填写正确的顺序:

iex> rnd = &(Float.round(&1, &2))
&Float.round/2
iex> rnd.(5.673, 2)
5.67
iex> rnd = &(Float.round(&2, &1))
#Function<12.17052888 in :erl_eval.expr/5>
iex> rnd.(2, 5.673)
5.67

你可能注意到了定义的函数的时候弹出了引用Erlang的信息。因为Elixir是运行在Erlang虚拟机上的。如果你使用&abs(&1)会看到更多的证据。Elixir的引用Erlang库的绝对值abs函数,只需要写&:erlang.abs/1

iex> abs -1
1
iex> abs_fn = &:erlang.abs/1
&:erlang.abs/1
iex> abs_fn.(-1)
1

因为[]{}是Elixir中定义列表和元组的操作符,它们也可以转成成函数。下面的函数就是繁华了一个元组,里面有两个表达式,分别对两个参数做整除和取余操作。

iex> divrem = &{ div(&1,&2), rem(&1,&2) }
#Function<12.17052888 in :erl_eval.expr/5>
iex> divrem.(13, 5)
{2, 3}

&还有另外一种形式。你可以用&和函数的定义式(函数名和参数个数的形式,function name/arity)来引用函数,会返回一个可调用的匿名函数。匿名函数的参数将会被传到这个引用函数中。

我们已经知道:当我们在iex中输入&(IO.puts(&1))putsIO模块的函数,它接收一个参数。Elixir中它的函数定义式为IO.puts/1。如果我们在这个表达式前加上&,我们将会得到这个函数的引用。可以理解为这样的组合得到了一个函数的别名。来看几个例子:

iex> l = &length/1   # length/1 是一个函数,使用&引用length/1 函数,得到一个length/1 函数的别名。使用别名调用该函数(匿名函数方式调用)。
&:erlang.length/1
iex> l.([1,3,5,7])
4
iex> len = &Enum.count/1
&Enum.count/1
iex> len.([1,2,3,4])
4
iex> m = &Kernel.min/2  # Erlang 函数的别名
&:erlang.min/2
iex> m.(99,88)
88

我们写了命名函数的定义式,就像我们定义了函数一样。(实际上并没有定义函数,而是引用了函数而已)。

&其实是匿名函数的简写,在Elixir中很有用:

iex> Enum.map [1,2,3,4], &(&1 + 1)
[2, 3, 4, 5]
iex> Enum.map [1,2,3,4], &(&1 * &1)
[1, 4, 9, 16]
iex> Enum.map [1,2,3,4], &(&1 < 3)
[true, true, false, false]

函数是核心

毫无疑问,函数式Elixir中的核心。函数编程的基础是数据转换。函数是转换的引擎,它们就是Elixir的心脏。

目前为止,已经介绍了匿名函数,并绑定给变量。其实函数也是可以有名字的。

Elixir也支持命名函数(named function )。Elixir命名函数定义在模块内,对,接下来我们将窥视Elixir的模块和命名函数。

你可能感兴趣的:(Elixir 简明笔记(九)--- 匿名函数)