Elixir 簡明筆記(十)--- 命名函數(shù)

程序由幾行代碼開始惩琉,逐漸變多之后,開發(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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诚啃,隨后出現(xiàn)的幾起案子淮摔,更是在濱河造成了極大的恐慌,老刑警劉巖始赎,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件和橙,死亡現(xiàn)場離奇詭異,居然都是意外死亡极阅,警方通過查閱死者的電腦和手機胃碾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筋搏,“玉大人仆百,你說我怎么就攤上這事”计辏” “怎么了俄周?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵吁讨,是天一觀的道長。 經(jīng)常有香客問我峦朗,道長建丧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任波势,我火速辦了婚禮翎朱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘尺铣。我一直安慰自己拴曲,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布凛忿。 她就那樣靜靜地躺著澈灼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪店溢。 梳的紋絲不亂的頭發(fā)上叁熔,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音床牧,去河邊找鬼荣回。 笑死,一個胖子當著我的面吹牛叠赦,可吹牛的內(nèi)容都是我干的驹马。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼除秀,長吁一口氣:“原來是場噩夢啊……” “哼糯累!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起册踩,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤泳姐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后暂吉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胖秒,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年慕的,在試婚紗的時候發(fā)現(xiàn)自己被綠了阎肝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡肮街,死狀恐怖风题,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤沛硅,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布眼刃,位于F島的核電站,受9級特大地震影響摇肌,放射性物質(zhì)發(fā)生泄漏擂红。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一围小、第九天 我趴在偏房一處隱蔽的房頂上張望昵骤。 院中可真熱鬧,春花似錦吩抓、人聲如沸涉茧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至伦连,卻和暖如春雨饺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惑淳。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工额港, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人歧焦。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓移斩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親绢馍。 傳聞我的和親對象是個殘疾皇子向瓷,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容