How Clojure teaches me about side-effects

ดัดแปลงมาจากคอมเมนต์นี้ https://www.facebook.com/groups/919377878100706/permalink/1121787907859701/?comment_id=1121803797858112&reply_comment_id=1121826181189207

หลังจากที่ผมโพสว่า Clojure สอนผม ว่าผมจะอยู่ร่วมกับ side-effects ได้อย่างไร

พี่มาคอมเมนต์ Patraphong Sukhonpitumart ถามว่า

clojure สอนยังไง มีตัวอย่างมั้ยครับ

ผมเลยตอบว่า

ที่ผมพอจะนึกออกตอนนี้ ไม่ได้ exclusive สำหรับ clojure นะครับ

  • สอนให้ model domain เป็น value ไม่ใช่ object มันเหมาะกับแนวคิด bounderies (Functional Core, Imperative Shell) มาก
  • แยกที่เก็บ mutable data (atom) ให้ชัดเจน เราจะเรียกใช้แก้ไขมันเมื่อไหร่ก็ได้ แต่ก็จะบอกว่าพยายามให้มีให้น้อยที่สุด
  • ไม่มีคนทำ ORM แล้วประสบความสำเร็จ
  • Referential transparency จะใช้ไม่ได้ถ้ามี side-effects ฟังก์ชันอย่าง memoize ก็จะทำงานผิด ถ้าไม่สนใจเรื่องนี้
  • ความที่เราสามารถทำ side-effect ในฟังก์ชันได้ตามใจ แต่เค้าก็จะบอกว่าไม่ควรทำ เช่น ฟังก์ชันที่ใช้ update ค่าให้ atom (เพราะมันไม่การันตีว่าจะถูกเรียกแค่ครั้งเดียว)
  • ไม่สร้างความยุ่งยากให้เราหากเราจะทำ side-effects ง่ายๆ เช่น เราอยากจะ println เมื่อไหร่ก็ได้ ไม่โดน type บังคับ
  • พวก React wrapper ก็จะบอกให้เราระวังเรื่อง state โดยที่บางตัวก็ strict บางตัวก็ปล่อยหลวมๆ
  • def เป็น mutable เพราะมันไม่จำเป็นต้อง immutable
  • do, doseq ทำให้เรารู้ว่าเราทำ side-effects อยู่นะคือ ผมมีโอกาสเลือกอยู่เสมอว่าผมจะทำ side-effects ได้ แต่ผมก็จะเห็นชัดเจนว่าถ้าผมทำแล้วปัญหาที่ตามมาคืออะไร

Blending functional programming with imperative. Higher-level of abstraction or functional core.

ดัดแปลงและเพิ่มเติมจากคอมเมนต์คำถามในโพสนี้ https://www.facebook.com/nuttanart/posts/1015437440718608 เป็นคำถามจาก Supanat IBoss Potiwarakorn

ผมได้ยินคนพูดถึง functional core, imperative shell มาซักพักและ แต่ผมก็มักจะได้ยินว่า functional เนี่ยเป็น higher level abstraction เราแอบเอาลำดับขั้นตอนการทำงานต่างๆ ไปแอบไว้ภายใต้ declarative code ผมก็เลยยังรู้สึกสับสนอยู่เล็กน้อย (รู้สึกว่าเห้ย ถ้าเป็น shell มันต้องอยู่บนกว่าสิ)

เท่าที่ผมเข้าใจคือ monad เนี่ยมันคือเหมือน imperative shell ซึ่งจริงๆ แล้วมันก็คือ เรา แอบ state mutation เอาไว้ ไอสองเรื่องที่ผมพูดอ้างถึงบนนี่มันไม่ได้ขัดแย้งกัน แต่มันแค่อธิบายในคนละมุมกันถูกมั้ยฮะ

คำตอบของผม

จริงๆ เราคิดอะไรคล้ายๆ กันนะเนี้ย ผมก็เคยสงสัยเรื่องเดียวกันนี้เลย จำไม่ได้แล้วว่าตอนนั้นตอบตัวเองว่ายังไง แต่ถ้าพยายามตอบคำถามนี้ด้วยความเข้าใจในปัจจุบันจะตอบแบบนี้

ตอนนี้มีสองเรื่อง
a) functional เป็น declarative (higher-level abstraction) อยู่ด้านบน ส่วน imperative (lower-level abstraction) อยู่ด้านล่าง อย่างเช่นเราเขียน map,reduce โดยไม่ต้องวนลูปเอง
b) imperative อยู่ด้านบน functional อยู่ด้านล่าง ตามแนวคิด Functional Core, Imperative Shell เช่น เอา UI, http request, database ไว้ด้านบน หรืออย่าง Actor  model

เวลาเราเขียนโปรแกรมจริงๆ เราจะไม่ได้คำนึงถึงข้อ a) อะเพราะไม่ใช่หน้าที่เราจะต้องไปสนใจการทำงานตรงนั้น platform มีหน้าที่การันตีให้เรา ด้านล่างของเรามันไม่ functional อยู่แล้ว แต่ platform ทำให้เรารู้สึกว่าเราควบคุมทุกอย่างได้ในลักษณะ functional (input – output) แต่สำหรับ b) เราต้องสนใจเพราะเป็นสิ่งที่เราต้องเป็นคนสั่งให้มันทำ เพราะฉะนั้นแล้วในชั้นของเราที่ใช้ภาษาเขียนเพื่อแก้ปัญหาอื่นๆ เราคงโฟกัสที่ b) มากกว่า ส่วน a) สำหรับเราน่าจะสำคัญแค่ตอนที่เราอธิบายให้คนอื่นฟังว่า functional มันทำงานยังไงแค่นั้นมั้ง

จริงๆ แล้วเราอาจจะต้องไปเขียน imperative ด้านล่างบ้างก็ได้ แต่เราก็ควรห่อให้ imperative นั้นเรียบร้อยอยู่ภายในแล้ว expose ออกมาเป็นแค่ functional ให้ด้านบนใช้ ผมเชื่อว่าการที่ด้านล่างของเราเรียบง่ายและตรงไปตรงมาจะทำให้เราสามารถเข้าใจระบบโดยรวมของเราได้ง่ายกว่า จะเพิ่มฟีเจอร์ก็มั่นใจ จะ debug ก็ไล่ได้ง่าย

ผมชอบประโยคนี้ของ José Valim (ผู้สร้าง Elixir)

I often summarize functional programming as a paradigm that forces us to make the complex parts of our system explicit

ผมคิดว่าการที่เราเอา imperative ซึ่งเป็นส่วนที่ยากขึ้นมาไว้ด้านบน (ไม่ซ่อนอยู่ด้านล่าง) เป็นตัวอย่างที่ดีมากของการทำให้ complex parts explicit

ส่วนเรื่อง monad นี่ไม่รู้เลยอะ(เขียน clojure ไม่ต้องรู้จัก monad 😜) แต่ก็เคยได้ยินมาบ้างเหมือนกันที่ว่ามองมันเป็น imperative shell ได้

เรื่องสำคัญที่หนังสือ OOP ทั่วไปไม่ได้สอน

ดัดแปลงจาก Facebook โพสนี้ https://www.facebook.com/nuttanart/posts/10154374407186081

ซึ่งเป็นโพสต่อเนื่องจากโพสแนะนำหนังสือ 99 Bottles of OOP อันนี้ https://www.facebook.com/nuttanart/posts/10154374324641081

ถึงแม้ผมจะเชียร์หนังสือ 99 Bottles of OOP อย่างไร ผมยังรู้สึกว่ายังมีปัญหาสำคัญมากๆ อันหนึ่งที่ทำให้โค้ดเราเน่าและหนังสือเล่มนี้คงจะไม่ได้พูดถึง หนังสือเล่มอื่นๆที่ผมเคยได้อ่านมาก็ไม่ได้สอน คือ การอยู่ร่วมกันระหว่าง OO กับสิ่งเหล่านี้
– การเชื่อมต่อ database
– การทำ external call
– logging
– อ่าน/เขียนไฟล์
– UI
– ฯลฯ

อาจจะมีแนวคิด Clean Architecture ที่พูดถึงอยู่บ้าง เพียงแต่ว่างบางครั้งเราไปโฟกัสที่ตัว pattern และแต่ละ component มากเกินไปจนลืมว่าจริงๆ แล้วต้นเหตุของปัญหาของเราคืออะไร

ผมคิดว่าตราบใดที่สิ่งเหล่านี้ยังปะปนกันอยู่ในโค้ดของเราอยู่ เราจะรู้สึกว่าสิ่งที่หนังสือสอนเขียนโค้ดดีๆ เค้าสอนเนี้ย มันทำไม่เห็นจะทำได้จริงเลย

บางคนอาจจะคิดในใจว่า นี่มัน side-effects นี่หว่า ใช่แล้วครับ ผมคิดว่าอีกปัจจัยหนึ่งที่จะทำให้เราเขียนโค้ด OO ได้ดี ดันเป็นสิ่งที่ OO ไม่ได้สอน แต่มันอยู่ในแนวคิด functional programming ถ้ารู้ว่าจะจัดสิ่งเหล่านี้ให้อยู่ถูกที่ถูกทางได้อย่างไร มันจะทำให้เราจะมีพื้นที่ให้จัดโค้ด OO ให้ดีได้

ลองไปศึกษาเรื่อง side-effects ดูนะครับ แล้วจะพบว่าหลายๆ ปัญหาที่เราเจอกันอยู่ สาเหตุเกิดจากพวกมันนี่แหละ

Update: talk นี้อธิบายเพิ่มเติมได้ดีว่า side-effects คืออะไร และมันสำคัญอย่างไร

เลือกพฤติกรรมของฟังก์ชันด้วยพารามิเตอร์ฟังก์ชัน

Clojure เป็น functional language ซึ่งหมายความว่าเราสามารถส่งฟังก์ชันเป็นพารามิเตอร์ได้ ตัวภาษาได้มีการออกแบบไลบราลีมาตรฐานโดยใช้ประโยชน์จากคุณสมบัตินี้ ทำให้เราสามารถส่งฟังก์ชันเข้าไปในฟังก์ชันเพื่อเลือกพฤติกรรมที่ต้องการได้ ตัวอย่างฟังก์ชันที่ถูกออกแบบมาในลักษณะนี้ได้แก่

ผมขอยก merge-with ขึ้นมาเป็นตัวอย่างเพื่ออธิบาย
 
Clojure มีฟังก์ชัน merge ที่ใช้รวม map มากกว่า 2 อันเข้าด้วยกัน โดยหากมี key ที่ซ้ำกันระหว่าง map จะทำการใช้ value จาก map อันสุดท้าย

แต่การเลือกเอาค่าสุดท้ายไม่ได้เป็นสิ่งที่เราต้องการเสมอไป Clojure จึงมีฟังก์ชัน merge-with ให้เราเลือกใช้หากเราต้องการพฤติกรรมเมื่อมี key ซ้ำกันที่แตกต่างออกไปจาก merge ปกติ เช่น
เราสามารถเลือกรวม vector เช่นนี้ได้โดยใช้ concat ช่วย

(merge-with concat {:a [1 2 3]} {:a [4 5 6]})
;=> {:a (1 2 3 4 5 6)}
 หรือจะเลือกเอาค่าที่มากที่สุดโดยการส่ง max เข้าไป
 
(merge-with max {:a 1} {:a 3} {:a 2}) 
;=> {:a 3} 
นอกจากจะมีคู่ merge & merge-with แล้ว ฟังก์ชันอื่นๆ ด้านบนก็มีคู่ในลักษณะเดียวกัน partition & partition-by, split & split-with, assoc & update, assoc-in & update-in, sort & sort-by

จะเห็นได้ว่าการเขียนโปรแกรมในลักษณะนี้ ทำให้เกิดโค้ดที่มีพฤติกรรมใหม่ๆ ได้มากมายไม่จำกัด โดยเราแทบจะไม่ต้องเขียนอะไรเพิ่มขึ้นเลย ผมว่านี่แหละเป็น code reuse ที่เราโปรแกรมเมอร์พยายามทำให้เกิดในโปรแกรมที่เราเขียน และนี่เป็นหนึ่งในสาเหตุที่ทำให้โปรแกรมที่เขียนในลักษณะ functional สามารถสั้นกว่าโปรแกรมที่เขียนในลักษณะอื่นได้ ด้วยการทำให้เกิดการรวมกันของฟังก์ชัน (composability) เพื่อเกิดเป็นโปรแกรมที่มีพฤติกรรมใหม่ๆ ตามต้องการได้ง่าย

หวังว่าตัวอย่างเหล่านี้จะช่วยเพิ่มความเข้าใจในประโยชน์ของ functional programming มากขึ้นนะครับ

Guideline ในการเขียนเทสของผม

ทุกๆครั้งเวลาที่จะเขียนเทสสิ่งที่ผมคิดอยู่เสมอคือ การเขียนเทสในลักษณะใดที่จะทำให้เทสสร้างความมั่นใจให้แก่เราได้สูงสุด ในขณะที่เทสนั้นมีต้นทุนต่ำที่สุด

แนวทางในการเขียนเทสให้ได้แบบนั้น มีดังนี้

  • เทสต้องรันได้เร็ว ถ้ามันช้าเราจะไม่อยากรัน ไม่อยากเขียนเพิ่ม
  • เทสต้องดูแลได้ง่าย ซึ่งหมายถึง
    • เขียนง่าย การ setup ต้องไม่ซับซ้อน
    • อ่านง่าย ชัดเจนว่าต้องการทดสอบอะไร
    • faill เมื่อควรจะ fail จริงๆ
    • ไม่ fail แบบ random
  • เขียนให้น้อยที่สุดที่ตราบที่เรายังมั่นใจเวลาเราจะแก้โค้ด ตราบที่เรายังมั่นใจเวลาเราจะ deploy
    • ยิ่งปริมาณเทสเยอะยิ่งมีแต่วันที่มันจะรันช้าลง
    • ถ้าเราไม่เทสตรงนี้ ความเสียหายคืออะไร เรายอมรับได้มั้ย ถ้าเราพบภายหลังสามารถแก้ไขได้ยากหรือง่ายเพียงใด
    • บางจุดที่ถ้ามันมี error หรือ bug เราจะรู้หรือเห็นได้ทันที ผมก็ไม่เขียนเทส

Sandi Metz at Keep Ruby Weird 2015 – Be Yourself

“In order to make a best group we can be, then each of you have to act as you are the only one here.”

งานนี้ Sandi Metz เป็น keynote speaker เลยไม่ได้พูดเรื่องโค้ด แต่พูดในลักษณะให้แนวคิดแก่ community และเซต theme งาน

เธอเล่าถึงการทดลองเกี่ยวกับพฤติกรรมมนุษย์ 3 ชิ้น
Asch conformity experiments, Milgram experiment, Latané and Darley(Bystander effect)

แนะนำให้ใส่หูฟังๆ เพราะคุณภาพการอัดเสียงของ video นี้ไม่ค่อยดีเท่าไหร่

ต่อไปนี้จะเป็นการ spoil สิ่งที่ Sandi สรุปจากการทดลอง ดู/ฟังการทดลองก่อนจะสนุกกว่านะครับ

จาก Asch:

  • ถ้าอยากให้กลุ่มคนเห็นพ้องต้องกันเรื่องอะไร ให้ทุกคนพูดความเห็นของตัวเองออกมาตรงๆ โดยให้กลุ่มคนที่ตำแหน่งใหญ่กว่าพูดก่อน
  • ถ้าอยากได้ความหลากหลายทางความคิด ให้คนเขียนความเห็นลงกระดาษก่อน ก่อนที่จะพูดออกมา และให้ทุกคนได้พูดในเวลาที่เท่าเทียมกัน

จาก Milgram:

  • ถ้าต้องการให้คนทำ(ความผิด)ตามคำสั่ง
    • ให้มีคนคอยสั่งเค้าอยู่ใกล้ๆ
    • จับคนนั้นให้ห่างกับผู้ที่ได้รับผลกระทบ (เหมือน Developer กับ User)
    • อย่าให้ได้คุยกับคนที่โดนสั่งแล้วไม่เห็นด้วยคนอื่นแล้ว
  • ถ้าอยากให้คนมีความคิดเป็นของตัวเอง
    • อยู่ให้ห่างจากคนสั่งการ
    • อยู่ให้ใกล้กับผู้รับผลกระทบ
    • ให้กลุ่มคนที่มีความคิดเป็นของตัวเองได้แลกเปลี่ยนพูดคุยกัน

จาก Latané and Darley

  • ถ้าต้องการให้ไม่มีใครตอบสนองคำขอร้อง ให้ขอโดยตรงจากกลุ่ม
  • ถ้าต้องการให้มีคนตอบสนอง ให้ขอไปที่คนโดยตรง

สิ่งที่ได้จากเรื่องเหล่านี้คือ เราสามารถเป็นคนหนึ่งที่เปลี่ยนความคิดของกลุ่มได้ เพียงแค่เราทำออกมา ถ้ามีใครขอความช่วยเหลือแล้วเราเสนอความช่วยเหลือ คนอื่นก็จะเข้ามาร่วมช่วยเหลือ

“Not conforming with the Group. Be yourself.”

Podcast ที่ติดตามอยู่ Oct 2015

พอดีมีน้องมาถามว่ามี video หรือ podcast อะไรเกี่ยวกับ software development ที่น่าสนใจ สำหรับการพัฒนาตัวเองบ้าง ผมก็ไม่รู้ว่าจะแนะนำอะไรดี เพราะเรื่องน่าสนใจมันก็กระจายไปทั่ว และสิ่งที่ผมสนใจอยู่ คนอื่นก็อาจจะไม่ได้สนใจก็ได้ ก็เลยลิส podcast ที่ผมติดตามอยู่ทั้งหมดขึ้นมาให้น้องเค้าไปเลือกเองละกัน

ฟังทุกตอน
Changelog: เกี่ยวกับแวดวง open source ไว้เปิดหูเปิดตาเกี่ยวกับ tool ใหม่ๆ แบบ in depth weekly email เค้าก็ดี
Ruby Rogues: เกี่ยวกับ Ruby ​community เป็นหลัก แต่ก็พูดเรื่อง software dev ทั่ว ๆ ไปด้วยเยอะเหมือนกัน เมื่อก่อนดีมากแต่ drop ลงเพราะ panel เก่งๆ ออกไป
Giant Robots Smashing into other Giant Robots Podcast: เนื้อหาเปลี่ยนไปเรื่อยตามความสนใจของผู้จัดรายการแต่ก่อนเป็น tech จ๋าๆ หลังๆ เป็นเรื่อง product dev มากขึ้น
The Bike Shed: ถกระดับ code คนจัดเป็น Rails contributor เอาสิ่งที่เค้าเจอจากการทำงานแต่ละอาทิตย์มาเล่าให้ฟัง ฟังรู้เรื่องบ้างไม่รู้เรื่องบ้าง หลังๆ มีเรื่อง Rust เยอะขึ้น
Functional Geekery: ครอบคลุมเนื้อหา Functional programming ทั้งหมด
The Cognicast: บริษัทหัวหอกของ Clojure พูดเรื่อง Clojure เป็นหลัก ทั้งแนวคิดและ tool

เลือกฟังบางตอน
CodeNewBie: เป็นแนวการพัฒนาตัวเองในด้าน software dev ในแง่มุมต่าง ๆ
Software Engineering Radio: ตามชื่อ
JavaScript Jabber: ตามชื่อ

เคยฟังบางตอน
Talking Code: software dev & product dev
Turing-Incomplete: software dev หลายๆ เรื่อง
Developer On Fire: เคยฟังแต่ตอน DHH