程序由幾行代碼開始惩琉,逐漸變多之后,開發(fā)者期望一種結(jié)構(gòu)化的方式組織代碼。Elixir模塊化代碼很簡單囊颅。你只需要將函數(shù)拆分寫入模塊中炉峰,當然Elixir中的函數(shù)(指命名函數(shù)畏妖,接下來除非特別指出匿名函數(shù),否則所有函數(shù)都指命名函數(shù))在模塊中定義疼阔。
先看一個簡單的例子戒劫。在你的目錄中創(chuàng)建一個Elixir源文件times.exs
mm/times.exs
defmodule Times do
def double(n) do
n * 2
end
end
我們定義了一個Times
模塊。該模塊包含一個單獨的double
函數(shù)婆廊。該函數(shù)只有一個參數(shù)谱仪,在Elixir中,是通過參數(shù)的個數(shù)來區(qū)分不同模塊的函數(shù)子句否彩。例如上面的函數(shù)定義式是double/1
編譯模塊
有兩種方式編譯文件疯攒。第一種使用使用iex載入文件并編譯:
$ iex times.exs
iex> Times.double 4
8
命令行中輸入iex + 文件名就可以編譯源文件并將編譯結(jié)果的上下文載入iex提示界面。
如果你先進入了iex交互提示界面列荔,可以使用c幫助函數(shù)進行編譯敬尺。
iex> c "times.exs"
[Times]
iex> Times.double(4)
8
iex> Times.double(123)
246
這一行c "times.exs"
代碼將對源文件進行編譯并載入編譯后的程序。使用模塊加函數(shù)名即可調(diào)用函數(shù)贴浙,即Times.double
砂吞。
如果給函數(shù)傳入一個整數(shù)將會發(fā)生什么呢?
iex> Times.double("cat")
** (ArithmeticError) bad argument in arithmetic expression
times.exs:3: Times.double/1
堆棧信息中可以看見發(fā)生了一個ArithmeticError
異常崎溃,第一行告訴我們發(fā)生了什么錯誤蜻直。下一行告訴我們錯誤發(fā)生在什么地方。注意錯誤信息中是定義式Times.double/1
描述出錯的函數(shù)袁串。
Elixir中通過函數(shù)名及其參數(shù)個數(shù)(arity)來識別不同的函數(shù)子句概而。double
函數(shù)有一個參數(shù),因此被標記為double/1
囱修。如果它有三個函數(shù)赎瑰,則會被標注為double/3
。無需為Elixir擔(dān)心無法區(qū)分這兩個函數(shù)破镰。設(shè)想一下餐曼,如果有兩個同名函數(shù)挨著压储,即使同名有不同的參數(shù)個數(shù),對于人還是無法容易的分辨源譬。為了避免這種情況集惋,請盡量不用把不相關(guān)的函數(shù)使用相同的名字。
函數(shù)體也是一個塊
do...end
塊是組織表達式和代碼的一種方式踩娘。Elxir的模塊和函數(shù)使用他們定義刮刑,控制代碼結(jié)構(gòu),以及任何需要處理的代碼體霸饲。
可是为朋,do…end
并不是基礎(chǔ)語法。他們的本質(zhì)是像這樣的:
def double(n), do: n * 2
可以給do:
提供任何使用小括號包裹的表達式厚脉。
def greet(greeting, name), do: (
IO.puts greeting
IO.puts "How're you doing, #{name}?"
)
do…end
只是組織代碼塊的一個語法糖习寸,他們都可以轉(zhuǎn)換成do: from
的形式(do: from
本身并沒有特別之處,只是一個簡單的關(guān)鍵字列表)傻工。人們習(xí)慣使用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)
(最好不好這樣做中捆,可讀性不好)
函數(shù)調(diào)用及其模式匹配
前一章鸯匹,我們了解了匿名函數(shù)式的參數(shù)是如果通過模式匹配將參數(shù)應(yīng)用于函數(shù)體。這個規(guī)則對于命名函數(shù)也同樣適用泄伪。不同的地方在于殴蓬,命名函數(shù)需要寫多個不同參數(shù)列表和函數(shù)體子句的函數(shù)。盡管看起來像是定義了多個函數(shù)蟋滴,其實這是一個函數(shù)的多個子句而已染厅。
當你調(diào)用一個命名函數(shù)的時候,Elixir會首先把你傳入的參數(shù)和第一個函數(shù)子句進行模式匹配津函。只有前面的函數(shù)子句沒有匹配肖粮,才會嘗試和后面的函數(shù)子句進行匹配(記住,參數(shù)的個數(shù)必須和模式匹配相同)尔苦。直到匹配完所有的函數(shù)子句涩馆。
我們來玩一玩。編寫一個從1到n的階乘(n!
)函數(shù)factorial
允坚。通常0!等于1魂那。
類似數(shù)學(xué)語言的表達:
factorial(0) → 1
factorial(n) → n * factorial(n-1)
這是一個階乘的偽代碼,但是它已經(jīng)和Elixir實現(xiàn)很接近了屋讶。
mm/factorial1.exs
defmodule Factorial do
def of(0), do: 1
def of(n), do: n * of(n-1)
end
這里我們定了兩個同名的函數(shù)of
冰寻。調(diào)用Factorial.of(2)
,Elixir會用參數(shù)2與第一個of函數(shù)的參數(shù)模式進行匹配皿渗,0不匹配2斩芭,第一個函數(shù)匹配失敗。然后乐疆,Elixir會把這個參數(shù)和第二個函數(shù)進行模式匹配n=2
划乖,匹配成功,然后把n
傳入該子句的函數(shù)體進行求值運算挤土。其中由遞歸的調(diào)用of(2-1)
琴庵。此時同樣的的處理過程,還是第二個函數(shù)匹配調(diào)用仰美,再接下來遞歸調(diào)用迷殿,Factorial.of(0)
,此時則匹配第一個函數(shù)了咖杂。該函數(shù)執(zhí)行返回1庆寺,直到遞歸回溯完畢。Elixir會自動回溯棧诉字,將所有數(shù)字相乘法懦尝,然后返回答案。這個factorial
模塊實現(xiàn)了正常的功能壤圃,但是這段代碼還可以使用尾遞歸優(yōu)化提高陵霉。
先運行看看結(jié)果:
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中使用模式匹配設(shè)計和編程很普遍(幾乎所有函數(shù)式語言都如此)。先來說一下最簡單的情況下伍绳,一個過程有了明確的答案踊挠。將是解決問題的切入點。然后找一個遞歸解決方案冲杀,將這些過程集合處理效床。
更多的例子:
n個數(shù)的和
第一個0的和是0
第n個數(shù)的和又n-1個數(shù)的和與n相加。
列表的長度
- 空列表的長度是0
& 任何列表的長度都等于第一個元素的長度1加上剩余列表元素的長度
重要的知識點:將上面的語句用代碼表達漠趁,將會寫出多個子句函數(shù)扁凛。Elixir總是從最先定義的函數(shù)進行模式匹配。一旦前面的滿足了要求闯传,就不會往下匹配了谨朝。所有下面的代碼將不會工作:
mm/factorial1-bad.exs
defmodule BadFactorial do
def of(n), do: n * of(n-1)
def of(0), do: 1
end
無論是什么參數(shù),第一個函數(shù)子句都能匹配甥绿,第二個函數(shù)永遠不會被執(zhí)行字币。當然,出現(xiàn)這種情況共缕,Elixir會在定義之后給出一個警告:
iex> c "factorial1-bad.exs"
.../factorial1-bad.ex:3: this clause cannot match because a previous clause at
衛(wèi)子句
通過傳入?yún)?shù)的模式匹配洗出,Elixir可以選擇匹配的函數(shù)調(diào)用。但是图谷,當我們需要匹配的結(jié)果是基于不同類型或者是匹配特定的值的時候該怎么辦呢翩活?不用擔(dān)心阱洪,Elixir提供了衛(wèi)子句。這是通過關(guān)鍵字給函數(shù)定義添加謂詞菠镇。當模式匹配的時候冗荸,Elxir首先會使用參數(shù)的模式匹配,然后在針對when關(guān)鍵字的子句進行邏輯判斷利耍,如果條件為真蚌本,才執(zhí)行對于的函數(shù)子句。
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
如果我們傳入一個負數(shù)隘梨,那么這段代碼將會進入死循環(huán)程癌,一直遞歸調(diào)用,永遠不會滿足遞歸結(jié)果的條件轴猎,即n等于0嵌莉。因此我們可以通過衛(wèi)子句對參數(shù)進行過濾,才能愉快的玩耍税稼。
mm/factorial2.exs
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0 do
n * of(n-1)
end
end
此時調(diào)用的時候再傳入負數(shù)烦秩,第二個匹配的函數(shù)將會執(zhí)行衛(wèi)子句檢查,返回false將不會執(zhí)行函數(shù)子句郎仆,即沒有函數(shù)匹配只祠。(沒有函數(shù)匹配將會拋出異常)
iex> c "factorial2.exs"
[Factorial]
iex> Factorial.of -100
** (FunctionClauseError) no function clause matching in Factorial.of/1...
#### 衛(wèi)子句的限制
你只能寫一些特定的Elixir表達式。下面的例子來自官方的Getting Started guide
* 比較操作符
==, !=, ===, !==, >, <, <=, >=
* 布爾操作符和取反操作符
or, and, not, !. 注意:|| 和 && 是不能用的.
* 算術(shù)操作符
+, -, *, /
* 連接操作符
<> and ++
* in操作符
成員的集合和范圍
### 類型檢測函數(shù)
下面是一些內(nèi)建類型檢測函數(shù)扰肌,具體可以參考其官方文檔抛寝。
* 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
### 其他函數(shù)
下面的內(nèi)建函數(shù)將返回參數(shù)的值,而不是`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)
### 默認參數(shù)
定義命名函數(shù)的時候曙旭,可以使用`\\`語法符給函數(shù)的參數(shù)設(shè)置默認值盗舰。調(diào)用函數(shù)的時候,如果該參數(shù)沒有傳值桂躏,則函數(shù)使用默認的參數(shù)钻趋。Elixir還會檢測調(diào)用函數(shù)的時候所傳入的參數(shù)個數(shù),如果傳入的參數(shù)少于必須參數(shù)剂习,則不會匹配成功蛮位。如果正好滿足必須的參數(shù),那么必須參數(shù)則使用傳入的參數(shù)進行匹配鳞绕,其他的就使用其默認值失仁。如果傳入的參數(shù)個數(shù)大于必須參數(shù)的個數(shù),那么就會匹配必須參數(shù)之后们何,覆蓋默認參數(shù)的值萄焦。參數(shù)匹配是由左向右的。
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模式匹配的時候冤竹,默認參數(shù)有驚人的表現(xiàn)拂封。錄入茬射,編譯下面代碼:
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
因為第一個函數(shù)子句的定義(帶有默認參數(shù)),無論參數(shù)是兩個烘苹,三個還是四個躲株,幾乎可以匹配任何第二個函數(shù)子句的模式片部。也就是第二個函數(shù)永遠不會被匹配镣衡。
對于默認函數(shù),還有一點需要知道档悠。下面一個函數(shù)兩個函數(shù)頭(雙函數(shù)子句)的情況廊鸥,只不過另外一個寫清楚了默認值:
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
編譯會報錯(兩個子句都有默認的參數(shù)了):
** (CompileError) default_params1.exs.exs:8: def func/2 has default
values and multiple clauses, define a function head
使用默認參數(shù)
為了降低默認值帶來的函數(shù)沖突。會把帶有默認參數(shù)定義寫在函數(shù)的頭部辖所,只需要聲明卻不寫函數(shù)體惰说。剩下的函數(shù)子句就使用正常的參數(shù)。下面的例子中缘回,默認參數(shù)的聲明寫在了頭部吆视,這個規(guī)則使用下面兩個函數(shù)的子句。
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
上面的定義酥宴,其實等價于(只不過把默認參數(shù)提取出來了):
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
私有函數(shù)
defp
宏可以定義聲明一個私有函數(shù)啦吧,私有函數(shù)只能在聲明它的模塊內(nèi)部調(diào)用。
也可以定義多個私有函數(shù)拙寡,就像使用def
定義函數(shù)一樣授滓。可是肆糕,不能定義參數(shù)一致的同名公有和私有函數(shù)般堆。正如下面是非法的:
def fun(a) when is_list(a), do: true
defp fun(a), do: false