การเขียนโปรแกรมจากซ้ายไปขวา
(graic.net)- แนวทางการเขียนโปรแกรมจากซ้ายไปขวา ทำให้โปรแกรมยังคงอยู่ในสถานะที่ถูกต้องทันทีที่พิมพ์โค้ดลงไป ส่งผลให้ การรองรับจากเครื่องมือ อย่างการเติมโค้ดอัตโนมัติของเอดิเตอร์ทำงานได้เต็มประสิทธิภาพ
- list comprehension ของ Python รบกวนการทำงานของการเติมโค้ดอัตโนมัติ เนื่องจากมีตัวแปรที่ยังไม่ได้ประกาศและไม่มีการอนุมานชนิดข้อมูล
- Rust และ JavaScript สามารถประกอบโปรแกรมจากซ้ายไปขวาได้อย่างเป็นธรรมชาติ ทำให้การใช้งานตัวแปรและการสำรวจเมธอดทำได้ตรงไปตรงมามากกว่า
- สไตล์แบบฟังก์ชันใน C และ Python ทำให้ชื่อฟังก์ชันหรือโครงสร้างค้นพบได้ยาก จนลดทอนประสบการณ์การเขียนโค้ดอย่างมีประสิทธิภาพ
- สำหรับ ลอจิกที่มีความซับซ้อนสูง โค้ดที่คลี่ออกจากซ้ายไปขวาจะอ่านง่ายกว่า และมีข้อได้เปรียบด้านการบำรุงรักษาและการขยายต่อ
การเขียนโปรแกรมจากซ้ายไปขวา
โค้ดควรถูกต้องทันทีที่พิมพ์ลงไป
ข้อจำกัดของ Python list comprehension
- ไวยากรณ์ list comprehension ของ Python อย่าง
words_on_lines = [line.split() for line in text.splitlines()]มีปัญหาเพราะต้องเข้าถึงตัวแปร (line) ที่ยังไม่ได้ประกาศ ทำให้เอดิเตอร์ไม่สามารถให้ การเติมโค้ดอัตโนมัติ หรือการอนุมานชนิดข้อมูลได้อย่างเหมาะสม - ระหว่างกระบวนการพิมพ์โค้ดทีละส่วน
- หากพิมพ์เป็น
words_on_lines = [line.spเอดิเตอร์จะไม่รู้ชนิดของlineจึงไม่สามารถแนะนำเมธอดได้ - ยังตรวจจับข้อผิดพลาดที่อาจเกิดขึ้น เช่น การพิมพ์ชื่อตัวแปรผิด (
limeเป็นต้น) ได้ยาก
- หากพิมพ์เป็น
- หากต้องการรับคำแนะนำที่ถูกต้อง ผู้ใช้กลับต้องเขียนโค้ดที่ยังไม่เสร็จให้ครบในระดับหนึ่ง ซึ่งเป็นกระบวนการที่ ไม่เป็นธรรมชาติและไม่สะดวก
การประกอบจากซ้ายไปขวาใน Rust
- ตัวอย่างใน Rust (
let words_on_lines = text.lines().map(|line| line.split_whitespace());) แสดงให้เห็นว่า- เมื่อมีการประกาศฟังก์ชันนิรนาม ตัวแปร (
line) ที่ปรากฏขึ้นครั้งแรกจะถือว่าเป็นการประกาศทันที จึงสามารถใช้ การเติมโค้ดอัตโนมัติและคำแนะนำเมธอด ได้ในทันที - เมธอด
split_whitespaceเองก็ถูกค้นพบได้ง่ายเพราะมีระบบแนะนำอัตโนมัติ
- เมื่อมีการประกาศฟังก์ชันนิรนาม ตัวแปร (
- วิธีนี้ทำให้โปรแกรมยังคงอยู่ใน สถานะที่ถูกต้องอย่างน้อยบางส่วนเสมอ ดังนั้น IDE หรือเอดิเตอร์จึงช่วยรองรับการเขียนโค้ดแบบเรียลไทม์ได้
การเปิดเผยแบบค่อยเป็นค่อยไป (Progressive Disclosure) และการใช้งาน API
- การเปิดเผยแบบค่อยเป็นค่อยไป (Progressive Disclosure) คือหลักการออกแบบที่ให้ผู้ใช้พบความซับซ้อนเท่าที่จำเป็น และสามารถนำมาใช้กับการเขียนโปรแกรมได้
- ตัวอย่าง: คล้ายกับ UX ของโปรแกรมประมวลผลคำที่จะแสดงตัวเลือกที่เกี่ยวข้องเฉพาะเมื่อเพิ่มรูปภาพ
- ภาษา C ขาดการรองรับลักษณะนี้
- ฟังก์ชันทั้งหมดที่เกี่ยวข้องกับ
FILE *fileไม่ได้ถูกสำรวจผ่านfile.จึงต้องจำรูปแบบชื่อฟังก์ชันอย่างfread,fcloseเป็นต้น และยากต่อการค้นพบความสามารถต่าง ๆ - ในทางกลับกัน หากเป็นภาษาที่ออกแบบได้เหมาะสม ผู้ใช้ควรค้นพบความสามารถที่เกี่ยวข้องได้แบบ ค่อยเป็นค่อยไป ผ่านคำแนะนำเมธอดจาก
file.
- ฟังก์ชันทั้งหมดที่เกี่ยวข้องกับ
ความแตกต่างด้านการค้นพบได้ของฟังก์ชันและเมธอด
- เปรียบเทียบตัวอย่าง
map(len, text.split())ของ Python กับtext.split(" ").map(word => word.length)ของ JavaScript- ใน Python ชื่อฟังก์ชันอย่าง
len,length,sizeคาดเดาได้ยาก จึงอาจต้องลองหลายครั้งกว่าจะรู้ว่าคำสั่งใดใช้ได้จริง - ใน JavaScript เพียงพิมพ์
word.แล้วตามด้วย.lเอดิเตอร์ก็สามารถเสนอlengthและเมธอดอื่น ๆ ได้ จึงมี ความสามารถในการค้นพบสูงกว่า - แม้แต่ฟังก์ชันระดับสูงอย่าง
mapก็ยังทำให้ค่าที่ส่งกลับและชนิดข้อมูลที่เกี่ยวข้องปรากฏชัดได้ทันที
- ใน Python ชื่อฟังก์ชันอย่าง
ยิ่งลอจิกซับซ้อน ยิ่งเห็นข้อดีของการเขียนแบบมีโครงสร้าง
- ในกรณีของลอจิกที่มีความซับซ้อนสูง (เช่น โค้ด Python ยาว ๆ ที่มี
filterและlambdaซ้อนกัน)- ผู้เขียนต้องคอยย้อนดูจุดเริ่มต้นและจุดสิ้นสุดของโค้ดซ้ำไปมา และเกิด ปัญหาด้านความอ่านง่าย รวมถึงความยากในการทำความเข้าใจจากเงื่อนไขหรือการจับคู่วงเล็บ
- แต่ในเวอร์ชัน JavaScript ของลอจิกเดียวกัน สามารถ อ่านและทำความเข้าใจอย่างเป็นลำดับ ได้จากบนลงล่างและจากซ้ายไปขวา
หลักการสำคัญ
โค้ดควรถูกต้องในทุกขณะที่พิมพ์
- แม้พิมพ์เพียง
textโปรแกรมก็ยังคงอยู่ในสถานะที่ถูกต้อง - เมื่อเขียนถึง
text.split(" ")แล้ว และพิมพ์ต่อเป็น.map(word => word.length)สถานะระหว่างทางทั้งหมดก็ยัง คงความถูกต้องอยู่เสมอ - รูปแบบการเขียนโค้ดเช่นนี้ช่วยเพิ่มโอกาสให้เอดิเตอร์ รองรับแบบเรียลไทม์ ได้ดีขึ้น และในสภาพแวดล้อม REPL ก็สามารถเห็นผลลัพธ์ได้ทันที
บทสรุป
- การออกแบบภาษาและ API ควรสนับสนุนให้ผู้ใช้พิมพ์โค้ดจากซ้ายไปขวาได้อย่างเป็นธรรมชาติ และทำให้ทุกขั้นตอนระหว่างทางเป็น โปรแกรมที่ถูกต้อง
- การออกแบบ API ที่ดีคือหัวใจสำคัญของการยกระดับประสบการณ์การเขียนโค้ดในลักษณะนี้
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
FROM tableแทนSELECT * FROM tableก็น่าจะละ SELECT clause ไปได้ด้วยซ้ำ ถึงจะฟังเหมือนคนแก่ช่างบ่น แต่ก็เป็นแค่ความค้างคาใจส่วนตัวของผมTABLE <table>ด้วย{3} for {2} in {1}ก็ได้ เครื่องมือแบบนี้เป็นจุดประนีประนอมระหว่าง "ไวยากรณ์ที่อ่านง่าย" กับ "ไวยากรณ์ที่พิมพ์ง่าย" สำหรับผม ผมเอนเอียงไปทางให้ความสำคัญกับไวยากรณ์ที่อ่านง่าย แม้ต้องพึ่ง tooling ก็ตาม ผมไม่ได้คิดว่าจำเป็นต้องยึดติดกับโครงสร้าง "for-in" อย่างเดียวbake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12)ถ้าใช้ pipe เป็นmix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12)จะง่ายและดูดีกว่ามากfrom some_library import child_moduleของ Python นั้นตรงไปตรงมามาก ส่วนใน JS จะเป็นโครงสร้างอย่างimport { asYetUnknownModule } from SomeLibraryซึ่งให้ความรู้สึกไม่ตรงไปตรงมาน้อยกว่าเยอะlen(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs)))ผมคิดว่าใช้ NumPy array จะดีกว่า เพราะไม่ต้องสร้างลิสต์ใหม่ในหน่วยความจำและยังจัดการทั้งบรรทัดได้พร้อมกันดีกว่าด้วย เช่น แบบนี้สะท้อนความเป็น "ซ้ายไปขวา" ได้มากกว่าเยอะ