Functional Thinking สำหรับภาษาที่ไม่ใช่แนว Functional Programming
Published: 2022-12-23 15:15:22

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

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

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

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

```elixir
def list_order() do
  date = Date.today()
  query = from(r in Order, where: r.create_date == ^date)
  DB.all(query)
end
```

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

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

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

```elixir
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 อื่นได้ด้วย เช่น

```elixir
list_order(
  fn -> Date.new(..., ...) end,
  fn query -> [...] end
)
```

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

```elixir
list_order(
  Date.today,
  DB.all
)
```

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

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

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

```elixir
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 ย่อย ๆ 

```elixir
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 ใหม่ 

```elixir
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 อาจจะไม่เห็นผลอะไรครับ

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