你的程式碼聞起來很臭嗎?不用擔心!大夫幫你...越治越臭

這是 Elixir 從零開始 系列 11 唷

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 12 唷

前言

這是一篇結束文,本來在 Exercism round 3 之後就要寫了...

但是中間有些事情耽擱了好好好久,直到最近才想到要補這篇文章 XD

回想我全馬鐵人挑戰的第一篇文章發下豪語說,要雙贏:學新東西+獎盃

是是是...是有學到新東西啦,但是獎盃還是沒有(看來我就跟獎盃無緣啊啊啊)

現在我們就回頭來看看 Elixir

我們從頭開始介紹了基礎知識,然後利用了 Exercism 來練習所學,但是啊

這只是基礎中的基礎喔,我們不過學了皮毛,想要再進階除了練習再練習,還是要多練習啊!

最後,希望我的基礎系列文能有幫助到想要學習 Elixir 的人!

這是 Elixir 從零開始 系列 10 唷

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 11 唷

前言

我們繼續再戰~練習得越多,就越熟悉!

Ex5: Bob

題目

Bob is a lackadaisical teenager. In conversation, his responses are very limited.

Bob answers ‘Sure.’ if you ask him a question.

He answers ‘Whoa, chill out!’ if you yell at him.

He answers ‘Calm down, I know what I’m doing!’ if you yell a question at him.

He says ‘Fine. Be that way!’ if you address him without actually saying anything.

He answers ‘Whatever.’ to anything else.

Bob’s conversational partner is a purist when it comes to written communication and always follows normal rules regarding sentence punctuation in English.

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defmodule Bob do
def hey(input) do
cond do
String.trim(input) == "" -> "Fine. Be that way!"
shouting?(input) && question?(input) -> "Calm down, I know what I'm doing!"
shouting?(input) -> "Whoa, chill out!"
question?(input) -> "Sure."
true -> "Whatever."
end
end

defp upcase?(input), do: input == String.upcase(input)
defp contain_letter?(input), do: String.match?(input, ~r/[[:alpha:]]/)
defp question?(input), do: String.trim(input) |> String.ends_with?("?")
defp shouting?(input), do: contain_letter?(input) && upcase?(input)
end

重點提示

這題的重點是在做條件篩選,當遇到什麼條件時要做什麼事情。

依照題目的講解,我們來看 test case 可以分析出五個條件

  • 什麼話都沒有講的時候,要回答 "Fine. Be that way!"
  • 講話內容去掉空白且最後一個字是問號的時候,要回答 "Sure."
  • 講話內容都是大寫的時候,要回答 "Whoa, chill out!"
  • 講話內容都是大寫且最後一個字又是問號的時候,要回答 "Calm down, I know what I'm doing!"
  • 不是以上條件,要回答 "Whatever."

根據以上我們可以定義一些函式,例如 upcase?contain_letter?question? 等等,利用他們來排列組合以上五點條件,詳細內容就參考程解答吧

Ex6: Beer Song

題目

這題的題目很長很長…

Recite the lyrics to that beloved classic, that field-trip favorite: 99 Bottles of Beer on the Wall.

Note that not all verses are identical.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
99 bottles of beer on the wall, 99 bottles of beer.
Take one down and pass it around, 98 bottles of beer on the wall.

98 bottles of beer on the wall, 98 bottles of beer.
Take one down and pass it around, 97 bottles of beer on the wall.

97 bottles of beer on the wall, 97 bottles of beer.
Take one down and pass it around, 96 bottles of beer on the wall.
...
2 bottles of beer on the wall, 2 bottles of beer.
Take one down and pass it around, 1 bottle of beer on the wall.

1 bottle of beer on the wall, 1 bottle of beer.
Take it down and pass it around, no more bottles of beer on the wall.

No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.

For bonus points

Did you get the tests passing and the code clean? If you want to, these are some additional things you could try:

  • Remove as much duplication as you possibly can.
  • Optimize for readability, even if it means introducing duplication.
  • If you’ve removed all the duplication, do you have a lot of conditionals? Try replacing the conditionals with polymorphism, if it applies in this language. How readable is it?

Then please share your thoughts in a comment on the submission. Did this experiment make the code better? Worse? Did you learn anything from it?

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
defmodule BeerSong do
@doc """
Get a single verse of the beer song
"""
@spec verse(integer) :: String.t()
def verse(0) do
"""
No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.
"""
end

def verse(1) do
"""
1 bottle of beer on the wall, 1 bottle of beer.
Take it down and pass it around, no more bottles of beer on the wall.
"""
end

def verse(number) do
"""
#{number} bottles of beer on the wall, #{number} bottles of beer.
Take one down and pass it around, #{number - 1} bottles of beer on the wall.
"""
end

@doc """
Get the entire beer song for a given range of numbers of bottles.
"""
@spec lyrics(Range.t()) :: String.t()
def lyrics(default \\99..0) do
default
|> Enum.map(&verse/1)
|> Enum.join("\n")
end
end

重點提示

Enum.map 的定義如下

map(enumerable, fun)

Returns a list where each element is the result of invoking fun on each corresponding element of enumerable.

For maps, the function expects a key-value tuple.

Range 是個 enumerable,所以我們可以使用 map 這個函式,針對裡面所有的元素都來執行 fun,這邊也是一個典型的利用自我遞迴來取代迴圈的例子喔

參考資料

  1. https://hexdocs.pm/elixir/String.html#trim/1
  2. https://hexdocs.pm/elixir/String.html#upcase/2
  3. https://hexdocs.pm/elixir/String.html#match?/2
  4. https://hexdocs.pm/elixir/Enum.html#map/2
  5. https://hexdocs.pm/elixir/Enum.html#join/2

這是 Elixir 從零開始 系列 09 唷

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 10 唷

前言

寫練習題的好處多多,一來可以消化之前所學,二來更符合實際應用,其實就是『坐而思,不如起而行』

Ex3: Word Count

題目

Given a phrase, count the occurrences of each word in that phrase.

For example for the input “olly olly in come free

1
2
3
4
olly: 2
in: 1
come: 1
free: 1

Words are compared case-insensitively. The keys are lowercase.

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defmodule WordCount do
@doc """
Count the number of words in the sentence.

Words are compared case-insensitively.
"""
@spec count(String.t()) :: map
def count(sentence) do
String.downcase(sentence)
|> String.split([" ", "_", ":", "&", "@", "%", "^", "$", ",", "!"], trim: true)
|> Enum.reduce(%{}, &word_add/2)
end

defp word_add(word, acc) do
Map.update(acc, word, 1, &(&1 + 1))
end
end

重點提示

首先介紹 |>,它會將左側的結果傳遞到右側,如果右側是函式,則是傳遞至函式的第一個參數,因此,第一步驟我們把 sentence 全部轉成小寫後利用 |> 傳遞給字串切割,split 會將字串切割後回傳列表(list)

Enum.reduce 定義如下

reduce(enumerable, acc, fun)

Invokes fun for each element in the enumerable with the accumulator.

The initial value of the accumulator is acc. The function is invoked for each element in the enumerable with the accumulator. The result returned by the function is used as the accumulator for the next iteration. The function returns the last accumulator.

Examples

1
2
Enum.reduce([1, 2, 3], 5, fn x, acc -> x + acc end)
11

在程式碼內的 Enum.reduce(%{}, &word_add/2) 此處解釋:

  1. 第一個參數 enumerablesplit 所吐出之列表,利用 |> 傳遞過來,會長的類似 [“aaa”, “bbb”, “ccc”]
  2. 第二個參數 acc%{} 是一個空的 map 用來儲存字母的數量,它同樣也是 accumulator 的初始值
  3. 第三個參數 fun 為函式 word_add/2,所要帶入的兩個參數就是 enumerableacc,會回傳 accumulator,也就是 map,這個 map(accumulator) 會被 word_add(Map.update) 不斷更新

Map.update 定義如下

update(map, key, initial, fun)

Updates the key in map with the given function.

If key is present in map with value value, fun is invoked with argument value and its result is used as the new value of key. If key is not present in map, initial is inserted as the value of key. The initial value will not be passed through the update function.

Map.update(acc, word, 1, &(&1 + 1)) 此處的解釋為從 accEnum.reduce 中的 accumulator) 這個 map 中找尋是否有 word 有的話就更新(利用函式 &(&1 + 1)),沒有的話就新增 wordacc 中,最後回傳 map(accumulator)

Ex4: Roman Numerals

題目

Write a function to convert from normal numbers to Roman Numerals.

The Romans were a clever bunch. They conquered most of Europe and ruled it for hundreds of years. They invented concrete and straight roads and even bikinis. One thing they never discovered though was the number zero. This made writing and dating extensive histories of their exploits slightly more challenging, but the system of numbers they came up with is still in use today. For example the BBC uses Roman numerals to date their programmes.

The Romans wrote numbers using letters - I, V, X, L, C, D, M. (notice these letters have lots of straight lines and are hence easy to hack into stone tablets).

1
2
3
 1  => I
10 => X
7 => VII

There is no need to be able to convert numbers larger than about 3000. (The Romans themselves didn’t tend to go any higher)

Wikipedia says: Modern Roman numerals … are written by expressing each digit separately starting with the left most digit and skipping any digit with a value of zero.

To see this in practice, consider the example of 1990.

In Roman numerals 1990 is MCMXC:

1000=M 900=CM 90=XC

2008 is written as MMVIII:

2000=MM 8=VIII

See also: http://www.novaroma.org/via_romana/numbers.html

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
defmodule RomanNumerals do

# module attributes
@mapping [
{1_000, "M"},
{900, "CM"},
{500, "D"},
{400, "CD"},
{100, "C"},
{90, "XC"},
{50, "L"},
{40, "XL"},
{10, "X"},
{9, "IX"},
{5, "V"},
{4, "IV"},
{1, "I"}
]

@doc """
Convert the number to a roman number.
"""
@spec numeral(pos_integer) :: String.t()
def numeral(0), do: ""
def numeral(number) do
# 檢查數字有沒有大於 list, 有的話則輸出該羅馬字並且數字相減, 重複此步驟
{match_value_x, match_value_y} = Enum.find(@mapping, fn {x, _} -> number >= x end)
# 使用 <> 來連接輸出之羅馬字
match_value_y <> numeral(number - match_value_x)
end
end

重點提示

這題比較簡單,就是考驗怎麼把迴圈的想法轉換成 Elixir 函式,這裡我們直接看註解就可啦

參考資料

  1. https://hexdocs.pm/elixir/Enum.html#reduce/3
  2. https://hexdocs.pm/elixir/Map.html#update/4
  3. https://hexdocs.pm/elixir/Kernel.html#%7C%3E/2

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 9 唷

這是 Elixir 從零開始 系列 08 唷

前言

我們零零總總也「看」好多篇文章,現在是時候來動手做做看了!Exercism 是一個程式碼練習的網站,裡面有很多程式語言可以選擇,每個語言都有很多的練習題可以練習,更讓人興奮的是呢,我們可以上傳我們的程式碼,讓線上導師給予評論,或者我們也可以看到世界各地的菁英上傳的答案喔,透過線上導師的提醒或參考別人的程式碼,可以讓自己的實力更上一層樓喔,讓我們透過真正實作,把之前講的都化為應用上來吧!

Exercism installation

讓我們一步一步來做

  1. 我們進入網站 Exercism

  2. 建立 Exercism 的帳號,Exercism 提供直接用 Github 作為登錄帳號,或是你可以建立一組新的帳號密碼

  3. 點選程式語言 Elixir,第一個練習題將會是很熟悉的 Hello World

  4. 開始之前呢,我們要先確定我們的電腦裡面有 Homebrew,這是一個 Mac 電腦上面的套件管理程式

    對的沒錯,筆者的練習平台是用 Mac 電腦唷

  5. 透過 brew update && brew install exercism 來安裝 exercism cli,這個工具的功能就是負責幫你下載題目到你的電腦上以及把寫好的程式碼上傳到 Exercism

  6. 測試指令 exercism version ,來測試一下安裝是否成功

  7. exercism cli 與您的Exercism 的帳號綁定,讓他們做一個關聯,我們輸入 exercism configure --token=your-token-key,這邊的 your-token-key 可以到網站的 setting page 裡面去找找

  8. 回到練習題 Hello World 的頁面

  9. 頁面右上角的 Begin walk-through 其實也有寫到前面幾點的步驟,偷偷跟你們講其實呢不一定要 Mac,Windows 和 Linux 也可以跑啦

  10. 頁面右手邊有個 Download 連結,複製貼上並在終端機執行,這個連結是用來下載題目,使用編輯器去編輯下載好的檔案,寫出符合題目的答案出來!

  11. 頁面右手邊還有個 Submit 的連結,複製貼上並在終端機執行,這個連結是用來上傳答案的,可以選擇上傳一個檔案或是多個檔案,上傳好之後呢到網站看一下,重新讀取一下,很快地就會看到你的答案被上傳了啦

  12. 完成!

我們今天來做個幾題測試測試,讓大家上手一下喔!

Ex1: Hello World

題目

The classical introductory exercise. Just say “Hello, World!”.

“Hello, World!” is the traditional first program for beginning programming in a new language or environment.

The objectives are simple:

  • Write a function that returns the string “Hello, World!”.
  • Run the test suite and make sure that it succeeds.
  • Submit your solution and check it at the website.

If everything goes well, you will be ready to fetch your first real exercise.

解答

這一題要我們寫一個函式,功能是印出 Hello World 出來

1
2
3
4
5
6
7
8
defmodule HelloWorld do

@spec hello() :: String.t
@spec hello(String.t) :: String.t
def hello(name \\ "World") do
"Hello, #{name}!"
end
end

重點提示

  1. 反斜線 \\ 代表的是函式的預設值(可以參考 Function & Module
  2. 如果要在字串中使用變數要使用 #{} 來包含,比如說 #{1 + 1} 得到的字串會回傳 2,#{name} 會得到變數name的值(可以參考 基本資料型別與運算

Ex2: RNA Transcription

題目

Given a DNA strand, return its RNA complement (per RNA transcription).

Both DNA and RNA strands are a sequence of nucleotides.

The four nucleotides found in DNA are adenine (A), cytosine (C), guanine (G) and thymine (T).

The four nucleotides found in RNA are adenine (A), cytosine (C), guanine (G) and uracil (U).

Given a DNA strand, its transcribed RNA strand is formed by replacing each nucleotide with its complement:

  • G -> C
  • C -> G
  • T -> A
  • A -> U

解答

第二個範例是要做字元的交換

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defmodule RnaTranscription do
@spec to_rna([char]) :: [char]
def to_rna(dna) do
Enum.map(dna, fn (x) -> change_word(x) end)
end

def change_word(input) do
case input do
?G -> ?C
?C -> ?G
?T -> ?A
?A -> ?U
_ -> input
end
end
end

重點提示

  1. _ 代表是所有其它條件都會進入
  2. ?G 這個問號是一個 construct ,功能為轉換字元成 integer code point(可以參考 1)
  3. Enum.map 將每一個元素都用後面的函式執行過一次並回填其值(可以參考 3)
  4. fn (x) -> change_word(x) end 這是一個匿名函式(可以參考 Function & Module

以上兩題小試身手,透過寫題庫的方式讓自己熟練語法,有沒有回到高中的感覺呢,讓大家熟悉熟悉一下,未來會有更多的練習題分享大家喔

參考資料

  1. https://hexdocs.pm/elixir/String.html

  2. https://exercism.io/

  3. https://hexdocs.pm/elixir/Enum.html#map/2

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 8 唷

這是 Elixir 從零開始 系列 07 唷

前言

在這篇文章之前,我們大部分都用 IEx 來操作,但是如果我們之後要進行大型的專案,更多的程式碼,更複雜的架構,單單只靠 IEx 是不夠的喔,因此我們需要專案管理工具!

Mix

Mix tool 的定義如下

Mix 是一個編譯工具,它提供建立、編譯與測試 Elixir 專案,它也可進行依賴套件的管理等

在這裡,我們將學會怎麼利用 Mix tool 建立新專案

Build a new project

我們到終端機底下測試一下 Elixir 環境是否正常

1
2
3
4
$ elixir -v
Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [hipe]

Elixir 1.10.1 (compiled with Erlang/OTP 22)

再來我們利用 mix new 新增一個專案,mix new 的用法如下

1
mix new PATH [--app APP] [--module MODULE] [--sup] [--umbrella]

我們可以用 --app 來幫 application 取名稱,更多詳細可以參考 這裡 唷,這裡我們新建立一個 my_app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$mix new my_app

* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/my_app.ex
* creating test
* creating test/test_helper.exs
* creating test/my_app_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

cd my_app
mix test

Run "mix help" for more commands.

建好之後呢,我們找到 lib/my_app.ex,在這裡我們可以新增我們的自己想要的函式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
defmodule MyApp do
@moduledoc """
Documentation for `MyApp`.
"""

@doc """
Hello world.

\## Examples

iex> MyApp.hello()
:world

"""
def hello do
:world
end
end

這邊預設了一個函式 hello,讓我們再多新增一些函式到 my_app.ex 裡面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def hello(:one) do
"hello world"
end

def hello(:two) do
"hello world hello world"
end

def hello(:three) do
"hello world hello world hello world"
end

def hello(_) do
"null"
end

好了之後呢,我們進到剛剛建好的專案資料夾內,我們可以使用兩個方法

  1. mix compile 單純做編譯
  2. 或直接 iex -S mix 編譯後進入 iex,這會載入剛建立好的應用程式
1
2
3
4
5
6
7
$ cd my_app
$ iex -S mix
Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [hipe]

Compiling 1 file (.ex)
Interactive Elixir (1.10.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

iex 內我們輸入我們剛剛建好的Module MyApp 後按下 tab

1
iex(1)> MyApp.

我們可以看到出現了 hello/0hello/1,這兩個都是我們剛剛建好的函式有沒有

1
MixProject    hello/0       hello/1

試著執行看看囉

1
2
3
4
iex(1)> MyApp.hello(:two)
"hello world hello world"
iex(2)> MyApp.hello(:three)
"hello world hello world hello world"

如果我們亂打一些參數進去 hello 裡面呢?可以看到我們用了之前學到的技巧 hello(_)_ 代表不在乎,也就是輸入什麼都可接受,然後被 pattern matching 到此函式內囉

1
2
3
4
5
6
iex(3)> MyApp.hello(:yoooo)
"null"
iex(4)> MyApp.hello(400)
"null"
iex(5)> MyApp.hello(3.14)
"null"

那可能會有人會問,那我修改函式之後怎麼重新編譯呢?有沒有什麼簡單的方法咧?

答案是有的,而且滿簡單的,使用 recompile 即可

我們更改了 my_app.ex 的內容,重新編譯後會回傳 :ok

1
2
3
iex(10)> recompile
Compiling 1 file (.ex)
:ok

我們沒有更改 my_app.ex 的內容,重新編譯後會回傳 :noop

1
2
iex(9)> recompile
:noop

Mix format

在我們利用 mix new 新增專案後,會一併產生檔案 .formatter.exs,這個檔案其實是給 mix format 使用的,mix format 是一個 task,它可以自動格式化你的程式碼,讓你的程式碼符合規則更漂亮一點,我們可以測試一下,修改原本的檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defmodule MyApp do
def hello do
:world
end
def hello(:one) do
"hello world"
end
def hello(:two) do
"hello world hello world"
end
def hello(:three) do
"hello world hello world hello world"
end
def hello(_) do
"null"
end
end

再執行 mix format 看看,可以發現,程式碼又變整齊了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
defmodule MyApp do
def hello do
:world
end

def hello(:one) do
"hello world"
end

def hello(:two) do
"hello world hello world"
end

def hello(:three) do
"hello world hello world hello world"
end

def hello(_) do
"null"
end
end

這邊建議每次寫完程式都執行一下讓程式碼變得美美香香的喔

參考資料

  1. https://hexdocs.pm/mix/Mix.html#content
  2. https://hexdocs.pm/mix/Mix.Tasks.New.html
  3. https://hexdocs.pm/mix/Mix.Tasks.Format.html

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 7 唷

這是 Elixir 從零開始 系列 06 唷

前言

Pattern matching 又稱模式比對,是 Elixir 內很重要的一個環節也是一個特色,很多地方都會使用到,強烈建議必讀必看喔

The match operator

如果是在一般的程式語言中,比如說 C# 或 Java 中,= 在這段程式碼內的意思就是

把 22 指派給變數 val

1
int val = 22;

但是當我們換到 Elixir 中,就沒有所謂指派給,這種運算子,打個比方,在之前的文章中常常會看到以下類似的程式碼

1
iex(1)> person = {"Bob", 25}

在這裡,在等號左邊的 person 我們稱呼它為 pattern,在等號右邊的 {"Bob", 25} 則可以稱呼為 termperson = {"Bob", 25} 這整句話的意思呢,我們可以這樣說

We match the variable pseron to the right-side term {"Bob", 25}

或者我們可這樣想

想像成拼圖,當等號左邊(pseron)與等號右邊({"Bob", 25})長得很像時,就可以合理推測兩邊的外型是一樣的(同樣都是 tuple ),同時間,外型一樣就代表內容也是一樣的,內容一樣就等於 pseron 的內容就是 {"Bob", 25}

以上就一個模式比對的解釋,可能聽不懂?沒關係!讓我們來舉更多更多的例子來解釋

Fail matching

模式比對都是要比對成功的,那如果是一個失敗的比對呢?這左右兩邊的外型長的根本不一樣,自然也就匹配不起來囉

1
2
iex(4)> {name, age} = "hello world"
** (MatchError) no match of right hand side value: "hello world"

再一發

1
2
3
4
iex(5)> {amount, amount, amount} = {127, 127, 127}
{127, 127, 127}
iex(6)> {amount, amount, amount} = {127, 127, 1}
** (MatchError) no match of right hand side value: {127, 127, 1}

這裡的 amount 被匹配為 127,當下一行再次做比對的時候會發現失敗,因為左邊的 pattern 形狀是必須三個變數都長得一樣才行,有一個 127 不等於 1 就不行啦

Matching tuples

更多的例子來了

1
iex(1)> {name, age} = {"Bob", 25}

利用模式比對來比較的話,name 的值為 "Bob"age 的值為 25

再來一個

1
2
3
4
5
6
iex(1)> {_, time} = :calendar.local_time()
iex(2)> time
{20, 44, 18}
iex(3)> {_, {hour, _, _}} = :calendar.local_time()
iex(4)> hour
20

Matching constants

這很無聊,就是自己比對自己,得到的答案會是自己

1
2
iex(1)> 1 = 1
1

但我們今天把 person 匹配給 {:person, "Bob", 25},並且自己比對自己,我們可以發現,比對成功!

1
2
3
4
iex(2)> person = {:person, "Bob", 25}

iex(3)> {:person, name, age} = person
{:person, "Bob", 25}

Matching lists

1
2
3
4
5
6
7
8
9
10
iex(1)> [first, second, third] = [1, 2, 3]
[1, 2, 3]

iex(3)> [head | tail] = [1, 2, 3]
[1, 2, 3]

iex(4)> head
1
iex(5)> tail
[2, 3]

Matching maps

1
2
3
4
5
6
7
8
iex(1)> %{name: name, age: age} = %{name: "Bob", age: 25}
%{age: 25, name: "Bob"}

iex(2)> name
"Bob"

iex(3)> age
25

要注意一點是,在 map 中可以不用全部匹配,以下這樣部分匹配也行,但沒有 key 是匹配不起來的

1
2
3
4
iex(4)> %{age: age} = %{name: "Bob", age: 25}

iex(5)> age
25
1
2
iex(6)> %{age: age, works_at: works_at} = %{name: "Bob", age: 25}
** (MatchError) no match of right hand side value

Matching with functions

我們來看一個函式長怎麼樣子

1
2
3
def my_fun(arg1, arg2) do
...
end

在這裡的 arg1 和 arg2 是 patterns,而 term 就是我們在呼叫它的時候輸入的變數

1
2
3
4
5
defmodule TestModule do
def my_fun(val1, val2) do
val1 * val2
end
end

我們可以這樣

1
2
iex(1)> TestModule.my_fun(2, 3)
6

但我們不能這樣,這樣會造成匹配失敗

1
iex(1)> TestModule.my_fun({2, 3})

接下來我們來介紹 Matching with functions 的一個延伸應用,在一般的程式語言中,如果我們要使用條件分支,我們或許可以這樣寫

1
2
3
4
5
function test(x){
if (x < 0) return "negative";
if (x == 0) return "zero";
return "positive";
}

但到了 Elixir 中,我們必須改成變一下寫法,我們可以使用 Multiclause functions 來達到一樣的效果,如下

1
2
3
4
5
defmodule TestNum do
def test(x) when x < 0, do: :negative
def test(0), do: :zero
def test(x), do: :positive
end

所謂的 Multiclause functions 其實就是多個 Matching with function,藉由 pattern matching 讓每一個條件都可以進入一個 function 中處理事情,達到一樣條件分支的效果喔

Multiclause functions

在 Elixir 中允許 overload,也就是同一名稱函式可以出現多次,差別只在輸入的變數不一樣,假設今天我們要計算面積,不管是圓型或是長方形都要使用同一個函式來計算,那我們可以這樣做

1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule Geometry do
def area({:rectangle, a, b}) do
a * b
end

def area({:square, a}) do
a * a
end

def area({:circle, r}) do
r * r * 3.14
end
end

我們可以看到,在 Geometry 這個 Module 中定義了 rectangle、square 和 circle,唯獨沒有定義 triangle,所以當我們在 iex 上嘗試想要輸入 {:triangle, 1, 2, 3} 的時候會失敗,因為我們匹配不到任何東西

1
2
3
4
5
6
7
8
9
iex(1)> Geometry.area({:rectangle, 4, 5})
20
iex(2)> Geometry.area({:square, 5})
25
iex(3)> Geometry.area({:circle, 4})
50.24
iex(4)> Geometry.area({:triangle, 1, 2, 3})
** (FunctionClauseError) no function clause matching in Geometry.area/1
geometry.ex:2: Geometry.area({:triangle, 1, 2, 3})

那好假設我們不想要出現 error 要什麼修改 Module 呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defmodule Geometry do
def area({:rectangle, a, b}) do
a * b
end

def area({:square, a}) do
a * a
end

def area({:circle, r}) do
r * r * 3.14
end

def area(unknown) do
{:error, {:unknown_shape, unknown}}
end
end

我們新增一個 pattern,讓任何奇怪形狀的輸入都可進到 area(unknown) 來,這樣一來所有的 input 皆可被捕捉到啦

注意,area(unknown) 不可以放在 Module 的第一行當作第一個函式,這樣所有的 input 都只會跑到第一個 pattern 內,而不會進入第二第三個 pattern

1
2
3
4
5
iex(1)> Geometry.area({:square, 5})
25

iex(2)> Geometry.area({:triangle, 1, 2, 3})
{:error, {:unknown_shape, {:triangle, 1, 2, 3}}}

Multiclause lambdas

一個正常的匿名函式(lambda)長這樣

1
2
3
iex(1)> double = fn x -> x*2 end
iex(2)> double.(3)
6

讓我們讓它變得更豐富一點,變成 Multiclause lambdas

1
2
3
4
5
6
7
8
iex(3)> test_num =
fn
x when is_number(x) and x < 0 ->
:negative
0 -> :zero
x when is_number(x) and x > 0 ->
:positive
end

那我們可以測試一下

1
2
3
4
5
6
iex(4)> test_num.(-1)
:negative
iex(5)> test_num.(0)
:zero
iex(6)> test_num.(1)
:positive

參考資料

  1. https://elixirschool.com/en/lessons/basics/pattern-matching/

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 6 唷

這是 Elixir 從零開始 系列 05 唷

前言

遞迴在 Elixir 中是一個很重要的部分,讓我們趕緊來瞧瞧

不可變特性(Immutability)

對於 Functional programming language (Elixir 也是成員之一)來說,有一個特性叫不可變特性(Immutability),什麼是不可變特性呢?在 Elixir 中所有的資料都是不可修改的,像是所有的函數回傳一定都是回傳一個新的值,那要修改一個特定變數呢?

1
2
3
4
iex(1)> var1 = 20
20
iex(2)> var1 = 40
40

如以上範例,看起來我們更改了 var1 的值,但實際上內部作業不是這樣的

要更改變數的唯一方法就是給予同樣的變數名稱但是賦予它新一個記憶體位置

由於不可變特性的關係, Loop 在 Elixir 或者說 Functional programming language 中不存在,舉例子來說好了

For loop

1
2
3
4
5
int[] a1 = new int[10];
for (int i = 0; i < 10; i++)
{
a1[i] = i;
}

While loop

1
2
3
4
5
6
int n = 0;
while (n < 5)
{
Console.WriteLine(n);
n++;
}

以上 C# 兩個 Loop 範例,我們變動了 in 和 array 的值,這在指令式程式設計是一個很常見的用法,但在 Elixir 裡面可不行這樣做喔,取而代之的是遞迴

Rescursion

今天假設我們要設計一個 Loop,功能為輸入次數與字串,輸出為次數*字串,那我們可以這樣做

1
2
3
4
5
6
7
8
9
10
defmodule Recursion do
def print_multiple_times(msg, 1) do
IO.puts msg
end

def print_multiple_times(msg, n) do
IO.puts msg
print_multiple_times(msg, n - 1)
end
end

我們可看到結果

1
2
3
4
5
iex(1)> Recursion.print_multiple_times("hello",3)
hello
hello
hello
:ok

這就是一個利用遞迴取代迴圈的作法,不斷的呼叫 print_multiple_times(msg, n) 來達到迴圈的效果,同時間也設立一個 print_multiple_times(msg, 1) 作為遞迴停止的條件,我抓了上面的範例並加了註解

1
2
3
4
5
iex(1)> Recursion.print_multiple_times("hello",3)
hello # print_multiple_times(msg, n)
hello # print_multiple_times(msg, n)
hello # print_multiple_times(msg, 1)
:ok

Tail function calls

你們可能會有一個問題是,我每一個 Loop 都用遞迴取代,當有遞迴次數很多的時候,那有可能會發生 stack overflow,針對這樣的問題,Elixir 提供了一個解決方案 tail-call optimization,當函式的最後一行是呼叫自己或是另一函式,該行我們叫它 Tail call,當呼叫到該行時,此時 Elixir 並不會新產生一個堆疊空間(Stack space)來配置給新函式, 而是利用 JUMP 的概念在原來的堆疊空間操作新的函式,利用此一方法,就不會有不斷新增新的堆疊空間問題,我們就可以毫不羞恥的使用遞迴啦(誤 XD

以下是一個 tail call

1
2
3
4
5
6
7
def fun(...) do
...
if something do
...
another_fun(...) #Tail call
end
end

以下不是一個 tail call,因為最後一行不是單純函式呼叫

1
2
3
def fun(...) do
1 + another_fun(...) # Not a tail call
end

參考資料

  1. https://elixir-lang.org/getting-started/recursion.html#loops-through-recursion
  2. https://en.wikipedia.org/wiki/Tail_call

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 5 唷

這是 Elixir 從零開始 系列 04 唷

前言

要寫這篇之前,回頭來看前面寫的幾篇文章,我發現其實我應該把 Function & Module 擺在這篇後面比較有從基礎到進階的感覺,不過算了懶得改,覺得奇怪的彭油記得自己跳著看壓 XD。

這一章節我們來介紹一些條件分支(Conditional branching)的語法,說白話點也就是程式語言最基本的 ifelse

if, else and unless

用法如下

1
2
3
4
5
if condition do
...
else
...
end

當然我們也可以用一行解決

1
if condition, do: something, else: another_thing

我們來看範例

1
2
3
4
5
if value> 20 do
IO.puts("Look !")
else
IO.puts("Yo Yo")
end

再來看一個範例

1
2
3
def max(a, b) do
if a >= b, do: a, else: b
end

我們來介紹 unless 這個 macro,它其實就是 if (not …)

利用上面範例

1
2
3
def max(a, b) do
unless a >= b, do: b, else: a
end

cond

cond 也是一個macro,用法如下

1
2
3
4
5
6
7
8
cond do
expression_1 ->
...

expression_2 ->
...
...
end

來看個範例

1
2
3
4
5
6
def max(a, b) do
cond do
a >= b -> a
true -> b
end
end

這裡我們可以注意一下,上述程式加 true -> b 的緣由是為了讓剩下的條件都被 true 捕捉到,這種用法值得學習一下唷

case

cond 長得很像,但是其實是不一樣的,用法如下

1
2
3
4
5
6
7
8
case expression do
pattern_1 ->
...

pattern_2 ->
...
...
end

來看一個範例

1
2
3
4
5
6
def max(a,b) do
case a >= b do
true -> a
_ -> b
end
end

這裡跟上面的 cond 一樣,使用 _ 可以捕捉剩下的其他條件,但如果沒有使用 _ 比對時將失敗,進而引發一個錯誤訊息

想像一下 _ 將會比對該行以上條件之外的所有條件,就很類似 C# 中的 default 或者我們可以說 anything else

1
2
3
4
5
6
7
8
9
10
11
12
switch (caseSwitch)
{
case 1:
Console.WriteLine("Case 1");
break;
case 2:
Console.WriteLine("Case 2");
break;
default:
Console.WriteLine("Default case");
break;
}

參考資料

  1. https://elixir-lang.org/getting-started/case-cond-and-if.html#case
  2. https://elixirschool.com/zh-hant/lessons/basics/control-structures/

前言

其實這個問題存在心中很久了,只是我一直以來只會使用 Commit and Push,所以問題就一直擺著 XD,今天我們就來介紹一下!

差異

在 VS2017 中,Commit 按鈕會有三個選項可以選

  • Commit
  • Commit and Push
  • Commit and Sync

三者的差異呢?

  1. Commit 會記錄更動過的地方並且上傳(from working directory to git directory)到本地端的 repository
  2. Commit and Push 則是上面動作再加上 push 到 remote repository
  3. Commit and Sync 會做三件事情,Commit,Pull 和 Push

參考資料

  1. https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2013/hh850436(v=vs.120)?redirectedfrom=MSDN

這是 w3HexSchool 鼠年全馬鐵人挑戰 Week 4 唷

這是 Elixir 從零開始 系列 03 唷

前言

前一篇講完 function 和 module,現在我們來回頭介紹一些基本的型別系統囉

基礎型別

Numbers

整數

1
2
iex(1)> 3
3

二進位、八進位和十六進位

1
2
3
4
5
6
iex(4)> 0xff
255
iex(5)> 0b011
3
iex(6)> 0o11
9

浮點數

1
2
3
4
iex(3)> 3.14
3.14
iex(4)> 1.0e-2
0.01

一個神奇的語法糖,底線會被 Elixir 忽略,通常用來方便閱讀標記

1
2
iex(10)> 1_000_000
1000000

Atoms

Atom

Atom 是一個被命名的常數,它很類似 C++ 的 enumeration,在使用上以冒號開頭,後面搭配一個名稱,而這個名稱就代表它本身(的值),在 runtime 的時候,這個名稱就會被存在 atom table 內,當你把一個 atom 指派給某個變數的時候,這個 atom 不會直接被指派而是使用參考的方式從 atom table 中使用 atom

我對於 atom 的理解就是,把它當作一個 key-value 的 key 使用,這個 key 本身沒有意義,就只是一個名稱,只有在 key 有對應關係時才有意義,舉個例子,我想要創造一個變數叫做 name 並且把這個變數當作是 key-value 中的 key,我只要建立一個 atom 名為 :name 就可以使用它了,如下範例,這個 atom 在 map 中的關係就對應到 "Bob" 這個字串這樣子

1
2
3
4
5
:name
:age
:works_at

iex(1)> bob = %{:name => "Bob", :age => 25, :works_at => "Initech"}

以下這裡我們建立兩個 atom,可以看到這兩個(的值)是不相等的

1
2
3
4
5
6
iex(9)> :foo
:foo
iex(10)> :bat
:bat
iex(11)> :foo == :bat
false

Atoms as Booleans

在 Elixir 中,並沒有專用的 Boolean 型別,反而使用 :true:false 這兩個 atom,這裡也有一個語法糖,我們可以拿掉冒號,如下例子

1
2
3
4
iex(1)> :true == true
true
iex(2)> :false == false
true

Falsy and truthy values

在 C++ 中 的 null 在 Elixir 中是一個叫 :nil 的 atom,它跟 Booleans 一樣有語法糖可以省略冒號

1
2
iex(1)> nil == :nil
true

在 Elixir 中有一個特別的規則 falsy and truthy values,它的分類如下表格

truthy falsy
everything else nil and false

由於有這樣的一個規則存在,我們可以看到以下

1
2
iex(1)> nil || false || 5 || true
5

nilfalse 是 falsy,而 5 是 truthy,因為 || 的特性為有 true 之後的後面值就可以忽略,所以上面這行最後結果回傳是 5

strings

根據 Elixir in Action 的介紹,字串解釋如下

Elixir uses UTF-8 to encode its strings, which means that code points are encoded as a series of bytes.

因為字串是 bytes 的組合,所我們要注意一個特性,如下

1
2
iex(1)> is_binary("hello")
true

直接來看一些範例

1
2
iex(1)> "This is a string"
"This is a string"

字串插值的用法為 #{}

1
2
3
4
5
iex(2)> "Embedded expression: #{3 + 0.14}"
"Embedded expression: 3.14"
iex(3)> name = "Sean"
iex(4)> "Hello #{name}"
"Hello Sean"

字串串接,我們使用 <> 運算子來串接

1
2
3
iex(1)> name = "Sean"
iex(2)> "Hello " <> name
"Hello Sean"

可以支援多行字串,我們稱呼它 heredocs syntax

1
2
3
4
iex(9)> """
Heredoc must end on its own line
"""
"Heredoc must end on its own line\n"

新介紹!

字元列表(character list)

1
2
iex(10)> 'hełło'
[104, 101, 322, 322, 111]

使用雙引號表示為字串,使用單引號則表示為字元列表,在 Elixir 中我們通常是使用字串,會用到字元列表是因為有些純 Erlang library 用到,這裡就不多解釋

sigils

這裡還有一個語法要介紹 sigils,這個中文翻譯叫符咒?!所有的符咒將以波浪符號 ~ 開頭,已經有內建一些了,或是我們也可以自創符咒,這部分有興趣可以自行 Google,這邊我只介紹跟字串有關的符咒

  • ~S
  • ~s

小寫 s 可以處裡字串插值,而大寫就是原封不動的印出來所有的東西

1
2
3
4
iex(7)> ~S|fff #{3+0.14}|
"fff \#{3+0.14}"
iex(8)> ~s|fff #{3+0.14}|
"fff 3.14"

用來分隔符號清單如下,這些都可以用

  • <...> 尖括號 (pointy brackets)
  • {...} 大括號 (curly brackets)
  • [...] 中括號 (square brackets)
  • (...) 小括號 (parentheses)
  • |...| 管線符號 (pipes)
  • /.../ 斜線 (forward slashes)
  • "..." 雙引號 (double quotes)
  • '...' 單引號 (single quotes)
1
2
3
4
5
6
iex(3)> ~s/the cat in the hat on the mat/
"the cat in the hat on the mat"
iex(4)> ~s(This is also a string)
"This is also a string"
iex(5)> ~s|This is also a string|
"This is also a string"

(Linked) Lists

列表(List)是資料的群集,使用中括號 [...] 定義,裡面可以包含任何型態唷

1
2
3
4
iex(1)> [3.14, :pie, "Apple"]
[3.14, :pie, "Apple"]
iex(2)> prime_numbers = [2, 3, 5, 7]
[2, 3, 5, 7]

列表看起來像是 array 但是它其實比較像是 singly linked list,所以多數針對列表的操作都是 O(n),比如說函式 Kernel.length/1 或是 Enum.at/2,這兩個函式的操作都要從頭開始執行,時間複雜度就是 O(n),因此,有一個特性要特別提醒的就是,通常前置插入 (prepend) 比後綴置入 (append) 更快

以下是 O(n) 操作

1
2
3
4
5
iex(2)> length(prime_numbers)
4

iex(3)> length(prime_numbers)
4

以下是 prepend vs append

1
2
3
4
5
6
7
8
iex(4)> list = [3.14, :pie, "Apple"]
[3.14, :pie, "Apple"]
# Prepending (fast)
iex(5)> ["π" | list]
["π", 3.14, :pie, "Apple"]
# Appending (slow)
iex(6)> list ++ ["Cherry"]
[3.14, :pie, "Apple", "Cherry"]

列表串接使用 ++/2 運算子

1
2
iex(11)> [1, 2, 3] ++ [4, 5]
[1, 2, 3, 4, 5]

通過提供 --/2 運算子支援減法;即使減去不存在的值也是安全的

1
2
iex(12)> ["foo", :bar, 42] -- [42, "bar"]
["foo", :bar]

注意重複的值。對於右邊的每個元素,左邊中第一個出現的將被移除

1
2
iex(13)> [1,2,2,3,2,3] -- [1,2,3,2]
[2, 3]

注意!

減法為嚴格匹配

1
2
3
4
iex(1)> [2] -- [2.0]
[2]
iex(2)> [2.0] -- [2.0]
[]

使用列表時,常常操作列表的頭和尾。 頭是列表的第一個元素,而尾是包含剩餘元素的列表。 在這個部份的操作中 Elixir 提供了兩個有用的函式 hdtl

1
2
3
4
iex(14)> hd [3.14, :pie, "Apple"]
3.14
iex(15)> tl [3.14, :pie, "Apple"]
[:pie, "Apple"]

Tuples

中文翻譯成元組?!我覺得這翻譯很奇怪就是了。這是一組沒有型態的結構,使用大括號 {...} 來定義

1
2
iex(1)> person = {"Bob", 25}
{"Bob", 25}

我們可以很常看到用來做為函式回傳接收

1
2
3
4
iex(1)> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex(2)> File.read("path/to/unknown/file")
{:error, :enoent}

Maps

映射是一組 key-value 的對應關係,我們使用 %{} 來定義它,並使用 => 來做對應

1
2
3
4
5
6
7
8
9
10
11
12
iex(2)> squares = %{1 => 1, 2 => 4, 3 => 9}
iex(3)> squares[2]
4
iex(4)>
nil

iex(5)> map = %{:foo => "bar", "hello" => :world}
%{:foo => "bar", "hello" => :world}
iex(6)> map[:foo]
"bar"
iex(7)> map["hello"]
:world

我們也可這樣用

1
2
iex(8)> map.foo
"bar"

2020/03/17 update:

有另外一種特殊的語法,使用冒號來區分,如下範例

1
2
3
4
iex> %{foo: "bar", hello: "world"}
%{foo: "bar", hello: "world"}
iex> %{foo: "bar", hello: "world"} == %{:foo => "bar", :hello => "world"}
true

Keyword lists

關鍵字列表其實是上面介紹過的 list 和 tuple 的組合,我們直接看範例

1
[ method: "", path: "", resp_body: "", status: nil ]

事實上在內部表示為

1
[ {:method, ""}, {:path, ""}, {:resp_body, ""}, {:status, nil} ]

所以呢,我們可以總結幾點

  • 關鍵字列表就是一個列表(list)
  • 列表內的元素全部都是 tuple
  • Tuple 的第一個元素必須為 atom

基礎運算

基本的四則運算不可少!

1
2
3
4
5
6
7
8
iex(1)> 2 + 2
4
iex(2)> 2 - 1
1
iex(3)> 2 * 5
10
iex(4)> 10 / 5
2.0

這裡的除法 永遠會回傳浮點數

如果要取餘數或是整數的除法的話可以用以下方法

1
2
3
4
iex(1)> div(10, 5)
2
iex(2)> rem(10, 3)
1

andornot,搭配這三個操作的第一個參數必須是 truefalse

1
2
3
4
5
6
7
8
9
10
11
12
13
iex(23)> true and 'a'
'a'
iex(24)> false and 'a'
false
iex(25)> not false
true
iex(26)> false or 'a'
'a'
iex(26)> 12 and 3
** (ArgumentError) argument error: 12
iex(26)> not 12
** (ArgumentError) argument error
:erlang.not(12)

Elixir 提供 ||&&!

|| 運算子會回傳第一個不是 falsy 的值

&& 運算子,如果第一個表達式為 truthy 則會回傳第二個表達式,反之,回傳第一個表達式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
iex(1)> -20 || true
-20
iex(2)> false || 42
42

iex(3)> 42 && true
true
iex(4)> 42 && nil
nil

iex(5)> !42
false
iex(6)> !false
true

比較運算子: ==!====!==<=>=<>

1
2
3
4
5
6
7
8
iex(1)> 1 > 2
false
iex(2)> 1 != 2
true
iex(3)> 2 == 2
true
iex(4)> 2 <= 3
true

為了嚴謹比較整數和浮點數,請使用 ===

1
2
3
4
iex(5)> 2 == 2.0
true
iex(6)> 2 === 2.0
false

Elixir 的一個重要特點是可以比較任意兩種型別;這在排序中特別有用。我們不需要記住排序順序,但還是要注意它的重要性

1
number < atom < reference < function < port < pid < tuple < map < list < bitstring

這個特點可能會導致一些詭異但合乎語法,而您在其他語言中找不到的的比較運算

1
2
3
4
iex(1)> :hello > 999
true
iex(2)> {:hello, :world} > [1, 2, 3]
false

參考資料

  1. https://elixirschool.com/zh-hant/lessons/basics/basics/
  2. https://docs.microsoft.com/zh-tw/cpp/cpp/enumerations-cpp?view=vs-2019
  3. http://blog.zhulinpinyu.com/2017/07/04/elixir-keyword-list/