ที่บริษัทมี 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
โค้ดตัวอย่างเป็นโค้ดที่หา order ทั้งหมดที่อยู่ในวัน ถ้าสังเกตเราจะเห็นของเป็น side-effect อยู่คือ
-
Date.today()
เปลี่ยนตามวันเวลา นั้นหมายความว่าเรียกเหมือนเดิมมันได้ผลลัพธ์ไม่เหมือนเดิม -
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
เห็นไหมครับ side-effect หายออกไปจาก function แล้วเพราะเราควบคุมปัจจัยพวกนี้ได้แล้ว เรา test function นี้ได้ด้วยการเปลี่ยนเป็น function อื่นได้ด้วย เช่น
list_order(
fn -> Date.new(..., ...) end,
fn query -> [...] end
)
หรือถ้าอยาก integrate test ก็ทำได้
list_order(
Date.today,
DB.all
)
คนที่เขียนภาษาแนว 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
หลังจากนั้นสิ่งต่อมาที่ผมทำคือแยกพวกนี้ให้เป็น 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
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
ปล. สำหรับภาษา 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)
อันนี้ list_order ยังไม่ใช่ pure function นะครับ ยังมี side effect อยู่ดีถ้า
date_fun, query_all_fun
มี side-effect ซึ่งผมตีความเองว่ามันมีถูกครับ