DEV Community

Thanabodee Charoenpiriyakij
Thanabodee Charoenpiriyakij

Posted on • Edited on

Functional Thinking สำหรับภาษาที่ไม่ใช่แนว Functional Programming

ที่บริษัทมี share knowledge เกี่ยวการเอาแนวคิด Functional Programming ไปใช้กับภาษาอื่นที่ไม่ใช่ Functional Programming แล้วในช่วง Q&A มีคนมาถามผม แต่ผมก็ตอบได้ไม่ค่อยชัดเจน เลยคิดว่ายกตัวอย่างมาใส่ใน blog ไว้น่าจะดี

ปล. ผมพยายามจะอธิบายให้คนที่เขียนภาษาอื่นเข้าใจได้ด้วย ถ้าไม่เข้าใจสามารถถามหรือทักท้วงได้ครับ

แยกของที่เป็น side-effect ออกจาก function

side-effect บางคนอาจจะงงว่ามันคืออะไร ลองพิจารณาโค้ดตัวอย่าง

def list_order() do
  date = Date.today()
  query = from(r in Order, where: r.create_date == ^date)
  DB.all(query)
end
Enter fullscreen mode Exit fullscreen mode

โค้ดตัวอย่างเป็นโค้ดที่หา order ทั้งหมดที่อยู่ในวัน ถ้าสังเกตเราจะเห็นของเป็น side-effect อยู่คือ

  1. Date.today() เปลี่ยนตามวันเวลา นั้นหมายความว่าเรียกเหมือนเดิมมันได้ผลลัพธ์ไม่เหมือนเดิม
  2. DB.all(...) เราต่อ database ถ้ามันหายตัว function อาจจะ raise exception หรือ [] ก็ได้ นั้นหมายความว่าเรียกเหมือนเดิมอาจจะไม่เหมือนเดิม

ที่นี้คงพอเห็นภาพใช่ไหมครับ ว่าจริง ๆ แล้วมันคือ function ที่เรียกไปแล้วอาจจะโดนปัจจัยภายนอกทำให้มันได้ผลลัพธ์ไม่เหมือนเดิม แล้วเราจะแก้ปัญหานี้ยังไง?​ คำตอบนึงที่ผมค้นพบคือแยก function พวกนี้ให้กลายเป็น argument ของ function นั้น ๆ เช่น

def list_order(date_fun, query_all_fun) do
  date = date_fun.()
  query = from(r in Order, where: r.create_date == ^date)
  query_all.()
end
Enter fullscreen mode Exit fullscreen mode

เห็นไหมครับ side-effect หายออกไปจาก function แล้วเพราะเราควบคุมปัจจัยพวกนี้ได้แล้ว เรา test function นี้ได้ด้วยการเปลี่ยนเป็น function อื่นได้ด้วย เช่น

list_order(
  fn -> Date.new(..., ...) end,
  fn query -> [...] end
)
Enter fullscreen mode Exit fullscreen mode

หรือถ้าอยาก integrate test ก็ทำได้

list_order(
  Date.today,
  DB.all
)
Enter fullscreen mode Exit fullscreen mode

คนที่เขียนภาษาแนว Imperative มักจะรู้จักกันดีในชื่อ dependency injection :)

เขียน function ให้เป็น function ย่อย ๆ แล้วนำมาประกอบกัน

ในตอนเริ่มแรกผมมักจะเขียน logic ของ function ให้อยู่ในที่เดียวก่อนยกตัวอย่าง เช่น

def create_order(order_data) do
  order_data.vat = order_data.total_amount * 0.07
  order_data.net = order_data.total_amount + order_data.vat
  ...
end
Enter fullscreen mode Exit fullscreen mode

หลังจากนั้นสิ่งต่อมาที่ผมทำคือแยกพวกนี้ให้เป็น function ย่อย ๆ

def create_order(order_data) do
  order_data.vat = calculate_vat(order_data.total_amount, 0.07)
  order_data.net = calculate_net(order_data.total_amount, order_data.vat)
  ...
end

def calculate_vat(total_amount, rate) do
  total_amount * rate
end

def calculate_net(total_amount, vat) do
  total_amount + vat
end
Enter fullscreen mode Exit fullscreen mode

function จะเริ่มชัดเจนว่า step การทำงานมันเป็นยังไงชัดเจน

มันเริ่มกลายเป็น declarative มากกว่า imperative แล้วเห็นไหมครับ :)

Immutable data

จากตัวอย่างก่อนหน้าเราจะเห็นว่าไปเปลี่ยนค่าใน order_data ในบางภาษา object พวกนี้เป็นแบบ pass by reference มาความว่าเมื่อไหร่ก็ตามที่ function นี้ไป order_data ที่ส่งมาจากคนเรียกจะถูกเปลี่ยนค่าตามไปด้วย นั้นหมายความว่ามันคือ function ที่เป็น side-effect นั้นเอง สิ่งที่เราควรทำคือทำให้แน่ใจว่าการแก้ไขพวกจะต้องไม่ทำให้ไปเปลี่ยน data ภายนอก หนึ่งในวิธีการคือทำการ clone object พวกให้กลายเป็น object ใหม่

def create_order(order_data) do
  order_data_1 = calculate_vat_to_order(clone(order_data), 0.07)
  order_data_2 = calculate_net_to_order(clone(order_data_1))
  ...
end

def calculate_vat_to_order(order_data, rate) do
  # calculate vat and assign here.
  # return new order_data
end

def calculate_net_to_order(order_data, rate) do
  # calculate net and assign here.
  # return new order_data
end
Enter fullscreen mode Exit fullscreen mode

ปล. สำหรับภาษา Functional อาจจะไม่ต้องกังวลเพราะภาษาพวกเป็น immutable by default

ทำ immutable data แล้วดียังไง?

Bug concurrency หลาย ๆ ครั้ง race condition เพราะว่ามี thread พยายาม mutate data เลยเป็นที่มาว่า Functional Programming เลยแก้เกมด้วยการ copy data ให้หมดเพืื่อลดอาการนี้

เคส let's encrypt ที่เคยต้องมีการ rotate cert ทุกใบก็เพราะว่าไป mutate data ที่ไม่ควรจะ mutate สุดท้ายก็ต้อง copy data ก่อน

มันไม่เปลือง memory เหรอ?

คำตอบคือเปลืองขึ้นครับ ฮาาา การทำ immutable data ดีอย่างนึงคือมีโอกาสที่ Garbage Collector จะ clear ได้เร็วขึ้นเนื่องจากมันเห็นว่าไม่มีใครใช้แล้ว หรือบางภาษาแนว ๆ Rust ก็อาจจะ clear ไปหลังจบการทำงานของ function เลย ทั้งนี้ทั้งนั้นขึ้นกับ size ของ data ที่เรา copy ไปครับ map สี่ห้า field อาจจะไม่เห็นผลอะไรครับ

ผมคิดว่าน่าจะพอเห็นภาพคร่าว ๆ บ้างแล้ว หวังว่าจะเอามันไปประยุกต์ต่อได้ครับ :)

Top comments (2)

Collapse
 
groupw66 profile image
Groupw
def list_order(date_fun, query_all_fun) do
  date = date_fun.()
  query = from(r in Order, where: r.create_date == ^date)
  query_all.()
end
Enter fullscreen mode Exit fullscreen mode

อันนี้ list_order ยังไม่ใช่ pure function นะครับ ยังมี side effect อยู่ดีถ้า date_fun, query_all_fun มี side-effect ซึ่งผมตีความเองว่ามันมี

Collapse
 
wingyplus profile image
Thanabodee Charoenpiriyakij

ถูกครับ