iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 14
1

Sigil 也是個鍊金術哏,中文的意思是「符文」。這個語法是為了統一各種情況所需的文字類表達式,並提供語言上擴充的空間,以符合各種不同領域的需求。

文字類表達式舉例來說,像是很常用的 regular expression,正規表示法。在 Elixir 裡,寫起來像是這樣:

regex = ~r/foo|bar/

"foo" =~ regex

我們用 ~r 來表示這是一個 regular expression,緊接著兩個 / 分隔符,中間夾著 regular expression 的內容。

而在第二行,我們用 =~ 與字串進行比對。關於 regular expression,限於篇幅無法詳述,請參考官網上 Regex 的文件

內建的 sigils

Elixir 內建了底下這幾種 sigils:

  • ~r: regular expression 正規表示法
  • ~s: 字串
~s/this is a string with " quote/
#=> "this is a string with \" quote"
  • ~w: word list,文字串列
~w/foo bar baz/
["foo", "bar", "baz"]

~w/foo bar baz/a
[:foo, :bar, :baz]
  • ~c: Char list,字元串列
~c/this is a char list with ' quote/
#=> 'this s a char list with \' quote'

而分隔符的部份,可以從底下這八種範例裡選一組,功能上都是一樣的。原則上就是你的內容裡有什麼,就避開那些符號:

~r/hello/
~r|hello|
~r"hello"
~r'hello'
~r(hello)
~r[hello]
~r{hello}
~r<hello>

sigil 的本體

雖然從名字聽起來好像很神秘,sigil 其實只是個函式,所以也可以用呼叫函式的方式來產生:

sigil_r(<<"foo">>, 'i')
#=> ~r/foo/i

自訂 sigils

剛剛說到 sigil 存在的目的之一,就是讓大家用來擴充需要的語法。而 sigil 又只是個函式,所以要定義自己的 sigil 非常簡單。例如你想做一個 ~z 的 sigil,只要定義這樣的函式就好:

def sigil_z(string, opts) do
  # do something with string and opts
end

之後只要你在程式中寫了 ~z/something/xy ,函式中的string 變數就會被綁定為 ”something",而 option 則會拿到 [120, 121],因為它們分別是 xy 的 ascii 編碼。你可以用 [?x, ?y] 來 pattern matching 這個部份,然後回傳你想要他表達的資料結構。

實際的應用可以參考這個 meyercm/shorter_maps 函式庫,他模仿 JavaScript 的 object shorthand notation,用 ~M{foo, bar} 語法產生 key 與 變數名稱相同的 map: %{foo: foo, bar: bar}


Struct

在物件導向程式中,我們會用 class 語法自定義一個類別,封裝了該類別共有的資料行為,分別稱為屬性與方法,把這些程式碼放在同一個地方。在 OO 的名詞王國 裡,行為 (動詞) 大多數的情況下不會單獨存在,而會依賴於物件 (名詞),像是 這樣: dog.eat()

在函數式編程裡,資料及行為是鬆散的藕合。資料就只是…資料而己,不同的行為,也就是函式,會依目前接收到的資料的形狀或值,來決定如何運作。而就像我們可以定義 class 一樣,Elixir 也可以讓你自定資料結構的型別,也就是 struct。

struct 跟具名函式一樣,也要定義在 module 裡,語法如下:

defmodule Beer do
  defstruct name: "18 days", brand: "Taiwan beer"

  # Beer 模組的函式跟 struct 是併列的
end

有了定義之後,你就有一個叫做 Beer 的資料型別了。基本上,它就只是個單純的 map、bare map。要宣告一個 Beer struct,就跟宣告 map 很像:

b = %Beer{}
#=>  b = %Beer{name: "18 days", brand: "Taiwan beer"}

怎麼看它就是個 map,只是在 %{ 之間多了模組的名稱而己。我們在定義時放的 key,可以視為這個資料結構會有的屬性,而後面的 value 則是該屬性的預設值。也可以宣告時把需要的值一起放進去,或是用 map 的更新語法 | 去更動它的值:

c = %Beer{brand: "Sapporo"}
#=> c = %Beer{name: "18 days", brand: "Sapporo"}
%{ c | name: "dry"}
#=> c = %Beer{name: "dry", brand: "Sapporo"}

但是如果你宣告或更新了這個 struct 沒有定義的 key,它就會噴 KeyError 錯誤給你:

%Beer{oops: :what}
** (KeyError) key :oops not found in: %Beer{name: "18 days", brand: "Taiwan beer"}

%{b | not_again: "humm"}
** (KeyError) key :not_again not found in: %Beer{name: "18 days", brand: "Taiwan beer"}

struct 的其它操作方式

剛剛說到 struct 只是個 bare map,意思是他沒有實作一般 map 裡的 AccessableEnumerable 兩個介面 (protocol,容後介紹)。所以你沒辦法用 get_in/2update_in/3 來讀取巢狀的值,沒辦法用 b.brand 取值,也沒辦法把 struct 傳進 EnumStream 模組的函式裡。

但是除此之外,所有 map 可以用的函式、pattern matching 及操作方式,都完全適用於 struct

沒有預設值的 struct 定義

defmodule User do
  defstruct [:name, :age]
end

這麼一來,每個 field 的預設值都會是 nil

針對特定 struct 的 pattern matching

def drink(b = %Beer{name: n}) do
  ## do_something
end

struct 的特別屬性

struct 會有個特別的屬性,叫做 __struct__

b.__struct__
#=> Beer

多型

是的,不需要封裝與繼承,透過型別判斷,函數式編程是可以有多型的行為的。例如 Enum 可以接收多種不同的集合型別,進行該型別對應的處理。但是這部份會留到之後進階篇的 protocol 再談。

重點回顧

  • 使用 sigil 語法 ~r/foo/ 來表達 regular expression
  • Elixir 內建了四種 sigil 語法:sigil_rsigil_ssigil_wsigil_c
  • 宣告 sigil_x 的方法,就是自定義一個 ~x// 的 sigil,這個函式接受分集符夾住的字串 string 及最後方的 char list option 兩個參數
  • struct 是 elixir 中的自訂資料結構語法
  • struct 跟 map 非常類似,只是不能用 . 取值,也不能用 get_in/2update_in/3
  • struct 是多形的關鍵

這應該是基本語法部份的倒數第二章了。下一章介紹 module 的各種用法後,我們就有足夠的工具來認識 Phoenix 了。

Happy hacking! 明天見。


上一篇
條件分支,還有不是你以為的那個 for
下一篇
mix 專案,與使用其它模組裡的函式
系列文
函數式編程: 從 Elixir & Phoenix 入門。31

尚未有邦友留言

立即登入留言