Sigil 也是個鍊金術哏,中文的意思是「符文」。這個語法是為了統一各種情況所需的文字類表達式,並提供語言上擴充的空間,以符合各種不同領域的需求。
文字類表達式舉例來說,像是很常用的 regular expression,正規表示法。在 Elixir 裡,寫起來像是這樣:
regex = ~r/foo|bar/
"foo" =~ regex
我們用 ~r
來表示這是一個 regular expression,緊接著兩個 /
分隔符,中間夾著 regular expression 的內容。
而在第二行,我們用 =~
與字串進行比對。關於 regular expression,限於篇幅無法詳述,請參考官網上 Regex 的文件 。
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_r(<<"foo">>, 'i')
#=> ~r/foo/i
剛剛說到 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]
,因為它們分別是 x
及 y
的 ascii 編碼。你可以用 [?x, ?y]
來 pattern matching 這個部份,然後回傳你想要他表達的資料結構。
實際的應用可以參考這個 meyercm/shorter_maps 函式庫,他模仿 JavaScript 的 object shorthand notation,用 ~M{foo, bar}
語法產生 key 與 變數名稱相同的 map: %{foo: foo, bar: bar}
。
在物件導向程式中,我們會用 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 只是個 bare map,意思是他沒有實作一般 map 裡的 Accessable
及 Enumerable
兩個介面 (protocol,容後介紹)。所以你沒辦法用 get_in/2
、update_in/3
來讀取巢狀的值,沒辦法用 b.brand
取值,也沒辦法把 struct 傳進 Enum
及 Stream
模組的函式裡。
但是除此之外,所有 map 可以用的函式、pattern matching 及操作方式,都完全適用於 struct。
defmodule User do
defstruct [:name, :age]
end
這麼一來,每個 field 的預設值都會是 nil
def drink(b = %Beer{name: n}) do
## do_something
end
struct 會有個特別的屬性,叫做 __struct__
:
b.__struct__
#=> Beer
是的,不需要封裝與繼承,透過型別判斷,函數式編程是可以有多型的行為的。例如 Enum
可以接收多種不同的集合型別,進行該型別對應的處理。但是這部份會留到之後進階篇的 protocol
再談。
~r/foo/
來表達 regular expressionsigil_r
、sigil_s
、sigil_w
、sigil_c
sigil_x
的方法,就是自定義一個 ~x//
的 sigil,這個函式接受分集符夾住的字串 string
及最後方的 char list option
兩個參數.
取值,也不能用 get_in/2
、update_in/3
。這應該是基本語法部份的倒數第二章了。下一章介紹 module 的各種用法後,我們就有足夠的工具來認識 Phoenix 了。
Happy hacking! 明天見。