Clojure takeaway : Scoping

Chris
Chris’ Dialogue
Published in
2 min readMay 1, 2017

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

ผมอยากให้ดูตัวอย่างนี้

ข้างบนผมมี function ชื่อ myfunc ทำหน้าที่ง่ายๆ รับ Argument 3 ตัว แล้วเอา (a + b) x c ให้คำตอบ

ทีนี้ถ้าผมต้องการจะสร้าง Naming ขึ้นมาตัวนึงเป็น b-plus-1 สิ่งที่ Clojure หรือ Lisp บังคับให้เราคิดคือ “เราจะใช้มันในวงเล็บไหนบ้าง”

ถ้าอ่านจากวงเล็บจะพบว่า ผมก็ให้มันอยู่ในเฉพาะส่วนที่ใช้ b เท่านั้น ในส่วนของการ คูณกับ c เนี่ย ตัวแปร (หรือใช้คำว่าสัญลักษณ์) b-plus-1 ก็ใช้ไม่ได้อีกต่อไปแล้ว

ซึ่งการบังคับให้เราต้องคิดแบบนี้ ในภาษาอื่นนั้นจะไม่เยอะ เพราะมันไม่ได้มีวงเล็บที่กำหนดสโคปของทุก Expression ทุกคำสั่งอยู่ตลอดเวลา จนเราไม่ต้องคิดว่าจะแทรกตรงไหนบ้างที่จำเป็น

ผมยกตัวอย่าง Javascript

คือโดยธรรมชาติเราจะประกาศตัวแปรขึ้นมาใน Function scope แล้วใช้มันเลย

ซึ่งถ้า Function เล็กขนาดนี้ ก็ไม่เป็นไรหรอก แต่ลองนึกภาพว่าถ้า Function เป็นแบบนี้ล่ะ

ถ้าสมมติว่าผมกำลังแก้ไขอยู่แล้วผมประกาศ myTemporaryVariable ขึ้นมา แล้วข้างล่างมันมีอีก 100 บรรทัด

มันยากมากเลยนะที่จะบอกว่าตกลงแล้ว myTemporaryVariable นี่มันถูกใช้งานเยอะขนาดไหน ถ้าเราแก้ไขมันจะไปกระทบอะไรบ้าง เพราะจำนวนบรรทัดข้างล่างมันเยอะ การจะหาก็ใช้เวลา

(ปกติที่ผมทำคือต้องใช้ Find/Replace หรือไม่ก็ Linter ช่วยหาว่ากระทบอะไรบ้าง)

ผมว่านั่นเป็นสาเหตุสำคัญเลยที่ทำให้ Imperative รวมถึง Object-oriented Language มีข้อบังคับว่า Method หรือ Function ควรจะมีจำนวนบรรทัดน้อยๆ เสมอ เพราะพอบรรทัดมันเยอะ ตัวแปรที่ประกาศใน Scope นั้นจะเริ่มเยอะ ทำให้เกิดความซับซ้อนเป็นอย่างมาก

ไม่เหมือนกับ Clojure คือ ถ้าใน Clojure นาทีที่เราประกาศ Temporary variable (ในที่นี้ไม่ใช่ตัวแปรนะ เอาเป็นว่า Naming ละกัน) ขึ้นมา คือเราต้องกำหนดแล้วว่า มันจะถูกใช้ใน Scope วงเล็บขนาดไหน ใช้ถึง Expression ไหนบ้าง

นั่นทำให้ Scope ของ Clojure มัน Minimal ทันที

แล้วเวลาที่เราจะย้าย Function หรือ Debug อะไรใน Clojure มันง่ายตรงที่ว่า ทุกๆ Expression ทุกๆ บรรทัด ทุกๆ วงเล็บ เรารู้ชัดเจนเลยว่า Input ที่จำเป็นในการที่ให้บรรทัดนี้ คำสั่งนี้ทำงานถูกต้อง มีอะไรบ้าง

ไม่ต้องกังวลว่า Method นี้ มันไปใช้ Singleton อันนั้น เราต้องเตรียม Singleton ก่อนมั้ยนะ

ไม่ต้องกังวลว่า มันจะ Access property อันนี้ แล้ว Property นี้ต้อง Initialize จาก Constructor ตัวนั้น พอ Construct เสร็จก็ต้องเตรียมพร้อมด้วย Method นี้ก่อน ถึงจะเริ่ม Test method นี้ได้

ไม่ต้องปวดหัวกับการไล่ตามว่า ตกลงฉันต้อง Setup อะไรบ้างเพื่อให้ทำงานได้

เพราะ Clojure ประกาศชัดมาก ว่า ใน Scope นี้ มีอะไรให้ใช้มั่ง

Minimal scoping make it simple

สำหรับ Key takeaway อย่างนึงที่ได้จากการลองเขียน Clojure แล้วคิดว่าไปใช้กับทุกภาษาได้

คือไม่ว่าเราจะทำ OOP ทำ Imperative หรือทำภาษาอะไรก็ตาม

พยายามรักษา Scope ของแต่ละ Function ไว้ให้ดี

ถ้าเราเขียน Method อันนึง ใน Object อันนึง

พยายามทำให้ Object นั้นมี State น้อยที่สุดเท่าที่จะน้อยได้ แล้วเราจะรู้ว่า Method นั้นสามารถ Access data ได้จำนวนน้อยมากๆ ได้จำนวนเท่าที่จำเป็น

นั่นแหละจะทำให้ Test ง่าย และแก้ไขง่าย Refactor ง่าย

ถ้ามันมีพลังในการ Access data จำนวนมากๆ เพราะ Object เราใหญ่ มี State เยอะ

ต่อให้มันใช้ State น้อยมาก เราก็จะกังวลอยู่ดี (ดังนั้นถ้ามันเป็นแบบนั้นไปแล้ว วิธีแก้คือเขียนฟังก์ชั่นสั้นๆ เล็กๆ แบบที่ OOP Practictioner มักจะแนะนำ)

และอย่าพยายามไปเรียกใช้ Singleton เรียกใช้ Global อะไรก็ตามถ้าไม่จำเป็น หรือถ้าจำเป็นก็ Inject มันเข้ามาใน Object ให้มั่นใจว่ามันอยู่ใน Scope เราประกาศชัดเจนว่า Object นี้จำเป็นต้องรู้ Global ตัวนี้

เวลาที่เราเขียนให้ทุกอย่างชัดเจนว่า Scope ของ Method นี้มันต้องการอะไรบ้าง ไม่มี Implicit หรือ “ความลับ” แบบที่ต้องลองรันดูก่อนถึงจะรู้ได้

มันทำให้ Refactor หรือ Debug ง่ายจริงๆ ครับ

อย่างเช่น Class สำหรับส่งอีเมล์ จะต้อง Construct เสร็จ เรียก Authen ก่อน ได้ Authen คืนมา Method ที่ชื่อว่า .send() ถึงจะทำงานได้

var sender = MailSenderFactory.create(user, pass)
if (sender.authenClient()) {
sender.send("somemail@gmail.com", "Hello", "Hello world")
}

อันนี้แหละผมเรียกว่า Implicit คือ มันอ่านจาก .send() มันไม่มีทางรู้ได้ว่า เห้ย ตกลงสภาวะไหนฉันถึงจะพร้อมส่งอีเมล์นะ เวลาเราจะ Debug มันจะลองเล่นกับมัน ก็ต้องไปหาอ่านทั่ว Codebase ก่อนเลยว่า เห้ย ฉันจะต้อง Setup อะไรบ้าง sender ถึงจะ send ได้

มันไม่ชัดเจนตั้งแต่อ่านโค้ดครั้งแรก

แต่ถ้าเราทำแบบนี้คือ

// Class MailConnection
MailConnection.connect() -> authenticatedMailConnection
// Class mailsender
MailSender.send(authenticatedMailConnection a) -> void

แบบนี้มันชัดเจนมากเลยว่า การส่งอีเมล์มันต้องเตรียมอะไรบ้าง ตั้งแต่ Signature ของมันแล้ว

นี่แหละครับคือ Key takeaway ตัวนึงที่ผมว่าสำคัญมากในการสร้าง Code ที่ดูแลง่าย

Scoping ครับ กำหนดให้ทุกอย่าง “รู้เท่าที่จำเป็นต้องรู้” “ใช้เท่าที่จำเป็นต้องใช้”​ และ​มีความชัดเจนว่า “ฉันต้องการอะไรบ้าง ถึงจะทำงานได้”

เรื่องนี้ช่วยได้มากจริงๆ ครับ

--

--

Chris
Chris’ Dialogue

I am a product builder who specializes in programming. Strongly believe in humanist.