Do you want to pick up from where you left of?
Take me there

Control Structures

ในบทนี้เราจะมาดูเรื่อง control structure ที่ใช้ได้ใน Elixir กัน

if และ unless

คุณน่าจะเคยใช้งาน if/2 กันมาก่อน และถ้าหากว่าคุณมาจากโลกของ Ruby จะต้องคุ้นเคยกับ unless/2 อย่างแน่นอน ใน Elixir มันก็จะทำงานเหมือนกันนั่นแหละ แต่ว่ามันถูกสร้างขึ้นในรูปของ macros (ไม่ใช่จากโครงสร้างภาษา) ซึ่งคุณก็สามารถหาอ่านเพิ่มเติมว่ามันสร้างขึ้นมายังไง ได้ที่ โมดูล Kernel

ข้อควรรู้อย่างหนึ่งคือ ใน Elixir ค่าที่เป็นเท็จ มีเพียงแค่ nil และ boolean false เท่านั้น

iex> if String.valid?("Hello") do
...>   "Valid string!"
...> else
...>   "Invalid string."
...> end
"Valid string!"

iex> if "a string value" do
...>   "Truthy"
...> end
"Truthy"

การใช้ unless/2 ก็จะคล้าย ๆ กับ if/2 ต่างกันตรงที่มันจะทำงานเมื่อเงื่อนไขเป็นเท็จ

iex> unless is_integer("hello") do
...>   "Not an Int"
...> end
"Not an Int"

case

ถ้าหากว่าบางสถานการณ์คุณต้องการ match ค่า กับ pattern ต่างๆ เราก็สามารถใช้ case/2 ได้

iex> case {:ok, "Hello World"} do
...>   {:ok, result} -> result
...>   {:error} -> "Uh oh!"
...>   _ -> "Catch all"
...> end
"Hello World"

ตัวแปร _ เป็นตัวแปรสำคัญมาก ๆ ใน case/2 เพราะถ้าค่ามันไม่ match กับอะไรเลย มันจะทำให้เกิด error

iex> case :even do
...>   :odd -> "Odd"
...> end
** (CaseClauseError) no case clause matching: :even

iex> case :even do
...>   :odd -> "Odd"
...>   _ -> "Not Odd"
...> end
"Not Odd"

ลองมอง _ ให้เป็น else เพื่อให้มัน match กับทุกสิ่งอย่างนอกเหนือจากที่กำหนดไว้

เนื่องจาก case/2 จะทำงานอยู่บน pattern matching ดังนั้นทุก ๆ กฎเกณฑ์และข้อจำกัดของ pattern matching จะถูกนำมาใช้ ถ้าหากว่าต้องการ match เทียบกับตัวแปรที่มีค่าอยู่แล้ว คุณก็สามารถใช้ pin ^/1 operator ได้

iex> pie = 3.14
 3.14
iex> case "cherry pie" do
...>   ^pie -> "Not so tasty"
...>   pie -> "I bet #{pie} is tasty"
...> end
"I bet cherry pie is tasty"

feature เท่ๆ อีกอย่างของ case/2 ก็คือมันรองรับ guard clauses

ตัวอย่างนี้มาจาก official Elixir [Getting Started] (http://elixir-lang.org/getting-started/case-cond-and-if.html#case)

iex> case {1, 2, 3} do
...>   {1, x, 3} when x > 0 ->
...>     "Will match"
...>   _ ->
...>     "Won't match"
...> end
"Will match"

ดู official doc เพิ่มเติมเกี่ยวกับ Expression ที่ใช้ได้กับ guard clauses.

cond

เมื่อเราต้องการจะ match เงื่อนไขแทนที่จะเทียบกับค่า เราสามารถเปลี่ยนมาใช้ cond/1 แทนได้ เทียบได้กับ else if หรือ elsif ในภาษาอื่น ๆ

ตัวอย่างนี้มาจาก official Elixir [Getting Started] (http://elixir-lang.org/getting-started/case-cond-and-if.html#cond)

iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
"But this will"

เหมือนกันกับ case/2 เจ้า cond/1 ก็จะ error เมื่อมันไม่ match กับอะไรเลย ดังนั้นเพื่อไม่ให้เกิด error เราสามารถตั้ง condition ว่า true ได้

iex> cond do
...>   7 + 1 == 0 -> "Incorrect"
...>   true -> "Catch all"
...> end
"Catch all"

with

รูปแบบพิเศษ with/1 จะมีประโยชน์มากเลย หากเห็นว่าอาจจะต้องใข้ case/2 ซ้อนกัน หรือในสถานการณ์ที่คุณไม่สามารถ pipe ต่อกันได้อย่างสวยงาม เจ้า with/1 จะประกอบด้วย keyword, generator และ expression

เราจะมาจับเข่าคุยถึง generator ใน บท list comprehensions แต่สำหรับตอนนี้เราควรจะรู้แค่ว่ามันใช้ pattern matching เพื่อเทียบฝั่งขวาของ <- กับของซ้ายก็พอ

เราจะเริ่มจากตัวอย่างง่าย ๆ ของ with/1 แล้วค่อยลงลึกไปเรื่อย ๆ กัน

iex> user = %{first: "Sean", last: "Callan"}
%{first: "Sean", last: "Callan"}
iex> with {:ok, first} <- Map.fetch(user, :first),
...>      {:ok, last} <- Map.fetch(user, :last),
...>      do: last <> ", " <> first
"Callan, Sean"

ในสถานการณ์ที่ expression ไม่ match มันจะคืนค่าที่ไม่ match ออกมา

iex> user = %{first: "doomspork"}
%{first: "doomspork"}
iex> with {:ok, first} <- Map.fetch(user, :first),
...>      {:ok, last} <- Map.fetch(user, :last),
...>      do: last <> ", " <> first
:error

คราวนี้มาดูตัวอย่างแบบที่ไม่ใช้ with/1 แล้วมาดูซิว่าเราจะ refactor มันยังไงได้บ้าง

case Repo.insert(changeset) do
  {:ok, user} ->
    case Guardian.encode_and_sign(resource, :token, claims) do
      {:ok, token, full_claims} ->
        important_stuff(token, full_claims)

      error ->
        error
    end

  error ->
    error
end

เมื่อเอา with/1 มาใช้ ก็มักจะจบลงด้วย code ที่เข้าใจง่ายๆ และใช้เพียงไม่กี่บรรทัด

with {:ok, user} <- Repo.insert(changeset),
     {:ok, token, full_claims} <- Guardian.encode_and_sign(user, :token) do
  important_stuff(token, full_claims)
end

ใน Elixir 1.3 เจ้า with/1 ก็รองรับ else ด้วย

import Integer

m = %{a: 1, c: 3}

a =
  with {:ok, number} <- Map.fetch(m, :a),
       true <- is_even(number) do
    IO.puts("#{number} divided by 2 is #{div(number, 2)}")
    :even
  else
    :error ->
      IO.puts("We don't have this item in map")
      :error

    _ ->
      IO.puts("It is odd")
      :odd
  end

มันช่วยให้เรา handle error โดยมี สิ่งที่ทำงานคล้ายกับ pattern matching แบบใน case ให้ใช้งาน ค่าที่ส่งเข้ามาใน else คือ expression แรกที่ไม่ match

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!