Elixir 编程Elixir & PhoenixElixir

Elixir 简明笔记(十)--- 命名函数

2016-01-11  本文已影响706人  人世间

程序由几行代码开始,逐渐变多之后,开发者期望一种结构化的方式组织代码。Elixir模块化代码很简单。你只需要将函数拆分写入模块中,当然Elixir中的函数(指命名函数,接下来除非特别指出匿名函数,否则所有函数都指命名函数)在模块中定义。

先看一个简单的例子。在你的目录中创建一个Elixir源文件times.exs

mm/times.exs

defmodule Times do
    def double(n) do
        n * 2
    end
end

我们定义了一个Times模块。该模块包含一个单独的double函数。该函数只有一个参数,在Elixir中,是通过参数的个数来区分不同模块的函数子句。例如上面的函数定义式是double/1

编译模块

有两种方式编译文件。第一种使用使用iex载入文件并编译:

$ iex times.exs
iex> Times.double 4
8

命令行中输入iex + 文件名就可以编译源文件并将编译结果的上下文载入iex提示界面。

如果你先进入了iex交互提示界面,可以使用c帮助函数进行编译。

iex> c "times.exs"
[Times]
iex> Times.double(4)
8
iex> Times.double(123)
246

这一行c "times.exs"代码将对源文件进行编译并载入编译后的程序。使用模块加函数名即可调用函数,即Times.double

如果给函数传入一个整数将会发生什么呢?

iex> Times.double("cat")
** (ArithmeticError) bad argument in arithmetic expression
times.exs:3: Times.double/1

堆栈信息中可以看见发生了一个ArithmeticError异常,第一行告诉我们发生了什么错误。下一行告诉我们错误发生在什么地方。注意错误信息中是定义式Times.double/1描述出错的函数。

Elixir中通过函数名及其参数个数(arity)来识别不同的函数子句。double函数有一个参数,因此被标记为double/1。如果它有三个函数,则会被标注为double/3。无需为Elixir担心无法区分这两个函数。设想一下,如果有两个同名函数挨着,即使同名有不同的参数个数,对于人还是无法容易的分辨。为了避免这种情况,请尽量不用把不相关的函数使用相同的名字。

函数体也是一个块

do...end块是组织表达式和代码的一种方式。Elxir的模块和函数使用他们定义,控制代码结构,以及任何需要处理的代码体。

可是,do…end并不是基础语法。他们的本质是像这样的:

def double(n), do: n * 2

可以给do:提供任何使用小括号包裹的表达式。

def greet(greeting, name), do: (
    IO.puts greeting
    IO.puts "How're you doing, #{name}?"
)

do…end只是组织代码块的一个语法糖,他们都可以转换成do: from的形式(do: from本身并没有特别之处,只是一个简单的关键字列表)。人们习惯使用do: form语法来表示单行的代码块的时候,使用do…end组织多行代码块。

例如我们的例子其实可以改写一下:

mm/times1.exs

defmodule Times do
    def double(n), do: n * 2
end

甚至可以这么写:

defmodule Times, do: (def double(n), do: n*2)

(最好不好这样做,可读性不好)

函数调用及其模式匹配

前一章,我们了解了匿名函数式的参数是如果通过模式匹配将参数应用于函数体。这个规则对于命名函数也同样适用。不同的地方在于,命名函数需要写多个不同参数列表和函数体子句的函数。尽管看起来像是定义了多个函数,其实这是一个函数的多个子句而已。

当你调用一个命名函数的时候,Elixir会首先把你传入的参数和第一个函数子句进行模式匹配。只有前面的函数子句没有匹配,才会尝试和后面的函数子句进行匹配(记住,参数的个数必须和模式匹配相同)。直到匹配完所有的函数子句。

我们来玩一玩。编写一个从1到n的阶乘(n!)函数factorial。通常0!等于1。
类似数学语言的表达:

factorial(0) → 1
factorial(n) → n * factorial(n-1)

这是一个阶乘的伪代码,但是它已经和Elixir实现很接近了。

mm/factorial1.exs

defmodule Factorial do

    def of(0), do: 1
    def of(n), do: n * of(n-1)
end

这里我们定了两个同名的函数of。调用Factorial.of(2),Elixir会用参数2与第一个of函数的参数模式进行匹配,0不匹配2,第一个函数匹配失败。然后,Elixir会把这个参数和第二个函数进行模式匹配n=2,匹配成功,然后把n传入该子句的函数体进行求值运算。其中由递归的调用of(2-1)。此时同样的的处理过程,还是第二个函数匹配调用,再接下来递归调用,Factorial.of(0),此时则匹配第一个函数了。该函数执行返回1,直到递归回溯完毕。Elixir会自动回溯栈,将所有数字相乘法,然后返回答案。这个factorial模块实现了正常的功能,但是这段代码还可以使用尾递归优化提高。

先运行看看结果:

iex> c "factorial1.exs"
[Factorial]
iex> Factorial.of(3)
6
iex> Factorial.of(7)
5040
iex> Factorial.of(10)
3628800
iex> Factorial.of(1000)
40238726007709377354370243392300398571937486421071463254379991042993851239862
90205920442084869694048004799886101971960586316668729948085589013238296699445
...
00624271243416909004153690105933983835777939410970027753472000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000

Elixir中使用模式匹配设计和编程很普遍(几乎所有函数式语言都如此)。先来说一下最简单的情况下,一个过程有了明确的答案。将是解决问题的切入点。然后找一个递归解决方案,将这些过程集合处理。

更多的例子:

列表的长度

& 任何列表的长度都等于第一个元素的长度1加上剩余列表元素的长度

重要的知识点:将上面的语句用代码表达,将会写出多个子句函数。Elixir总是从最先定义的函数进行模式匹配。一旦前面的满足了要求,就不会往下匹配了。所有下面的代码将不会工作:

mm/factorial1-bad.exs

defmodule BadFactorial do
    def of(n), do: n * of(n-1)
    def of(0), do: 1
end

无论是什么参数,第一个函数子句都能匹配,第二个函数永远不会被执行。当然,出现这种情况,Elixir会在定义之后给出一个警告:

iex> c "factorial1-bad.exs"
.../factorial1-bad.ex:3: this clause cannot match because a previous clause at

卫子句

通过传入参数的模式匹配,Elixir可以选择匹配的函数调用。但是,当我们需要匹配的结果是基于不同类型或者是匹配特定的值的时候该怎么办呢?不用担心,Elixir提供了卫子句。这是通过关键字给函数定义添加谓词。当模式匹配的时候,Elxir首先会使用参数的模式匹配,然后在针对when关键字的子句进行逻辑判断,如果条件为真,才执行对于的函数子句。

mm/guard.exs

defmodule Guard do

    def what_is(x) when is_number(x) do
        IO.puts "#{x} is a number"
    end
    
    def what_is(x) when is_list(x) do
        IO.puts "#{inspect(x)} is a list"
    end

    def what_is(x) when is_atom(x) do
        IO.puts "#{x} is an atom"
    end
end

Guard.what_is(99) # => 99 is a number

Guard.what_is(:cat) # => cat is an atom

Guard.what_is([1,2,3]) # => [1,2,3] is a list

从新看看阶乘factorial模块

mm/factorial1.exs

defmodule Factorial do

    def of(0), do: 1

    def of(n), do: n * of(n-1)

end

如果我们传入一个负数,那么这段代码将会进入死循环,一直递归调用,永远不会满足递归结果的条件,即n等于0。因此我们可以通过卫子句对参数进行过滤,才能愉快的玩耍。

mm/factorial2.exs

defmodule Factorial do

    def of(0), do: 1

    def of(n) when n > 0 do
        n * of(n-1)
    end
end

此时调用的时候再传入负数,第二个匹配的函数将会执行卫子句检查,返回false将不会执行函数子句,即没有函数匹配。(没有函数匹配将会抛出异常)

iex> c "factorial2.exs"
[Factorial]
iex> Factorial.of -100
** (FunctionClauseError) no function clause matching in Factorial.of/1...


#### 卫子句的限制 

你只能写一些特定的Elixir表达式。下面的例子来自官方的Getting Started guide

* 比较操作符

    ==, !=, ===, !==, >, <, <=, >=

* 布尔操作符和取反操作符

    or, and, not, !.  注意:|| 和 && 是不能用的.

* 算术操作符

+, -, *, /

* 连接操作符

    <> and ++

* in操作符
    成员的集合和范围



### 类型检测函数


下面是一些内建类型检测函数,具体可以参考其官方文档。


* is_atom
* is_binary 
* is_bitstring 
* is_boolean 
* is_exception 
* is_float 
* is_function 
* is_integer 
* is_l
* ist 
* is_map 
* is_number 
* is_pid 
* is_port 
* is_record 
* is_reference 
* is_tuple


### 其他函数

下面的内建函数将返回参数的值,而不是`true` 或者 `false`

* abs(number) 
* bit_size(bitstring) 
* byte_size(bitstring) 
* div(number,number) 
* elem(tuple, n) 
* float(term) 
* hd(list) 
* length(list) 
* node() 
* node(pid|ref|port) 
* rem(number,number) 
* round(number) 
* self() 
* tl(list) 
* trunc(number) 
* tuple_size(tuple) 


### 默认参数


定义命名函数的时候,可以使用`\\`语法符给函数的参数设置默认值。调用函数的时候,如果该参数没有传值,则函数使用默认的参数。Elixir还会检测调用函数的时候所传入的参数个数,如果传入的参数少于必须参数,则不会匹配成功。如果正好满足必须的参数,那么必须参数则使用传入的参数进行匹配,其他的就使用其默认值。如果传入的参数个数大于必须参数的个数,那么就会匹配必须参数之后,覆盖默认参数的值。参数匹配是由左向右的。

mm/default_params.exs

```elixir
defmodule Example do

    def func(p1, p2 \\ 2, p3 \\ 3, p4) do
        IO.inspect [p1, p2, p3, p4]
    end
end

Example.func("a", "b") # => ["a",2,3,"b"]
Example.func("a", "b", "c") # => ["a","b",3,"c"]
Example.func("a", "b", "c", "d") # => ["a","b","c","d"]

Elixir模式匹配的时候,默认参数有惊人的表现。录入,编译下面代码:

def func(p1, p2 \\ 2, p3 \\ 3, p4) do
    IO.inspect [p1, p2, p3, p4]
end

def func(p1, p2) do
    IO.inspect [p1, p2]
end

你会得到错误:

** (CompileError) default_params.exs:7: def func/2 conflicts with
defaults from def func/4

因为第一个函数子句的定义(带有默认参数),无论参数是两个,三个还是四个,几乎可以匹配任何第二个函数子句的模式。也就是第二个函数永远不会被匹配。

对于默认函数,还有一点需要知道。下面一个函数两个函数头(双函数子句)的情况,只不过另外一个写清楚了默认值:

mm/default_params1.exs

defmodule DefaultParams1 do

    def func(p1, p2 \\ 123) do
        IO.inspect [p1, p2]
    end


    def func(p1, 99) do
        IO.puts "you said 99"
    end
end

编译会报错(两个子句都有默认的参数了):

** (CompileError) default_params1.exs.exs:8: def func/2 has default
values and multiple clauses, define a function head

使用默认参数

为了降低默认值带来的函数冲突。会把带有默认参数定义写在函数的头部,只需要声明却不写函数体。剩下的函数子句就使用正常的参数。下面的例子中,默认参数的声明写在了头部,这个规则使用下面两个函数的子句。

mm/default_params2.exs

defmodule Params do

    def func(p1, p2 \\ 123)

    def func(p1, p2) when is_list(p1) do

        "You said #{p2} with a list"
    end

    def func(p1, p2) do

        "You passed in #{p1} and #{p2}"
    end
end

IO.puts Params.func(99)             # You passed in 99 and 123 
IO.puts Params.func(99, "cat")      # You passed in 99 and cat
IO.puts Params.func([99])           # You said 123 with a list
IO.puts Params.func([99], "dog")    # You said dog with a list

上面的定义,其实等价于(只不过把默认参数提取出来了):

defmodule Params do
    def func(p1, p2 \\ 123) when is_list(p1) do

        "You said #{p2} with a list"
    end

    def func(p1, p2 \\ 123) do

        "You passed in #{p1} and #{p2}"
    end
end

私有函数

defp宏可以定义声明一个私有函数,私有函数只能在声明它的模块内部调用。

也可以定义多个私有函数,就像使用def定义函数一样。可是,不能定义参数一致的同名公有和私有函数。正如下面是非法的:

def fun(a) when is_list(a), do: true

defp fun(a), do: false
上一篇下一篇

猜你喜欢

热点阅读