cond กับ expensive get function

สมมติว่าฟังก์ชั่น fn1 เป็นฟังก์ชันที่คืนเป็นค่าใดๆจาก collection หรือคืนค่าเป็น nil แต่ฟังก์ชัน fn1 นี้ต้องใช้การประมวลผลเยอะ ผมไม่อยากรันมันหลายครั้งถ้าไม่จำเป็น พอมันต้องไปอยู่เป็นหนึ่งในเงื่อนไขของ cond ผมจึงไม่อยากทำแบบนี้เพราะผมต้องรันมัน 2 ครั้ง

(cond
  criteria1 return1
  criteria2 return2
  criteria1 return3
  (fn1) (fn1)
  :else some-other-thing)

ผมไม่อยาก let มันเอาไว้ก่อนเพราะเงื่อนไขนี้อาจจะไม่ต้องถูกคำนวณถ้าเงื่อนไขก่อนหน้าสำเร็จไปก่อนแล้ว เช่น ในกรณีนี้ criteria1, criteria2 หรือ criteria3 อาจจะเป็นจริงทำให้ fn1 ไม่จำเป็นต้องถูกรัน

(let [x (fn1)]
  (cond
    criteria1 return1
    criteria2 return2
    criteria3 return3
    x x
    :else some-other-thing))

ผมอยากได้อะไรคล้ายๆ กับ if-let หรือ when-let ที่ผมสามารถนำผลลัพธ์ที่ใช้ตรวจสอบเงื่อนไขไปใช้เป็นค่า return ได้ แต่ cond ไม่มีสิ่งนี้ให้ผมใช้

(cond
  criteria1 return1
  criteria2 return2
  criteria3 return3
  (let [x (fn1)] x) x
  :else some-other-thing)

ผมเจอปัญหานี้บ่อย คิดวิธีแก้ไม่ออกซักที แต่วันนี้ผมคิดออกแล้ว! เอา delay มาช่วยไง

(let [x (delay (fn1))]
  (cond
    criteria1 return1
    criteria2 return2
    criteria3 return3
    @x @x
    :else some-other-thing))

delay จะทำการห่อ form ที่อยู่ข้างใน ไม่ทำการ evaluate มันจนกว่าตัว delay จะถูก dereference ด้วย @ หรือ deref จากนั้นมันจะทำการเก็บผลลัพธ์ไว้ คืนค่าเดิมทุกครั้งเมื่อถูก dereference อีกในครั้งต่อๆ ไป

Advertisements

Immutable data ลดปัญหาในการเขียนโปรแกรมของเราได้อย่างไร

ทุกอย่างในโลกการเขียนโปรแกรมมีทั้งข้อดีและข้อเสีย ในโพสนี้ผมจะขอโฟกัสเฉพาะข้อดีของ immutable data นะครับ

มาเริ่มจากการดูโค้ดที่ไม่ได้ใช้ immutable data กันก่อน

def method1(obj1) # ตำแหน่งที่ 1
  # ...
  method2()
  obj1 # ตำแหน่งที่ 2
  # ...
end

โค้ดนี้มันมีความเป็นไปได้ที่ obj1 ณ ตำแหน่งที่ 2 จะถูกเปลี่ยนแปลงไปจากตอนที่เมท็อดรับมันมา ณ​ ตำแหน่งที่ 1 เรื่องนี้มันเป็นสาเหตุหนึ่งที่ทำให้เราต้องพยายามจำโค้ดของเราทั้งหมดว่ามีอะไรเกิดขึ้น ณ จุดใดบ้าง เราต้องรู้ว่าโค้ดก่อนหน้า อย่างเช่น method2 หรือก่อนหน้านั้น ไม่ได้ไปทำอะไรกับ obj1 ของเราในทางอ้อม ถ้าเราเขียนโค้ดดีก็อาจจะไม่มีการเปลี่ยนแปลงให้น่าปวดหัวแบบนั้นเกิดขึ้น แต่มันไม่มีอะไรรับประกันและป้องกันความผิดพลาดนี้ การเขียนโค้ดผิดพลาดมันเป็นเรื่องธรรมดาที่เกิดขึ้นได้อยู่แล้ว

เมื่อเราต้องการ debug โค้ดนี้ เราก็ต้องพยายามใส่ print ให้ใกล้กับตำแหน่งที่เราต้องการเช็คค่ามากที่สุด เพื่อที่เราจะได้มั่นใจได้ว่าเราได้เห็นค่าที่ถูกต้องจริง ๆ แต่มันก็ยังอาจจะไม่จริงเสมอไปเมื่อโปรแกรมของเรามี concurrency เช่น multi thread เข้ามาเกี่ยวข้อง ค่าที่ print ออกมาก็อาจจะยังไม่ใช่ค่าจริง ๆ ณ เวลานั้น ถึงเราจะใช้ debugger ช่วย มันก็มีปัญหาลักษณะเดียวกันอยู่ดี แถม debugger อาจจะไปรบกวนการทำงานของโปรแกรม ทำให้เราไม่เห็นค่าที่ถูกต้องได้อีกด้วย

Immutable data เข้ามาช่วยลดภาระของสมองของเราตรงนี้

(defn func1 [data1] ; ตำแหน่งที่ 3
  ; ...
  (func2)
  data1 ; ตำแหน่งที่ 4
  (let [data1 "something else"] ; ตำแหน่งที่ 5
    data1 ; ตำแหน่งที่ 6
    )
  ; ...
  )

เมื่อเรามาเขียนภาษาที่ data เป็น immutable ปัญหาที่ผมกล่าวมาก็จะหมดไป ค่าของ data1 ในตำแหน่งที่ 4 จะเท่ากับตำแหน่งที่ 3 เสมอ* ถึงแม้ว่าเราอาจจะสามารถเปลี่ยนให้ data1 ไปชี้ที่ค่าอื่นได้ (shadow) ดังเช่นในตำแหน่งที่ 5 ที่ทำให้ค่าของ data1 ในตำแหน่งที่ 6 ไม่เหมือนตำแหน่งที่ 4 แต่มันเป็นเพียงแค่การเปลี่ยน reference โดยที่ค่าของ data1 จากตำแหน่งที่ 3 และ 4 จะไม่ได้ถูกแตะต้อง เพียงแค่เราไม่สามารถเข้าถึงมันได้ในฟังก์ชันนี้แล้วเท่านั้น สังเกตว่าเราสามารถเห็นการเปลี่ยนแปลงได้ชัดเจนเสมอ เพราะมันไม่ได้ถูกซ่อนเอาไว้ เมื่อเป็นเช่นนี้ เราจะได้รับการการันตีว่าไม่ว่าเราจะตรวจค่าของ data1 ณ จุดใด มันจะเป็นค่าเดียวกับที่ถูกส่งมาให้ หรือเป็นค่าที่เราเป็นคนเปลี่ยนเองในฟังก์ชันนี้เสมอ

ผมมีตัวอย่างคลาสิคอีกหนึ่งอัน

def method3
  obj2 = SomeClass.new # ตำแหน่งที่ 7
  method4(obj2)
  obj2 # ตำแหน่งที่ 8
end

เช่นเดียวกันกับตัวอย่างแรก ในตัวอย่างนี้ไม่มีอะไรที่การันตีเราได้ว่า obj2 ในตำแหน่งที่ 8 มีค่าเหมือนกับ obj2 ในตำแหน่งที่ 7 ถ้าเราเขียนโค้ดไม่ดีก็มีโอกาสเกิดขึ้นได้เสมอ

(defn func3 []
  (let [data2 {:key "value"}] ; ตำแหน่งที่ 9
    (func4 data2)
    data2)) ; ตำแหน่งที่ 10

ในขณะที่โค้ดในตัวอย่างสุดท้ายนี้ data2 ในตำแหน่งที่ 9 และ 10 จะมีค่าเหมือนกันเสมอเพราะ data2 มันชี้ไปที่ immutable data ({:key "value"})

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

*ยกเว้น data1 จะเป็น Java object ที่ mutate ได้ ซึ่งในการเขียนโปรแกรมโดยปกติเราไม่ต้องใช้

Datomic time travel query กับ schema migration

Datomic เป็น database ที่เราสามารถ query จากสถานะของ database ในอดีตได้ เช่น query กับ database ขณะที่ data เป็นเมื่อ 3 อาทิตย์ก่อน ผมเคยมีข้อสงสัยกับคุณสมบัตินี้ว่ามันจะเวิร์คจริงหรอ เช่น ถ้าเราเอาโค้ดปัจจุบันไป query data ในอดีต หรือโค้ดในอดีตที่เรา revert กลับไป ไป query กับ database ปัจจุบันได้เสมอมั้ย (ถ้าใครเคยเขียน Rails อาจจะเคยเจอกรณีที่เมื่อ codebase ของเรา evolve ไปซักพัก เราจะไม่สามารถรัน migration ตั้งแต่ต้นได้ เพราะ model ของเรามีการเปลี่ยนแปลงไปจากวันที่เขียน migration นั้นๆ) หรือเราต้องมีการ sync code กับ database แต่ละช่วงเวลาหรือเปล่า

วันนี้ผมได้คำตอบแล้ว หลังจากได้ฟังคำอธิบายของ David Nolen จากช่วง AMA ของเขาในงาน ReactiveConf 2017

Continue reading

Clojure กับเครื่องหมายมากกว่าและเครื่องหมายน้อยกว่า

Twitter thread นี้คุยกันเรื่องเครื่องหมายมากกว่าและน้อยกว่า ผมได้เทคนิคที่มีประโยชน์กับผมมากเลยครับ

DG0IhziUAAE1Lg2.jpg

ผมเป็นคนที่มีประสบการณ์เดียวกับคนต้น tweet ที่ครูสมัยประถมสอนการจำเครื่องหมายมากกว่าและน้อยกว่าว่า เหมือนกับปาก ที่จะหันไปกินสิ่งที่มากกว่าเสมอ ซึ่งผมก็จำแบบนั้นมาตลอดชีวิตการเรียนวิชาเลข จนมาถึงการเขียนโปรแกรมด้วยภาษาอื่นๆ ที่เป็น infix notation ผมมองเห็นภาพปากหันไปด้านที่มากกว่าเสมอ

พอมาเขียน Clojure ซึ่งเป็น prefix notation ผมต้องใช้วิธีจินตนาการย้ายตำแหน่งของโอเปอเรเตอร์กลับไปอยู่ตรงกลางแบบ infix ทุกครั้ง ที่ก็ถูกต้อง แต่มันมีข้อเสียอยู่ตรงที่ผมกลายเป็นต้องคิด 2 จังหวะอยู่ตลอด ซึ่งมันช้า ในลักษณะเดียวกับที่เวลาเราจะพูดภาษาอังกฤษเราต้องคิดเป็นภาษาไทยก่อนแล้วค่อยๆ แปลเป็นภาษาอังกฤษนั่นแหละ

และโดยเฉพาะอย่างยิ่งโอเปอเรเตอร์ของ Clojure สามารถเปรียบเทียบได้มากกว่า 2 ค่าอีก เช่น (> 3 7 8 5) ผมต้องค่อยๆ มองทีละคู่ ยิ่งทำให้ช้าไปกันใหญ่

ใน twitter thread นี้มีการพูดถึงวิธีการมองที่เหมาะกับ prefix notation ของ Clojure มากกว่า โดยให้มองเป็นการเพิ่มของค่าและการลดของค่าแทน ดังนี้

มองที่ขีดบนของเครื่องหมาย < ซึ่งจะเป็น / หมายถึงการเพิ่มจากน้อยไปมาก ถ้าตัวเลขที่ตามมาเพิ่มขึ้นจากน้อยไปมาก แปลว่าเป็น true หากไม่เป็นไปตามนั้น แปลว่า false
(< 1 4 5 7 9)

เช่นเดียวกัน > คือ \ หมายถึงการลดลงจากมากไปน้อย ถ้าตัวที่ตามมาลดลงจากมากไปน้อย แปลว่า true หากไม่เป็นไปตามนั้น แปลว่า false
(> 8 7 3 2)

สำหรับผมแล้วมันง่ายและเร็วมากขึ้นมากเลยครับ

REPL ของ Clojure

ตอบคำถามในกรุ๊ป Clojure in Thai

REPL ถ้ามองเผินๆ ก็จะคล้าย console หรือ interpreter ของภาษาอื่นๆแหละครับ แต่ของ Clojure จะเน้นให้เราสามารถส่งโค้ดจาก editor ได้ และด้วยความที่ภาษาออกแบบเรื่องการ redefine มาไว้ตั้งแต่ต้น ทำให้เราพัฒนาแบบ incremental / interactive ได้จริงๆ บวกกับการที่มันมีวงเล็บแสดงขอบเขตชัดเจนทำให้การเลือกส่งโค้ดกลุ่มใดๆ ไปที่ repl มันตรงไปตรงมาชัดเจนกว่า ลองคิดภาพดูว่าถ้าเราต้องการเปลี่ยน implementation แค่เมท็อดเดียวใน Ruby เราจะต้อง select โค้ดยังไงใน editor หรือถ้าเราจะ redefine class ใน python เราต้อง select โค้ดยังไง วงเล็บที่ชัดเจนของ Clojure ช่วยในเรื่องนี้ได้มาก

อีกเรื่องหนึ่งที่สำคัญ คือ การทำงานของ Clojure แบบที่เราสั่งให้มันอ่านไฟล์ทั้งหมดเอง กับการที่เราค่อยๆ ส่งทีละ expression ไปที่ repl จะมีค่าเหมือนกัน 100% ทำให้เราไม่ต้องคอยกังวลถึงความแตกต่างใดๆ

ถ้าใครเป็นคนที่ชอบได้รับ feedback เร็วๆ ในระหว่างการ development ชอบ feedback เร็วๆ ของ TDD สำหรับผมแล้ว REPL นี่เร็วกว่า TDD อีก

ถ้าใครที่เชื่อในเรื่องการ make it work ก่อน make it right จะพบว่าเราสามารถเริ่มจากโค้ด snippet ที่ทำงานได้ แล้วค่อยๆ ปั้นไปจนได้โปรแกรมที่มีโครงสร้างชัดเจน maintain ง่ายได้เลยผ่านการเล่นกับ REPL นี่แหละ

Clojure กับการ model โปรแกรมด้วย data structures

ตอบคำถามในกรุ๊ป Clojure in Thai

การ model โปรแกรมด้วย data structures เนี้ยก็คือ model โดยใช้ map ซ้อน map ซ้อน map นี่หละครับ ตอนที่เห็นครั้งแรกผมคิดว่านี่เราถอยหลังลงคลองกันหรือเปล่า นี่มัน Primitive Obsession ซึ่งเป็น code smell แบบหนึ่งหนิ ผมถูกสอนมาในยุค OO ที่เค้าให้ผมสร้าง class ขึ้นมาเพื่อให้เกิด encapsulation และ data hiding เพื่อที่จะได้ระบบที่ high cohesion, low coupling

  • แต่พอได้ทำลองทำจริงๆ กับ Clojure ก็พบว่า เออมันก็ยืดหยุ่นกว่านะ
    มันทำให้ reuse โค้ดได้ง่ายขึ้น เพราะฟังก์ชันไม่ผูกกับ class เราจะเอาฟังก์ชันนี้ไปใช้กับ data structure ไหนก็ได้ ส่งผลให้โค้ดเบสโดยรวมมีขนาดเล็กลงด้วย
  • สร้าง data ขึ้นมาสำหรับเทสก็ง่าย
  • debug ก็ง่าย print ออกมาดูได้เลย ไม่ต้องไป override toString ของ class นั้นๆ
  • จะเทียบ equality ก็ทำได้ทันที ไม่ต้องไป override hashCode
  • แปลงเป็น json ก็ง่าย serialize, deserialize ได้ไม่ยาก

หลายๆ คนที่เค้าเขียน JavaScript ในสไตล์ functional programming เค้าก็อาจจะใช้วิธีนี้กันอยู่บ้างแล้ว แต่พอเป็น Clojure ก็จะได้ข้อดีครงที่ immutable data ก็จะเพิ่มปลอดภัยให้อีกชั้นนึง และมี standard library พร้อม ทำให้ทุกๆ คนใช้ไลบราลีเดียวกัน

ตัวอย่างผลจากแนวคิด isolation ของ Clojure

คัดลอกมาเก็บไว้จากคอมเมนต์ตอบพี่ป้อ อันนี้ (มีเพิ่มเติมอีกนิดหน่อย)

เริ่ม —-

isolation มันฝังอยู่ในทุกๆส่วนของ Clojure ทั้งตัวภาษาและ ecosystem เช่น
– การแยกภาษาจาก runtime ทำให้สร้าง clojurescript ได้ง่าย
– การแยก ผู้ส่งกับผู้รับออกจากกันด้วย channel ของ core.async (ไม่เลือกทางที่ต้องรู้ผู้รับแบบ actor)
– การแยก check/validation ออกมาเรียกใช้เมื่อไหร่ก็ได้ ที่ไหนก็ได้ของ clojure.spec ถ้าไม่อยากใช้ก็ไม่บังคับ
– การแยกวิธีการประมวลผลของ transducer ทำให้เอามันไปใช้ได้กับ sequence หลายประเภท ทั้ง list, vector ธรรมดา, channel และอื่นๆ
– ทุกอย่างมี namespace (แม้กระทั่ง keyword) ทำให้ชื่อเดียวกันของต่าง component กัน ไม่จำเป็นต้องเป็นสิ่งเดียวกันเสมอไป
– เน้นการสื่อสารด้วย data ซึ่งแปลว่าแต่ละ component สามารถตีความหมายของ data ก้อนหนึ่งๆ ได้ตามความต้องการ
– การที่ Datomic แยก database ออกจาก storage ทำให้มันทำงานได้ทั้งบน memory, sql, DynamoDB, Cassandra, … แยก transactor ออกจาก query engine ทำให้ scale query engine ได้ทันที (ต่อตรงไปที่ storage)
– พวกไลบราลีต่างๆ ก็ค่อนข้างทำงานเฉพาะ เช่น ไลบราลีสำหรับ routing ที่ทำหน้าที่ parse url เป็น data อย่างเพียงอย่างเดียว
– Type มี polymorphism แต่ไม่มี inheritance เพราะ inheritance มันทำให้เกิดการผูกกันที่แน่นเกินไป

Isolation เป็นสิ่งที่ทั้ง community เห็นความสำคัญตรงกัน ในทาง implementation บางคนอาจทำได้มากได้น้อยก็แล้วแต่ความสามารถและประสบการณ์ของแต่ละคน แต่มันไปในทิศทางเดียวกัน บางอย่างภาษาอื่นอาจจะมีสิ่งที่คล้ายๆ กัน แต่ก็น่าจะไม่ได้รับความสำคัญเทียบเท่า

ผมว่าเรื่องนี้เป็น value ที่สำคัญมาก และอธิบายยากมากของ Clojure เวลาใครมาถามว่า Clojure ดีอย่างไร ผมจะรู้สึกว่ามันอธิบายยาก เพราะ syntax มันเป็นแค่ส่วนเดียว แต่จริงๆ แล้วความเชื่อใน simplicity และ isolation ของ community และทุกคนมุ่งไปทางนั้นมันสำคัญกว่ามาก

—จบ

Designing is fundamentally about taking things apart. It’s about taking things apart in such a way that they can be put back together (If that makes sense).
So separating things into things that can be composed that’s what design is.
— Rich Hickey