บทที่ 1
การคิดเชิงคำนวณ (Computational Thinking)#

จุดมุ่งหมายของบทนี้

  • แบ่งปัญหาออกเป็นส่วนย่อย ที่ต้องแก้ออกเป็นส่วนเล็ก ๆ

  • การเขียนฟังก์ชันที่นำกลับมาใช้อีกได้

  • การใช้วงวน for while แบบไม่มีตัวแปร

  • การกำหนดเงื่อนไข if และ else ประกอบกับบูลีน และตัวดำเนินการบูลีน and or not

  • การใช้คำสั่ง break

หลักสำคัญของการเขียนโปรแกรมมิได้อยู่ที่การจดจำคำสั่งทั้งหมด แต่เน้นที่การนำคำสั่งมาประยุกต์ใช้และประกอบกันอย่างเหมาะสมเพื่อให้ระบบสามารถปฏิบัติงานตามที่ผู้พัฒนาต้องการได้อย่างมีประสิทธิภาพ เมื่อได้รับโจทย์ เช่น การดึงข้อมูลความคิดเห็นจากโซเชียลมีเดียเพื่อนำมาวิเคราะห์ว่าผู้สมัครรับเลือกตั้งรายใดได้รับความสนใจจากสาธารณชนมากที่สุด หรือเขียนแอปพลิเคชันในการวิเคราะห์การใช้ภาษาของข้อความที่กำหนดให้ จะพบว่าโจทย์ดังกล่าวมีขอบเขตที่กว้างและซับซ้อน จนอาจไม่รู้ว่าจะเริ่มจากจุดไหนก่อน การแก้ปัญหาเชิงคำนวณ (computational thinking) เป็นหลักการสำคัญที่จะช่วยในการแก้โจทย์ลักษณะนี้ โดยมีองค์ประกอบหลัก 4 ประการ

  1. การแบ่งปัญหาออกเป็นส่วนย่อย (Decomposition) หมายถึง การวิเคราะห์และแบ่งแยกโจทย์ปัญหาออกเป็นส่วนย่อย ๆ เพื่อให้สามารถจัดการและแก้ไขได้ง่ายขึ้น ตัวอย่างเช่น หากเราต้องการประกอบตู้ลิ้นชักใส่เสื้อผ้า กระบวนการนี้สามารถแบ่งออกเป็นขั้นตอนต่าง ๆ ได้แก่ การประกอบลิ้นชัก การประกอบโครงตู้ และการติดตั้งขาตู้ จากนั้นนำแต่ละส่วนมาประกอบรวมกันเป็นตู้ลิ้นชักที่สมบูรณ์

  2. การสังเกตหาแบบแผน (Pattern Recognition) หมายถึง การพิจารณาและค้นหาแบบแผนหรือรูปแบบที่เกิดขึ้นซ้ำ ๆ ในปัญหาย่อย ๆ ซึ่งจะช่วยให้สามารถแก้ไขปัญหาได้อย่างมีประสิทธิภาพ เช่น ในกระบวนการประกอบลิ้นชัก แม้ว่าแต่ละลิ้นชักจะมีขนาดแตกต่างกัน แต่ขั้นตอนการประกอบ เช่น การขันน็อตทั้งสี่ด้านและการติดตั้งมือจับ ยังคงมีรูปแบบที่เหมือนกัน

  3. การคิดแบบนามธรรม (Abstraction) หมายถึง การระบุแก่นแท้ของปัญหาหรือกระบวนการ โดยตัดรายละเอียดที่ไม่จำเป็นออกไป ตัวอย่างเช่น ลิ้นชักทั้งหมดในตลาดจะมีลักษณะพื้นฐานที่คล้ายกัน คือ เป็นกล่องสี่เหลี่ยม มีฝาด้านหน้าที่สามารถดึงออกได้ ส่วนความแตกต่าง เช่น ขนาด วัสดุ หรือรูปทรงของหูจับ ถือเป็นรายละเอียดที่ไม่เกี่ยวข้องกับแก่นของการประกอบลิ้นชัก

  4. การออกแบบอัลกรอริธึม (Algorithm Design) หมายถึง การสร้างขั้นตอนหรือกระบวนการในการแก้ปัญหาอย่างชัดเจนและเป็นระบบ เพื่อให้สามารถนำไปปฏิบัติได้ ตัวอย่างเช่น อัลกอริทึมสำหรับการประกอบตู้ลิ้นชักใส่เสื้อผ้าอาจประกอบด้วยขั้นตอนในการประกอบแต่ละชิ้นส่วนและรวมเป็นโครงสร้างที่สมบูรณ์

1. ไล่ประกอบขาตู้ แล้วเอาพักไว้ก่อน
2. ประกอบโครงตู้ แล้วเอาพักไว้ก่อน
3. ประกอบผลลัพธ์จาก 1 และ 2
4. ถ้าตู้มี k ลิ้นชัก ไล่ประกอบตัวลิ้นชักจนครบ k อัน
5. สอดลิ้นชักทั้ง k อันเข้าไปในโครงตู้

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

หุ่นยนต์คาเรล#

ทักษะการคิดเชิงคำนวณตามตัวอย่างที่กล่าวมาข้างต้น ถือเป็นทักษะที่สำคัญยิ่งสำหรับการเขียนโปรแกรม ซึ่งในบทนี้เราจะฝึกฝนผ่านการใช้โปรแกรมที่ชื่อว่า คาเรล (Karel) [Pattis, 1994] คาเรลเป็นหุ่นยนต์จำลองขนาดเล็กที่สามารถเคลื่อนที่ไปมาภายในโลกสี่เหลี่ยมที่ถูกกำหนดไว้ โดยผู้ใช้สามารถเขียนโปรแกรมด้วยภาษาไพทอนเพื่อสั่งการคาเรลให้ปฏิบัติตามคำสั่งต่าง ๆ ที่ตั้งไว้สำหรับการแก้โจทย์ในสถานการณ์ที่กำหนดได้ ในขั้นตอนนี้ เราจะใช้คาเรลเพื่อฝึกฝนทักษะการคิดเชิงคำนวณ ก่อนที่จะเข้าสู่การเขียนโปรแกรมภาษาไพทอนในระดับที่ซับซ้อนขึ้น ซึ่งจำเป็นต้องเรียนรู้คำสั่งเพิ่มเติมอีกมากมาย วัตถุประสงค์ของการฝึกฝนผ่านคาเรล คือ การพัฒนาทักษะการคิดเชิงคำนวณของนักเขียนโปรแกรม โดยไม่ต้องจำคำสั่งที่ซับซ้อนจำนวนมาก

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

คำสั่ง

คำอธิบาย

move()

สั่งให้คาเรลเดินไปข้างหน้า ถ้าข้างหน้ามีกําแพงมันจะชนและเกิดข้อผิดพลาด

turn_left()

สั่งให้คาเรลเลี้ยวซ้าย

put_beeper()

วางกระดิ่งหนึ่งอันไว้บนจุดที่ยืนอยู่

pick_beeper()

เก็บกระดิ่งขึ้นมาหนึ่งอันจากจุดที่ยืนอยู่ (ถ้าไม่มีกระดิ่งที่จุดนั้น จะเกิดข้อผิดพลาด)

ตัวอย่าง#

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

../../_images/karel-ex1.png

ภาพที่ 4 โลกของคาเรลในโจทย์ จุดเริ่มต้นของโจทย์แสดงอยู่ในภาพด้านซ้าย จุดมุ่งหมายของโจทย์แสดงอยู่ในภาพด้านขวา#

โจทย์นี้อาศัยนำคำสั่งหลาย ๆ คำสั่งมาต่อกันในลำดับที่เหมาะสม ดังนี้

move()
pick_beeper()
turn_left()
move()
turn_left()
move()
put_beeper()

ถ้าหากโปรแกรมสั่งให้คาเรลชนกำแพง ดังโค้ดข้างล่างนี้

move()
move()

โปรแกรมจะเกิดข้อผิดพลาด และจะหยุดทำงานทันที

หรือถ้าหากโปรแกรมสั่งให้คาเรลหยิบกระดิ่งในจุดที่ไม่มีกระดิ่ง ดังโค้ดข้างล่างนี้

move()
turn_left()
move()
pick_beeper()

โปรแกรมจะเกิดข้อผิดพลาด และจะหยุดทำงานทันทีเช่นกัน

โปรแกรมและฟังก์ชัน#

สมมติว่าโจทย์คือ ให้คาเรลไปหยิบกระดิ่งตรงหน้าเนินแล้วเดินข้ามเนินไปวางกระดิ่งที่คอลัมน์ที่ 5 ดังภาพด้านล่าง

../../_images/karel-ex2.png

ภาพที่ 5 โลกของคาเรลในโจทย์ จุดเริ่มต้นของโจทย์แสดงอยู่ในภาพด้านซ้าย จุดมุ่งหมายของโจทย์แสดงอยู่ในภาพด้านขวา#

โจทย์นี้ซับซ้อนขึ้นมาก เนื่องจากต้องใช้คำสั่งหลายบรรทัด สมมติว่าเขียนโค้ดไพทอนเก็บไว้ในไฟล์ my_karel_first.py ดังนี้

# โปรแกรมสำหรับแก้โจทย์แรกในบทนี้
from stanfordkarel import *

def main():
    move()
    pick_beeper()
    move()
    turn_left()
    move()
    turn_left()
    turn_left()
    turn_left()
    move()
    move()
    put_beeper()
    move()

if __name__ == "__main__":
    run_karel_program()

ส่วนประกอบของโปรแกรมในไฟล์นี้มีดังนี้

คอมเมนต์#

# โปรแกรมสำหรับแก้โจทย์แรกในบทนี้

ข้อความที่นำหน้าด้วยเครื่องหมาย # เรียกว่า หมายเหตุ หรือคอมเมนต์ (comment) ซึ่งเป็นการแสดงความคิดเห็นหรือให้ข้อมูลเพิ่มเติมสำหรับผู้อ่านโค้ด โดยคอมเมนต์จะช่วยอธิบายการทำงานของโปรแกรมและจุดสำคัญที่ควรทราบ เพื่อให้ผู้ที่นำโค้ดไปใช้งานต่อสามารถเข้าใจการทำงานได้อย่างชัดเจน นอกจากนี้ การใช้คอมเมนต์ยังเป็นแนวทางในการเพิ่มความเข้าใจร่วมกันในทีมพัฒนาหรือในกรณีที่ต้องกลับมาทบทวนโปรแกรมในภายหลัง

การนำเข้าไลบรารี#

from stanfordkarel import *

ไลบรารี (Library) คือ คลังของโค้ดหรือฟังก์ชันที่ถูกเขียนขึ้นล่วงหน้าและรวบรวมไว้เพื่อให้นักพัฒนาสามารถนำไปใช้งานได้โดยไม่ต้องเขียนโค้ดใหม่ตั้งแต่ต้น ไลบรารีมักจะประกอบไปด้วยฟังก์ชัน และเครื่องมือที่ช่วยในการทำงานเฉพาะด้าน ในบทนี้ไลบรารีที่ต้องนำเข้ามาคือ stanfordkarel ซึ่งมีฟังก์ชันในการสั่งการคาเรลให้ปฏิบัติงานต่าง ๆ ได้ เช่น move() และ put_beeper()

การประกาศฟังก์ชัน#

def main():
    move()
    pick_beeper()
    move()

ส่วนนี้เรียกว่า ฟังก์ชัน โดยฟังก์ชันมีลักษณะสำคัญดังนี้:

  • ส่วนหัวของฟังก์ชัน (function header) ประกอบด้วย

    • คำสำคัญ (keyword) def ซึ่งใช้สำหรับประกาศฟังก์ชันใหม่

    • ชื่อของฟังก์ชันซึ่งตามหลังคำว่า def และตามด้วยวงเล็บเปิด-ปิด และโคลอน ():

  • ส่วนเนื้อหาของฟังก์ชัน (function body) คือ กลุ่มของคำสั่งที่ประกอบอยู่ภายในฟังก์ชัน โดยคำสั่งเหล่านี้จะต้องมีการย่อหน้า (indentation) ในทุกบรรทัด เพื่อบ่งชี้ว่าคำสั่งเหล่านี้เป็นส่วนหนึ่งของฟังก์ชันนั้น

กำหนดคำสั่งการรันไฟล์ไพทอน#

if __name__ == "__main__":
    run_karel_program()

โปรแกรมตัวอย่างนี้ถูกจัดเก็บในไฟล์ที่มีนามสกุล .py ซึ่งเมื่อไฟล์ถูกเรียกใช้ โปรแกรมจะทำงานทั้งไฟล์ โค้ดที่แสดงข้างต้นเป็นการกำหนดลำดับการทำงานของโปรแกรม โดยใช้โครงสร้าง if __name__ == "__main__": เพื่อระบุว่า หากโปรแกรมถูกรันโดยตรง ฟังก์ชัน run_karel_program() จะถูกเรียกใช้งานก่อน ซึ่งฟังก์ชันนี้มีหน้าที่ในการโหลดหน้าต่างที่แสดงผลหุ่นยนต์คาเรล จากนั้นโปรแกรมจะไปเรียกฟังก์ชัน main() ซึ่งได้ถูกประกาศไว้ในไฟล์เดียวกันนี้

การแบ่งปัญหาออกเป็นส่วนย่อย#

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

turn_left()
turn_left()
turn_left()

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

def main():
    move()
    pick_beeper()
    move()
    turn_left()
    move()
    turn_right() #หันขวา ฟังก์ชันใหม่
    move()
    move()
    put_beeper()
    move()

def turn_right():
    turn_left()
    turn_left()
    turn_left()

เพื่อปรับปรุงโครงสร้างของโค้ด เราได้สร้างฟังก์ชันใหม่ชื่อ turn_right() ซึ่งมีบทบาทสำคัญในการทำให้โค้ดในฟังก์ชัน main มีความกระชับและชัดเจนมากขึ้น การย้ายคำสั่งออกจากฟังก์ชัน main ไปไว้ในฟังก์ชันย่อยนี้ไม่เพียงแต่ช่วยลดปริมาณโค้ดในฟังก์ชันหลัก แต่ยังช่วยให้โค้ดดูแลรักษาในระยะยาวได้ง่ายกว่าด้วย

หลังจากที่ฟังก์ชันถูกสร้างขึ้น เราสามารถเรียกใช้งานฟังก์ชันจากส่วนต่าง ๆ ของโปรแกรมได้โดยการพิมพ์ชื่อฟังก์ชันตามด้วยเครื่องหมายวงเล็บ () ซึ่งเป็นส่วนที่จำเป็นสำหรับการเรียกใช้งานฟังก์ชัน ตัวอย่างเช่น คำสั่ง turn_right() หรือฟังก์ชันพื้นฐานอื่น ๆ เช่น move() และ pick_beeper() เป็นคำสั่งที่ทำงานในลักษณะเดียวกัน แต่เราไม่จำเป็นต้องสร้างขึ้นใหม่ เนื่องจากมีการนำเข้าจากไลบรารีที่เกี่ยวข้องแล้ว

สรุปส่วนต่าง ๆ ของฟังก์ชันได้ดังนี้

../../_images/function.png

ข้อควรระวัง#

การสร้างฟังก์ชันในภาษาไพทอนจำเป็นต้องปฏิบัติตามรูปแบบที่ถูกต้องตามกฎไวยากรณ์ (syntax) เนื่องจากภาษาไพทอน เป็นภาษาการเขียนโปรแกรมที่มีโครงสร้างชัดเจนและยึดกับไวยากรณ์อย่างเคร่งครัด การเขียนโค้ดผิดจากไวยากรณ์แม้แต่เพียงเล็กน้อยจะส่งผลให้โปรแกรมไม่สามารถทำงานได้ตามที่คาดหมาย ตัวแปลภาษาไพทอนจะตรวจสอบโค้ดเมื่อมีการรันโปรแกรม และหากพบความผิดพลาด โปรแกรมจะหยุดทำงานทันทีพร้อมแสดงข้อความแจ้งข้อผิดพลาด (error message) ซึ่งศัพท์เทคนิคในการเขียนโปรแกรมเรียกว่าโยน (throw) ข้อผิดพลาด เพื่อช่วยให้ผู้เขียนโปรแกรมสามารถตรวจสอบและแก้ไขโค้ดได้อย่างถูกต้อง ข้อผิดพลาดมีหลายประเภท ข้อผิดพลาดที่พบบ่อยประเภทหนึ่งคือ ข้อผิดพลาดทางไวยากรณ์ (syntax error) เช่น การลืมวงเล็บ การพิมพ์ชื่อฟังก์ชันผิด หรือการจัดวางบรรทัดโค้ดที่ไม่ถูกต้อง ตัวแปลภาษาไพทอนก็จะทำการโยน SyntaxError ดังตัวอย่างดังนี้

  File "my_first_karel.py", line 4
    def main()
             ^
SyntaxError: invalid syntax

ตัวแปลภาษาไพทอนโยน SyntaxError มา เพราะว่าเราลืม : ท้ายหัวฟังก์ชัน ในบรรทัดที่ 4 เพื่อแจ้งให้ทราบว่าเราเขียนโค้ดผิดไวยากรณ์ หรือถ้าเราลืม () ก็จะได้ SyntaxError เช่นกัน

อีกตัวอย่างหนึ่งคือ หากลืมใส่วงเล็บเปิดและปิด (()) ในหัวฟังก์ชัน ก็จะเกิดข้อผิดพลาดทางไวยากรณ์ (SyntaxError) เช่นเดียวกัน

  File "my_first_karel.py", line 4
    def main:
            ^
SyntaxError: invalid syntax

ข้อผิดพลาดอีกประเภทที่พบบ่อยสำหรับผู้ที่กำลังเริ่มต้นเขียนโปรแกรมคือ ข้อผิดพลาดในการจัดย่อหน้า (indentation) ตัวแปลภาษาไพทอนจะโยนข้อผิดพลาด IndentationError มาให้ ดังตัวอย่างดังนี้

  File "my_first_karel.py", line 5
   move()
      ^
IndentationError: expected an indented block

ตัวแปลภาษาไพทอนโยน IndentationError มาให้ถ้าเราลืมกั้นย่อหน้าให้คำสั่ง move() ในบรรทัดที่ 5 พร้อมแนะนำวิธีการแก้ว่าอาจจะต้องเพิ่มย่อหน้าให้เป็นบล็อกใหม่ (expected an indented block)

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

การคิดเชิงคำนวณเพื่อในการเขียนโปรแกรม#

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

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

../../_images/repair-road.png

ในความเป็นจริงแล้ว โจทย์นี้ไม่ได้ซับซ้อนมากนัก แนวทางการแก้ปัญหาที่ได้มาโดยไม่อาศัยกระบวนการคิดเชิงคำนวณ จะมีลักษณะดังโค้ดด้านล่างนี้ ซึ่งในที่นี้เราจะหยิบยกขึ้นมาเฉพาะฟังก์ชัน main และสมมติว่าเราได้ดำเนินการนำเข้าไลบรารีที่จำเป็นทั้งหมด และได้บันทึกโค้ดลงในไฟล์ที่มีนามสกุล .py

def main():
    move()
    turn_right()
    move()
    put_beeper()
    turn_right()
    turn_right()
    move()
    turn_right()
    move()
    move()
    move()
    # ไม่สมบูรณ์ยังมีต่ออีก...    

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

เราจะทดลองแก้ปัญหานี้อีกครั้งโดยใช้วิธีการแบ่งปัญหาออกเป็นส่วนย่อย ซึ่งเป็นขั้นตอนแรกในการคิดเชิงคำนวณที่ช่วยให้การแก้ปัญหามีประสิทธิภาพมากขึ้น โดยปัญหานี้สามารถแบ่งออกเป็น 3 ปัญหาย่อย ได้แก่

  1. การซ่อมหลุมแรก

  2. การเดินไปยังหลุมที่สอง

  3. การซ่อมหลุมที่สอง

เพื่อแก้ปัญหาย่อยแต่ละข้อ เราจะสร้างฟังก์ชันเฉพาะสำหรับแต่ละปัญหา ซึ่งจะช่วยให้โค้ดมีความชัดเจนและเข้าใจง่ายขึ้น ฟังก์ชันเหล่านี้จะถูกเรียกใช้ตามลำดับที่กำหนด ทำให้สามารถแก้ปัญหาได้อย่างเป็นระบบและตรวจสอบได้ง่าย

def fill_hole1():
    pass

def move_to_next_hole()
    move()
    move()

def fill_hole2():
    pass

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

หากพิจารณาอย่างละเอียด จะพบว่าปัญหาย่อยที่ 1 และปัญหาย่อยที่ 3 มีรูปแบบการแก้ปัญหาที่เหมือนกัน โดยทั้งสองปัญหาเกี่ยวข้องกับการซ่อมแซมหลุมที่อยู่ข้าง ๆ จุดที่คาเรลยืนอยู่ ดังนั้น เราสามารถใช้ฟังก์ชันเดียวกันในการแก้ไขทั้งสองปัญหา (แต่ต้องให้คาเรลยืนอยู่ที่จุดฝั่งซ้ายของหลุมก่อนเรียกฟังก์ชัน) ซึ่งจะช่วยลดความซ้ำซ้อนของโค้ดและทำให้โปรแกรมมีประสิทธิภาพมากขึ้น

../../_images/deposed-repair-road.png

ภาพที่ 6 กรอบสีแดงแสดงให้ว่าสองปัญหาย่อยเป็นแบบแผนของปัญหาที่เหมือนกัน#

เพราะฉะนั้นโค้ดในการแก้ปัญหานี้จะมีการเรียกใช้ฟังก์ชันซ้ำ เพื่อแก้ปัญหาย่อยที่มีแบบแผนเดียวกัน ฟังก์ชัน fill_hole1 และ fill_hole2 จึงถูกแทนที่ด้วยการเรียกฟังก์ชัน fill_hole สองครั้งในตำแหน่งที่เหมาะสมดังนี้

def main():
    fill_hole()
    move_to_next_hole()
    fill_hole()
    
def fill_hole():
    pass

def move_to_next_hole()
    move()
    move()

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

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

  1. กระโดดลงไปในหลุม

  2. วางกระดิ่ง

  3. กระโดดออกมานอกหลุม

หลังจากที่เราสามารถเริ่มลงมือเขียนโค้ด (อิมพลิเมนต์) อัลกอริทึมในการ fill_hole ได้ดังนี้

def fill_hole():
    jump_in()
    put_beeper()
    jump_out()

def jump_in():
    move()
    turn_right()
    move()

def jump_out():
    turn_around()
    move()
    turn_right()
    move()

หากสังเกตโดยละเอียด โค้ดข้างบนนี้มีแบบแผนอีกหนึ่งอย่าง คือ jump_in() กับ jump_out() มีส่วนของโค้ดที่เหมือนกันอยู่ถึงสามบรรทัด นั่นก็คือ move() turn_right() move() ซึ่งคือแบบแผนการสั่งให้คาเริลเดินเป็นรูปตัวแอล ดังนั้นเราสามารถเขียนฟังก์ชัน move_L() มาใช้ซ้ำ ได้อีกดังนี้

def fill_hole():
    move_L()
    put_beeper()
    jump_out()

def move_L():
    move()
    turn_right()
    move()

def jump_out():
    turn_around()
    moveL()

ในการแก้ปัญหาคาเรลซ่อมถนนข้อนี้ เราได้ใช้หลักการคิดเชิงคำนวณ 3 ใน 4 ขั้นตอน ได้แก่ การแบ่งปัญหาออกเป็นส่วนย่อย การสังเกตแบบแผน และการออกแบบอัลกอริทึม อย่างไรก็ตาม ในโจทย์ข้อนี้ เราไม่ได้ใช้การสร้างนามธรรมซึ่งเป็นขั้นตอนสำคัญอีกขั้นตอนหนึ่งของกระบวนการคิดเชิงคำนวณ เมื่อเราแก้ปัญหาที่มีความซับซ้อนมากขึ้นในอนาคต จะเห็นได้ว่าการคิดแบบนามธรรมจะมีบทบาทสำคัญอย่างยิ่งในการช่วยจัดการกับความซับซ้อนของปัญหา

วงวน for#

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

วงวน for เป็นการควบคุมการทำงานของโปรแกรมให้วนซ้ำ ๆ ตามจำนวนครั้งที่กำหนดไว้ล่วงหน้า ตัวอย่างเช่น ถ้าเราต้องการให้คาเรลวางกระดิ่งลงบนถนนที่มีหลุมบ่อ 5 หลุม โดยที่คาเรลต้องวางกระดิ่งลงในทุก ๆ หลุม โดยเริ่มจากหลุมที่ 1 จนถึงหลุมที่ 5 โค้ดที่เขียนด้วยวงวน for จะมีลักษณะดังนี้

def main():
    move()
    for i in range(5):
        put_beeper()
        move()
    turn_around()

ซึ่งหมายความว่า เราจะทำการประมวลผลกลุ่มคำสั่ง (code block) ที่อยู่ภายใต้คำสั่ง for ทั้งหมด 5 ครั้ง (นักพัฒนาโปรแกรมมักเรียกกระบวนการนี้อย่างไม่เป็นทางการว่า ฟอร์ลูปไป 5 รอบ) หรือเท่ากับว่าทำการประมวลผลโค้ดดังนี้

def main():
    move()
    put_beeper()
    move()
    put_beeper()
    move()
    put_beeper()
    move()
    put_beeper()
    move()
    put_beeper()
    move()
    turn_around()

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

for i in range(จำนวนครั้งที่จะวนซ้ำ):
    คำสั่งที่ต้องการวนซ้ำ
    คำสั่งที่ต้องการวนซ้ำ

กลุ่มคำสั่ง (code block) ที่ต้องการให้ทำงานซ้ำภายใต้คำสั่ง for จำเป็นต้องมีการจัดย่อหน้า (indentation) ให้เห็นอย่างชัดเจนว่าคำสั่งใดบ้างที่อยู่ภายในโครงสร้างการวนซ้ำ for การย่อหน้าแบบนี้เหมือนกับการกำหนดโครงสร้างภายในฟังก์ชัน ซึ่งช่วยให้โปรแกรมสามารถแยกแยะได้ว่าคำสั่งใดที่เป็นส่วนหนึ่งของการวนซ้ำ และคำสั่งใดที่เป็นส่วนหนึ่งของโปรแกรมแต่อยู่นอกการวนซ้ำ

วงวน while#

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

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

../../_images/while-ex1.png

ภาพที่ 7 จุดเริ่มต้นของโจทย์#

../../_images/while-ex2.png

ภาพที่ 8 จุดมุ่งหมายของโจทย์#

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

def main():
    move()
    for i in range(6):
        put_beeper()
        move()

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

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

ในขั้นตอนต่อไป เราควรตั้งคำถามว่า โลกของคาเรลที่มีขนาด 8 ช่อง 3 ช่อง และ 100 ช่อง มีลักษณะร่วมกันอย่างไร การหาจุดร่วมนี้เป็นแก่นสำคัญในการแก้ปัญหา และการตั้งคำถามเช่นนี้ถือเป็นตัวอย่างของการคิดเชิงนามธรรม ซึ่งเป็นกระบวนการคิดที่ช่วยให้เราเข้าใจถึงรูปแบบพื้นฐานของปัญหา ในโจทย์นี้ พื้นฐานของปัญหา คือ ให้คาเรลก้าวเดินและวางกระดิ่งไปเรื่อย ๆ จนกระทั่งถึงกำแพง เพราะฉะนั้นขนาดของโลกจะมีกี่ช่องไม่ใช่ประเด็นสำคัญ ไม่ใช่แก่นของปัญหา คำถามสำคัญคือ เราจะทราบได้อย่างไรว่าคาเรลอยู่หน้ากำแพงหรือไม่ คาเรลมีฟังก์ชันที่ใช้ตรวจสอบสถานะด้านหน้าว่ามีพื้นที่ว่างหรือไม่ นั่นคือฟังก์ชัน front_is_clear() ซึ่งจะคืนค่า True หากด้านหน้าของคาเรลไม่มีสิ่งกีดขวาง และคืนค่า False เมื่อด้านหน้ามีกำแพงหรือสิ่งกีดขวาง

แทนที่จะใช้วงวน for ซึ่งจำเป็นต้องระบุจำนวนรอบล่วงหน้า เราสามารถใช้วงวน while ร่วมกับฟังก์ชัน front_is_clear() เพื่อให้คาเรลเดินและวางกระดิ่งไปเรื่อย ๆ จนกระทั่งฟังก์ชันนี้คืนค่า False ซึ่งหมายความว่าคาเรลได้เดินไปจนถึงกำแพง เราสามารถกำหนดเงื่อนไขการออกจากวงวน while ดังตัวอย่างโค้ดต่อไปนี้

def main():
    move()
    while front_is_clear():
        put_beeper()
        move()

เราเปลี่ยน for i in range(6): เป็น while front_is_clear(): ซึ่งหมายความว่า ก่อนที่โปรแกรมจะทำการประมวลผลในแต่ละรอบของการวนซ้ำ จะมีการตรวจสอบเงื่อนไขว่าด้านหน้าของคาเรลว่างอยู่หรือไม่ (front_is_clear()) หากด้านหน้ายังว่างอยู่ โปรแกรมจะทำการประมวลผลคำสั่งที่อยู่ภายในโครงสร้างการวนซ้ำ เมื่อประมวลผลคำสั่งในแต่ละรอบเสร็จสิ้น โปรแกรมจะตรวจสอบเงื่อนไขอีกครั้ง หากเงื่อนไขยังคงเป็นจริง (ด้านหน้ายังว่าง) โปรแกรมจะวนซ้ำและทำงานตามคำสั่งในโครงสร้างการวนซ้ำอีกหนึ่งครั้ง กระบวนการนี้จะดำเนินไปเรื่อย ๆ จนกว่าเงื่อนไขจะไม่เป็นจริงอีกต่อไป กล่าวคือ คาเรลจะหยุดเมื่อด้านหน้ามีกำแพง

../../_images/while-flow-chart.png

ภาพที่ 9 แผนภูมิการทำงาน (flowchart) ที่แสดงลำดับขั้นตอนการทำงานของโปรแกรม#

นอกเหนือจากคำสั่ง front_is_clear() คาเรลมีฟังก์ชันที่ใช้ในการตรวจสอบเงื่อนไขสถานะของคาเรลทั้งหมดดังนี้

คำสั่งตรวจสอบ

คำสั่งที่ตรงข้าม

ตรวจสอบอะไร

front_is_clear()

front_is_blocked()

มีกำแพงอยู่ตรงหน้าคาเรลหรือไม่

beepers_present()

no_beepers_present()

มี กระดิ่งตรงที่คาเรลยืนอยู่หรือไม่

left_is_clear()

left_is_blocked()

มีกำแพงอยู่ทางซ้ายของคาเรลหรือไม่

right_is_clear()

right_is_blocked()

มีกำแพงอยู่ทางขวาของคาเรลหรือไม่

beepers_in_bag()

no_beepers_in_bag()

คาเรลมีกระดิ่งในกระเป๋าหรือไม่

facing_north()

not_facing_north()

คาเรลหันหน้าทางเหนือหรือไม่

facing_south()

not_facing_south()

คาเรลหันหน้าทางใต้หรือไม่

facing_east()

not_facing_east()

คาเรลหันหน้าทางตะวันออกหรือไม่

facing_west()

not_facing_west()

คาเรลหันหน้าทางตะวันตกหรือไม่

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

โดยสรุป โครงสร้างในการใช้คำสั่ง while มีดังนี้

while เงื่อนไขบูลีน:
    คำสั่งที่ต้องการให้วนซ้ำ
    คำสั่งที่ต้องการให้วนซ้ำ

การตั้งเงื่อนไขด้วย if else elif และตัวปฏิบัติการบูลีน#

โครงสร้างควบคุมการทำงานอีกประเภทหนึ่งที่มีความสำคัญมากในการเขียนโปรแกรมคือ การเลือกว่าคำสั่งใดควรได้รับการประมวลผลภายใต้เงื่อนไขที่กำหนด โดยใช้คำสั่ง if ซึ่งช่วยให้โปรแกรมสามารถเลือกรันกลุ่มคำสั่งตามเงื่อนไขที่กำหนดได้

ตัวอย่างเช่น หากเราต้องการให้คาเรลเดินไปข้างหน้า ถ้าด้านหน้าไม่มีสิ่งกีดขวาง เราสามารถเขียนฟังก์ชัน safe_move() เพื่อควบคุมการเดินของคาเรลได้ดังนี้

def safe_move():
    if front_is_clear():
        move()

ในตัวอย่างนี้ เราใช้เงื่อนไขบูลีน front_is_clear() เพื่อกำหนดการทำงานของคาเรล โดยโปรแกรมจะตรวจสอบว่าเงื่อนไขนี้เป็นจริงหรือไม่ ถ้าเงื่อนไขเป็นจริงกล่าวคือหมายความว่าด้านหน้าของคาเรลว่าง คาเรลจะเดินไปข้างหน้า แต่ถ้าเงื่อนไขนี้ไม่เป็นจริง คาเรลจะไม่เดิน การทำงานของ if นี้จะช่วยให้โปรแกรมสามารถตัดสินใจได้ว่าคาเรลควรทำงานอย่างไรในกรณีใด

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

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

../../_images/safe-move-diagram.png

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

การแก้ปัญหานี้สามารถทำได้โดยใช้คำสั่ง if และ else เพื่อแยกแยะสถานการณ์ที่มีและไม่มีการวางกระดิ่งในช่องปัจจุบัน ซึ่งจะทำให้คาเรลสามารถเลือกได้ว่าจะวางกระดิ่งหรือเก็บกระดิ่งตามเงื่อนไขที่กำหนด

../../_images/invert-beeper-world.png

ภาพที่ 10 (ซ้าย) จุดเริ่มต้นของคาเรล (ขวา) ผลลัพธ์ที่คาดหวังหลังจากรันโปรแกรมแล้ว#

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

def invert():
   if beepers_present(): # ถ้ามีกระดิ่ง
      pick_beeper()      # ให้เก็บกระดิ่ง
   else:                 # มิฉะนั้นแล้ว
      put_beeper()       # ให้วางกระดิ่งลงในช่องนั้น     

ในฟังก์ชันนี้ เราใช้โครงสร้าง if-else ซึ่งทำงานโดยการตรวจสอบเงื่อนไข if ก่อน หากเงื่อนไขเป็นจริง (มีกระดิ่งอยู่ในช่อง) โปรแกรมจะทำการเก็บกระดิ่งด้วยคำสั่ง pick_beeper() แต่หากเงื่อนไขไม่เป็นจริง (ไม่มีกระดิ่ง) โปรแกรมจะดำเนินการตามคำสั่งในส่วน else ซึ่งจะทำการวางกระดิ่งในช่องนั้นด้วยคำสั่ง put_beeper()

สิ่งที่ต้องระวังคือ หากเราไม่ใช้คำสั่ง else คำสั่งที่ตามมาจะถูกรันในทุกกรณี ซึ่งอาจทำให้โปรแกรมทำงานไม่ตรงตามที่เราต้องการ ดังนั้นจึงต้องใช้ else ในกรณีที่ต้องการให้โปรแกรมทำงานอย่างชัดเจนในกรณีที่เงื่อนไขไม่เป็นจริง

ขั้นตอนการทำงานของฟังก์ชัน invert() สามารถแสดงเป็นแผนภูมิการทำงานได้ดังนี้

../../_images/invert-beeper-flow-chart.png

ถึงขั้นนี้แล้วเราสามารถแก้โจทย์นี้ต่อไปได้ หากเราเห็นว่าเราไม่ต้องสนใจว่าในช่องนั้นจะมีหรือไม่มีกระดิ่งอยู่แล้ว

def main():
    invert()

ตอนนี้เราแก้โจทย์นี้บนช่องแรกได้แล้ว จากนั้นหากเราเห็นว่าเราต้องรัน invert() ซ้ำ ๆ ในทุกช่องจนกว่าเราหน้าติดกำแพง ซึ่งแปลว่าเราต้องกำหนดเงื่อนไขที่ทำให้คาเรลรันโค้ดซ้ำ ๆ โดยใช้ while loop ดังนี้

def main():
    invert()
    while front_is_clear():
        move()
        invert()

ซึ่งทั้งหมดนี้อยากให้เห็นว่าเป็นการใช้การคิดเชิงคำนวณในการแก้ปัญหาโดยใช้การแบ่งปัญหาออกเป็นส่วนย่อย การสังเกตหาแพทเทิร์น (ต้องทำซ้ำเหมือนกันทุกช่อง) การคิดแบบนามธรรม (หาเงื่อนไขในการวน) และการออกแบบอัลกอริทึม (เริ่มเขียนโค้ดรวมกันทั้งหมด) และเราได้ใช้ while รวมกับ if ซึ่งเป็นโครงสร้างการควบคุมที่สำคัญในการเขียนโปรแกรม

ข้อแตกต่างระหว่าง if กับ while#

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

เราสามารถสรุปวิธีการใช้ if ได้ดังนี้

def main
    คำสั่ง 1
    if เงื่อนไข
        คำสั่ง 2
        คำสั่ง 3
    คำสั่ง 4

../../_images/if-vs-while-flow-chart.png

เราสามารถสรุปวิธีการใช้ while ได้ดังนี้ เพื่อเปรียบเทียบกันกับ if ซึ่งรันคำสั่งเพียงแค่ครั้งเดียว

def main
    คำสั่ง 1
    while เงื่อนไข
        คำสั่ง 2
        คำสั่ง 3
    คำสั่ง 4
../../_images/while-vs-if-flow-chart.png

บูลีน#

การกำหนดเงื่อนไขด้วยบูลีนเป็นเครื่องมือที่สำคัญมากในการเขียนโปรแกรม เราสามารถเขียนเงื่อนไขที่ซับซ้อนขึ้นไปอีกได้โดยใช้โอเปอร์เรเตอร์ทางตรรกศาสตร์ เช่น or และ and เพื่อให้เห็นการใช้งานที่ชัดขึ้น เราจะลองแก้โจทย์คาเรลอีกหนึ่งข้อ

สมมติว่าเราต้องการให้คาเรลเอากระดิ่งที่อยู่รอบรั้วออกไปให้หมด ดังตัวอย่าง

../../_images/boolean-remove-fence.png

ภาพที่ 11 ซ้าย: จุดเริ่มต้น ขวา: จุดมุ่งหมาย#

เราสามารถสังเกตหาแบบแผนได้ว่าการเก็บกระดิ่งแต่ละข้างมีวิธีการเหมือนกันทุกประการ เราสามารถแบ่งปัญหาออกเป็นส่วนย่อย ออกมาเป็นฟังก์ชันที่เก็บกระดิ่งของหนึ่งข้าง และใช้ฟังก์ชันนั้นซ้ำ ๆ

def main():
    for i in range(4):
        remove_one_side()

จากนั้นเราสามารถคิดแบบนามธรรมไปได้ว่า รั้วจะยาวแค่ไหนก็ไม่สำคัญ สำคัญว่าเราเห็นว่ามีเงื่อนไขในการตรวจได้ว่าคาเรลยังไม่สุดรั้ว ซึ่งการตรวจหารั้วคือ การตรวจสอบว่าฝั่งซ้ายมีกำแพงหรือไม่โดยคำสั่ง left_is_blocked() หรือ อีกเงื่อนไขหนึ่งคือ ตรงที่ยืนอยู่มีกระดิ่งหรือไม่ด้วยโดยคำสั่ง beepers_present() ซึ่งการเชื่อมบูลีนสองตัวด้วย หรือ สามารถใช้ตัวปฏิบัติการ or ลงไปตรง ๆ แบบภาษาอังกฤษได้เลยดังนี้

def remove_one_side():
    while left_is_blocked() or beepers_present():
        if beepers_present():
            pick_beeper()
        move()

เพื่อทดสอบว่าตรรกะตามฟังก์ชันนั้นถูกต้องหรือไม่ เราอาจจะเขียนตารางออกมาเพื่อตรวจสอบอีกครั้ง

../../_images/or-operation.png

ภาพที่ 12 ตัวอย่างการใช้ตัวปฏิบัติการ or#

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

นอกจากนั้นแล้วภาษาไพทอนยังมีตัวปฏิบัติการ and ซึ่งมีการตีค่าบูลีนเหมือนกันกับ และ ในตรรกศาสตร์ ดังนี้

../../_images/and-operation.png

ภาพที่ 13 ตัวอย่างการใช้ and operator#

กลับมาที่โจทย์ซึ่งเราสามารถแก้โจทย์สำหรับรั้วหนึ่งข้างได้แล้ว ที่เหลือที่ต้องทำคือเชื่อมให้คาเรลวนโค้ดแบบเดิมกับด้านที่เหลือ

def main():
    for i in range(4):
        remove_one_side()
        turn_left()
        move()

def remove_one_side():
    while left_is_blocked() or beepers_present():
        if beepers_present():
            pick_beeper()
        move()

สรุป#

การใช้โครงสร้างการควบคุม ในการเขียนโปรแกรมเป็นเครื่องมือที่สำคัญมากในการแก้ปัญหา โดยเฉพาะการใช้วงวน for และวงวน while ในการวนรันโค้ดซ้ำ ๆ และการใช้ if เพื่อเลือกว่าจะรันโค้ดใดในเงื่อนไขใด การใช้โครงสร้างการควบคุมชี้ให้เห็นว่าเราสามารถแก้ปัญหาที่ซับซ้อนได้โดย

  1. การแบ่งปัญหาออกเป็นส่วนย่อย

  2. การสังเกตหาแบบแผน

  3. การคิดแบบนามธรรม

  4. การออกแบบอัลกอริทึม ซึ่งเป็นทักษะที่สำคัญในการเขียนโปรแกรมที่ดี

อ้างอิง#

1

Richard E Pattis. Karel the robot: a gentle introduction to the art of programming. John Wiley & Sons, 1994.