2 คะแนน โดย GN⁺ 2024-01-15 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • การเขียนโปรแกรม K เน้นย้ายโค้ดที่ทดลองใน REPL ไปเป็นสคริปต์ พร้อมค่อย ๆ ย่อแพตเทิร์นเชิงคำสั่งขนาดใหญ่ให้เป็น แพตเทิร์นแบบประกาศเชิงอาร์เรย์ ที่เล็กลง
  • สคริปต์ ngn/k ทำงานทีละบรรทัดเหมือนอินพุตใน REPL และสามารถ โหลดเข้าสู่ REPL ข้อมูลและฟังก์ชันที่บันทึกไว้ได้ด้วย \\l file.k
  • หากย้ายโค้ดคูณเมทริกซ์แบบลูป 3 ชั้นจาก Wikipedia มาตรง ๆ จะมีตัวแปร global, ลูปซ้อน, และการแก้ไขค่าจำนวนมาก ซึ่งขัดกับจุดเด่นของ K
  • กระบวนการปรับปรุงค่อย ๆ ผ่าน +/ fold, ' each, /: eachright, \: eachleft, การตัด transpose ออก, และการแปลงเป็น tacit จนย่อจาก matmul: {x{+/x*y}\:y} เหลือ matmul: (+/*)\:
  • ตัวอย่างการคูณเมทริกซ์แสดงให้เห็นว่าความชำนาญใน K อยู่ที่การทำ กระบวนการย่อโค้ด ซ้ำ ๆ เพื่อเปลี่ยนขั้นตอนที่ซับซ้อนให้เป็นนิพจน์แบบอาร์เรย์ที่อ่านง่ายขึ้น

เวิร์กโฟลว์การพัฒนา K ที่มี REPL เป็นศูนย์กลาง

  • ซอร์สโค้ดทั้งหมดดูได้ที่ matmul.k บน GitHub
  • การเขียนโปรแกรม K ส่วนใหญ่เกิดขึ้นใน REPL ซึ่งเหมาะกับการทดลองและปรับปรุงอย่างรวดเร็วบนโค้ดเดิม
  • การใช้ ngn/k ร่วมกับ rlfe รองรับประวัติด้วยปุ่มลูกศรขึ้น/ลง จึงเพียงพอสำหรับการพัฒนาโปรแกรม K ที่ใหญ่ขึ้น
  • แนวทางที่เป็นธรรมชาติคือทดสอบฟังก์ชันใน REPL ก่อน แล้วค่อยย้ายไปเป็นโค้ดจริง
  • prettyprinting ของ ngn/k จะคืนข้อมูล K ที่ถูกต้องเสมอ จึงสามารถคำนวณค่าบางส่วนล่วงหน้าเพื่อเพิ่มความเร็วให้โปรแกรมได้

โมเดลการรันสคริปต์ K

  • สคริปต์ K จะทำงานเหมือนกับการพิมพ์คำสั่งใน REPL
    • แต่ละบรรทัดทำงานตามลำดับ
    • ถ้าบรรทัดไม่ลงท้ายด้วยเซมิโคลอน ระบบจะพิมพ์ค่าที่คืนกลับ
  • สคริปต์รองรับ นิยามหลายบรรทัด เพื่อให้อ่านง่ายขึ้น
  • หากต้องการใช้ข้อมูลและฟังก์ชันที่บันทึกไว้จากใน REPL ให้รัน \\l file.k
    • ไฟล์จะถูกประมวลผล
    • ข้อมูลในไฟล์จะถูกโหลด
    • หากโหลดไฟล์เดิมหลายครั้ง ข้อมูลก่อนหน้าจะถูกเขียนทับ
  • สามารถดูคำสั่งเพิ่มเติมได้จากคู่มือช่วยเหลือใน REPL ที่เข้าถึงด้วย \\

วิธีลดรูปแพตเทิร์นในภาษาอาร์เรย์

  • K และการเขียนโปรแกรมแบบอาร์เรย์คือกระบวนการของการ ทำให้แพตเทิร์นเรียบง่ายลง อย่างต่อเนื่อง
  • แม้แพตเทิร์นจะใหญ่และจัดการยาก ก็มักมีอย่างน้อยหนึ่งวิธีที่จะย่อให้เล็กลง เป็นแบบประกาศมากขึ้น และอ่านง่ายขึ้น
  • ดูการอภิปรายที่เกี่ยวข้องได้ใน Patterns and Anti-patterns in APL: Escaping the Beginner's Plateau - Aaron Hsu - Dyalog '17
  • จุดเริ่มต้นที่พบบ่อยคือการพยายามแปลอัลกอริทึมที่คุ้นเคยจาก GeeksforGeeks หรือ Wikipedia มาเป็น K
  • ตัวอย่างนี้ใช้ การคูณเมทริกซ์

เมื่อย้ายการคูณเมทริกซ์แบบ imperative มาตรง ๆ

  • Matrix multiplication algorithm ของ Wikipedia เติมค่าให้เมทริกซ์ C ด้วยลูป 3 ชั้น i, j, k และการสะสมค่า sum
  • หากแปลมาตรง ๆ เป็น K จะต้องกำหนดค่าระดับ global จำนวนมาก เช่น A, B, n, m, p, C, i, j, k, sum
  • โค้ดลักษณะนี้ใช้ K ราวกับเป็นภาษาเชิงคำสั่ง ซึ่งไม่ค่อยสอดคล้องกับการออกแบบของ K
  • ปัญหาสามารถสรุปได้ 3 ข้อ
    • มี การกำหนดค่าระดับ global มากเกินไป
    • ยังเหลือลูปซ้อนหลายชั้น
    • มีการแก้ไขค่าเกิดขึ้นบ่อย

เริ่มย่อจากลูปชั้นในสุด

  • ลูปชั้นในสุดเริ่มจากตั้ง sum เป็น 0 แล้ววน k เพื่อสะสม A[i;k]*B[k;j]
  • การปรับปรุงแรกคือใช้ / ซึ่งเป็น fold เพื่อเปลี่ยนการบวกสะสมให้เป็น +/
    • ตัวแปร global sum หายไป
    • โค้ดถูกจัดให้อยู่ในรูป C[i;j]::+/...
  • จากนั้นใช้ข้อเท็จจริงที่ว่า ' each คืนค่าเป็นอาร์เรย์ ทำให้สามารถใช้ค่าที่ลูปซ้อนคืนกลับมาได้โดยตรงโดยไม่ต้องแก้ไข C
  • หลังขั้นตอนนี้จะเหลือเพียงลูป 3 ชั้นที่ไม่มีการแก้ไขค่า และตัวแปรหลักคือ i, j, k

กระบวนการลบ k, j, i

  • บทบาทของตัวแปรทั้งสามมีดังนี้
    • i ใช้ทำดัชนีแต่ละแถวของ A
    • j ใช้ทำดัชนีแต่ละคอลัมน์ของ B
    • k ใช้ทำดัชนีแต่ละคอลัมน์ของ A และแต่ละแถวของ B
  • k ทำหน้าที่จับคู่แต่ละแถวของ A กับแต่ละคอลัมน์ของ B เพื่อคูณกัน ดังนั้นจึงสามารถตัดดัชนีกลางออกและจับคู่กันโดยตรงได้
    • ขั้นตอนนี้ทำให้ลูปหายไปหนึ่งชั้นและไม่ต้องใช้ m อีกต่อไป
  • หากต้องการลบ j ต้องดึงแต่ละคอลัมน์ของ B ออกมาแล้วจับคู่กับ A[i]
    • ทำได้ด้วยการ transpose B แล้วใช้ eachright /: เพื่อจับคู่แต่ละองค์ประกอบ
  • i ก็ลบออกได้ในลักษณะเดียวกัน
    • ใช้ eachleft \: เพื่อจับคู่แต่ละแถวของ A กับแต่ละคอลัมน์ของ B
  • เมื่อทำถึงจุดนี้จะได้รูปแบบต่อไปนี้โดยไม่มีตัวแปร global
matmul: {x{+/x*y}\:y}

ตัด transpose ออกและไปสู่รูป tacit สุดท้าย

  • การ transpose ด้วย + มีต้นทุนสูง จึงควรตัดออก
  • วิธีเดิมคือ วิธีตรงไปตรงมา ที่นำแต่ละแถวของ x ไปคูณกับแต่ละคอลัมน์ของ y
  • แต่ถ้านำแต่ละแถวของ B ไปจับกับ A ทั้งหมดแทน ก็จะทำงานเดียวกันได้โดยปริยาย
matmul: {x{+/x*y}\:y}
  • ฟังก์ชันนี้สามารถแปลงเป็นรูป tacit ได้โดยใช้กฎจาก Chapter 3
  • ผลลัพธ์สุดท้ายเป็นดังนี้
matmul: (+/*)\:

สร้างสัญชาตญาณของภาษาอาร์เรย์ผ่านการฝึกฝน

  • matmul: (+/*)\: ถูกสรุปออกมาเป็นฟังก์ชันคูณเมทริกซ์ในแบบฉบับของ K
  • กระบวนการย่ออาจดูเหมือนมีหลายขั้นตอนในช่วงแรก
  • เมื่อฝึก K มากขึ้น การย่อโค้ด จะกลายเป็นงานที่ง่ายและเป็นธรรมชาติมากขึ้น
  • การคูณเมทริกซ์เป็นกระบวนการง่าย ๆ ที่เข้ากันได้ดีกับการรองรับอาร์เรย์ของ K
  • ในบทถัดไปจะกล่าวถึงอัลกอริทึมที่ไม่ค่อยเข้ากับ K และวิธีจัดการ

1 ความคิดเห็น

 
GN⁺ 2024-01-15
ความคิดเห็นจาก Hacker News
  • สิ่งที่แสดงให้เห็นศักยภาพของภาษาแบบอาร์เรย์ได้อย่างน่าโน้มน้าวที่สุดจริง ๆ คือวิดีโอที่ Aaron Hsu อธิบายการพัฒนา Co-dfns ซึ่งเป็น คอมไพเลอร์ APL แบบขนาน: https://www.youtube.com/watch?v=gcUWTa16Jc0&t=860s
    เขายังเขียนเรื่อง ความหนาแน่นของความหมาย หลายครั้งบน HN ในนาม arcfide โดยอธิบายว่าโค้ด APL ถูกออกแบบมาให้สามารถมองเห็นวิธีการทำงาน บริบทโดยรอบ และการพึ่งพาต่าง ๆ ได้ภายในหน้าจอเดียวโดยแทบไม่ต้องเลื่อนสายตาไปไหน: https://news.ycombinator.com/item?id=13571159
    มุมมองนี้คือ ถ้าทำให้กระชับได้จนชื่ออัลกอริทึมมีความยาวใกล้เคียงกับการเขียนตัวอัลกอริทึมนั้นออกมาตรง ๆ เราก็จะอ่านโค้ดเป็นหน่วยสำนวนคล้ายการอ่านวลีภาษาอังกฤษ และแทนที่จะสร้าง abstraction เพื่อการนำกลับมาใช้ใหม่ การแก้ทุกจุดใช้งานที่มองเห็นอยู่บนหน้าจอโดยตรงอาจเร็วกว่า

    • ก็สงสัยว่า LLM ที่มี หน้าต่างบริบทจำกัด อาจจัดการ APL ได้ดีกว่าภาษาอื่นหรือเปล่า
    • มองว่าการต้องเขียนคำอธิบายยาวแบบนั้นเป็นเพราะโค้ดมันดูไม่สวย ถ้าเลือกสัญลักษณ์ที่เอามาวางติดกันแล้วดูน่าเกลียดน้อยกว่านี้ ก็คงไม่ต้องใช้เวลา 18 ชั่วโมง เพื่อโน้มน้าวคนว่าภาษานี้ไม่ได้แย่
  • ถ้ายังไม่คุ้นกับ array programming ขอแนะนำ The Array Cast เป็นสื่อเริ่มต้น: https://www.arraycast.com/episodes/
    RSS คือ https://www.arraycast.com/episodes?format=rss

    • ฟัง The Array Cast ช่วงต้น ๆ ไปประมาณ 5 ตอนเพราะอยากให้ตัวเองถูกโน้มน้าว แต่สุดท้ายก็ยังไม่คล้อยตาม ผู้ดำเนินรายการบอกว่า ไวยากรณ์แบบสั้น และสัญลักษณ์ที่ไม่ใช่ ASCII ของภาษาอาร์เรย์จะโอเคเองเมื่อคุ้นเคย และข้อดีต่าง ๆ ก็คุ้มที่จะยอมรับมันไว้ แต่ข้อดีส่วนใหญ่พวกนั้นเป็นสิ่งที่คุ้นเคยกันอยู่แล้วจาก higher-order functions ในภาษากระแสหลักสมัยนี้
      map/filter/reduce ก็มีแทบทุกที่อยู่แล้ว และเหมือนพวกเขามองข้ามข้อดีที่ว่าเราใช้มันได้โดยไม่ต้องไปเรียนระบบสัญลักษณ์ใหม่ที่คล้ายอักษรภาพ
    • โพสต์นี้ทำให้ได้รู้จัก BQN แต่ยังไม่แน่ใจว่าจะใช้ในสภาพแวดล้อม production จริงไหม ถึงจะชอบมัน แต่ถ้าไม่ใช่ระดับ R, NumPy หรือ Julia ภาษาอาร์เรย์ส่วนใหญ่ก็ยังดูแปลกอยู่ดี และถ้าลงลึกกับ APL, J, BQN มากไป สุดท้ายอาจยิ่งทำให้คนที่จะมาช่วยต่อภายหลังถอยห่างออกไปเอง
  • เคยได้เจอ APL/APL2 ตอนยุค 70s ที่บน paper terminal ยังใช้การพิมพ์ทับกันจริง ๆ แล้วก็ติดใจทันที แต่พอภายหลังได้รู้จักการเขียนโปรแกรมเชิงฟังก์ชันผ่าน ML และ Haskell ก็พบว่าสิ่งที่ชอบจริง ๆ ใน APL ไม่ใช่เรื่องอาร์เรย์ แต่คือความสามารถด้าน การประกอบฟังก์ชัน
    Haskell นั้นบริสุทธิ์อย่างสมบูรณ์และมี type ครอบคลุมทั่วทั้งภาษา จึงดีกว่ามากในแง่นี้ และทั้งสนุกและทรงพลังกว่า APL เคยทำโปรเจ็กต์ขนาดเล็กถึงกลางไว้เยอะ รวมถึงทำต้นแบบที่แสดงว่า parser ของ LLVM Flang สามารถเขียนด้วย parser combinator ได้ และทุกปีก็แก้ Advent of Code ทั้งหมดด้วยโค้ดรวมกันเพียงไม่กี่ร้อยบรรทัด ถ้าชอบ APL ก็น่าลอง Haskell เช่นกัน
    ตอนนี้มองว่าด้านที่ APL เป็น “สัญกรณ์ในฐานะเครื่องมือช่วยคิด” ดูเหมือนเป็นคำอธิบายเพื่อทำให้ความกระชับเกินพอดูสมเหตุสมผล มันเหมาะกับการแสดงพลังของการประกอบ แต่ก็อาจบั่นทอนความชัดเจนได้

    • ในหัวข้อนี้คงต้องพูดซ้ำอีกครั้งว่า หลังจากเริ่มใช้ point-free Haskell ได้คล่องพอสมควร ก็แทบไม่ได้แตะ J กับ K อีกเลย ถ้าผสมพวก functor เข้าไปด้วย มันยิ่งทรงพลังกว่า train ของกริยาเสียอีก, <=< ก็มีอยู่แล้ว และถ้าใช้สิ่งที่เทียบได้กับ fmap มันทำงานได้ดีมาก
      |||, +++, &&&, *** ก็ดีเหมือนกัน และยังสร้างโอเปอเรเตอร์ UTF-8 ของตัวเองให้สั้นและสวยขึ้นได้ด้วย เพียงแต่น่าเสียดายที่โค้ด Haskell จริงจังที่ใช้ในงานจริงหรือเผยแพร่สู่สาธารณะมักไม่ค่อยเป็นมิตรกับพื้นที่แนวตั้งของหน้าจอแบบนี้
    • ถ้าได้เห็นลิงก์ซอร์สของ Advent of Code ก็น่าจะดี
  • อยากรู้ว่าในภาษาแบบอาร์เรย์ โดยทั่วไปจัดการปัญหาแนว “หาค่าทั้งหมดที่น้อยกว่า N และทำให้เพรดิเคต P เป็นจริง” อย่างไร เช่น การหา prime ที่น้อยกว่า 1000 หรือหา Pythagorean triple ที่มี z น้อยกว่า 1,000,000
    ถ้าเป็นภาษาเชิงคำสั่งก็มักจะตรวจเพรดิเคตในลูป ส่วนภาษาเชิงฟังก์ชันก็อาจใช้ recursion หรือลิสต์แบบ lazy กับ map/filter แต่สำหรับภาษาแบบอาร์เรย์ ผมเข้าใจว่าปกติจะสร้างอาร์เรย์ 1..N แล้วนำเพรดิเคตไปใช้เพื่อสร้าง mask array จากนั้นใช้ mask นั้นกรองอาร์เรย์ต้นฉบับ
    ถ้า N ใหญ่มากอย่าง 1 พันล้าน และเพรดิเคตแทบไม่เคยเป็นจริงเลย การสร้างทั้ง 1..N และ mask ซึ่งเป็นอาร์เรย์ชั่วคราวขนาดมหึมาสองชุด ดูเป็นการสิ้นเปลืองหน่วยความจำและทรัพยากรมาก อยากรู้ว่าภาษาแบบอาร์เรย์ช้าลงเพราะต้องสร้างอาร์เรย์ชั่วคราวแบบนี้ตลอดหรือไม่ และการ implement มีการ optimize ด้วยวิธีอย่าง lazy evaluation หรือเปล่า

    • ใช่ มันเปลืองหน่วยความจำมากพอสมควร แต่หน่วยความจำก็ราคาถูก และถ้าจำเป็นก็สามารถแบ่งการคำนวณเป็น บล็อก ได้ ในทางปฏิบัติไม่ค่อยถึงขั้นหน่วยความจำหมดจริง ๆ แต่การทำ blocking ก็มีประโยชน์เพื่อให้ข้อมูลอยู่ในชั้นแคชที่ต่ำกว่า
      ในทางกลับกัน ภาษาแบบสเกลาร์มีค่าเริ่มต้นคือประมวลผลทีละค่าหนึ่งค่า จึงสูญเสียศักยภาพของความขนานที่ภาษาแบบอาร์เรย์ใช้กับอัลกอริทึม SIMD ได้ เรื่องนี้ก็แค่ดูไม่ใช่ปัญหาใหญ่เพราะเราคุ้นกับสภาพปัจจุบันเท่านั้น และทางแก้ก็คือการทำ blocking เช่นกัน
      ในทางปฏิบัติ ภาษาแบบอาร์เรย์ดีหรือไม่ก็ขึ้นกับปัญหาเป็นหลัก สำหรับงานใช้งานจริงส่วนใหญ่ ประสิทธิภาพไม่สำคัญเลย และชื่อเสียงของ k ก็ดูจะมาจากการที่ kdb เร็วในฐานะฐานข้อมูล มากกว่าที่ตัวภาษา k จะเป็นภาษาที่รันเร็วในตัวเอง ถึงอย่างนั้น การโฟกัสที่อัลกอริทึมแบบอาร์เรย์ที่สวยงามแทนการจูนระดับรายละเอียดเฉพาะเครื่อง ก็อาจทำให้เร็วขึ้นอย่างน่าทึ่งได้: https://mlochbaum.github.io/BQN/implementation/versusc.html
    • มีทางอ้อมอยู่หลายแบบ lazy evaluation ก็เป็นหนึ่งในนั้น และ Kap ก็ใช้วิธีนี้: https://aplwiki.com/wiki/KAP
      อีกวิธีที่ตรงไปตรงมาคือทำ loop fusion กับทั้งบอดีเพื่อไม่ให้เกิดอาร์เรย์ชั่วคราวขึ้นมาเลย ทางเลือกที่ง่ายกว่านั้นคือแบ่งอาร์เรย์อินพุต/เอาต์พุตเป็นชังก์ขนาดระดับหลายสิบ KB เพื่อจำกัดการใช้หน่วยความจำชั่วคราวที่ไม่จำเป็น เท่าที่ผมรู้ยังไม่มีภาษาแบบอาร์เรย์ใดทำสิ่งนี้อัตโนมัติ แต่สักวันหนึ่งอยากลองทำใน CBQN ผู้ใช้ทำเองแบบ manual ก็ได้ และถ้าจะรีดประสิทธิภาพสูงสุดก็มักต้องทำบ่อยทีเดียว
    • สัญชาตญาณของคุณโดยรวมก็ถูกต้อง แต่ในทางปฏิบัติเป็นปัญหาที่พบไม่บ่อย ในสาย k เช่น ngn/k มีโครงสร้างแบบ lazy ที่จัดการ iota อย่าง !10000000 เป็นเพียงช่วงค่า ไม่ได้สร้างอาร์เรย์จำนวนเต็มสิบล้านตัวจริง ๆ ตั้งแต่แรก
      แน่นอนว่าเมื่อใช้ operator บางตัว สุดท้ายอาร์เรย์แบบนั้นก็อาจถูกสร้างขึ้นมาจริงได้ นอกจากนี้ยังมี optimization แบบเปลี่ยนแพตเทิร์นอย่าง +|x ซึ่งคือกลับลำดับ x แล้วหยิบสมาชิกตัวแรก ให้กลายเป็นเพียงการหยิบสมาชิกตัวสุดท้ายแทน
    • ดูเหมือนคุณกำลังคิดว่าการสร้างอาร์เรย์เกิดขึ้นแบบตรงตัวทุกครั้ง ไม่มีเหตุผลว่าทำไมภาษาแบบอาร์เรย์จะประมวลผลภายในแบบเป็นชังก์ไม่ได้ ต่อให้คุณขออาร์เรย์จำนวนเต็ม 10 พันล้านตัว มันก็อาจไม่จำเป็นต้องสร้างแบบทื่อ ๆ ตามนั้น
    • ภาษาแบบอาร์เรย์จำนวนมากมีปัญหานี้จริง ๆ พูดให้แม่นยำคือ วิธีที่เรียบง่ายและตรงไปตรงมามักคำนวณมากเกินกว่าที่จำเป็นอย่างมาก
      แน่นอนว่าสามารถเขียนเลี่ยงได้ แต่ทางออกแบบนั้นอาจยาวขึ้นและสวยน้อยลง ภาษาถิ่น APL ที่ผมกำลังทำอยู่คือ Kap ซึ่งจะเลื่อนการคำนวณออกไปจนกว่าจะต้องใช้ผลลัพธ์ ทำให้เขียนโค้ดในแบบที่เป็นธรรมชาติได้โดยไม่ต้องคำนวณผลที่ท้ายที่สุดจะถูกทิ้งในหลายกรณี
  • สิ่งที่ตระหนักได้มากที่สุดจากการใช้ภาษาแบบอาร์เรย์ โดยเฉพาะ k มีดังนี้ กริยาคืออัลกอริทึม และในภาษาเชิงคำสั่ง/เชิงวัตถุ เรามักต้องลงมือ implement อัลกอริทึมร่วมอย่าง find, sort, group ด้วยตัวเอง
    การต่อเนื่องกันของกริยาและ副詞เป็นรูปแบบการประกอบที่ตรงไปตรงมาที่สุดเท่าที่ผมเคยใช้มา และการ compose ก็ทำได้ง่ายและเป็นธรรมชาติ โปรแกรมจึงดูเหมือนไม่ใช่ชุดของ statement กับ expression แต่เป็นการประกอบของอัลกอริทึม
    การจัดการแนวคิดเรื่องโดเมนและโคโดเมนอย่างสม่ำเสมอในอาร์เรย์ แมป และฟังก์ชัน ทำให้การเลือกออกแบบง่ายขึ้น และเมื่อมีการประเมินจากขวาไปซ้าย เวลาจะอ่านโค้ดสายตาก็ไม่ต้องกระโดดไปมา
    แทนที่จะดึงข้อมูลเข้าหาโค้ด ยังสามารถและมักนิยม ส่งโค้ดไปหาข้อมูล ได้ โปรเจกต์ k ขนาดใหญ่ส่วนมากเมื่อตัดคอมเมนต์ออกแล้ว ยังอยู่ภายในขนาด network MTU คือ 1540 ไบต์ได้สบาย โบนัสอีกอย่างของ k คือ view สามารถ implement ความสัมพันธ์เชิงฟังก์ชันได้โดยตรง และการโหลดโค้ดร้อนผ่านอินเทอร์พรีเตอร์ก็ทำให้แอปพลิเคชันที่รัน “ตลอดกาล” เป็นไปได้

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

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

  • ตัวอย่างนี้อิง K แต่ยังมีภาษาแบบอาร์เรย์อีกตัวคือ J: http://jsoftware.com
    ใน J คุณเขียน dot =: +/ . *, P =: 2 3 4, Q =: 1 0 2, P dot Q แล้วจะได้ inner product ของ P และ Q เป็น 10

    • ภาษาแบบอาร์เรย์ต้นฉบับคือ APL และ inner product เขียนได้เป็น dot←+.× แต่ถ้ารูปแบบที่เขียนขยายออกมาสั้นพอ ๆ กับชื่อที่ตั้งแบบกระชับ ก็ดูไม่มีเหตุผลมากนักที่จะต้องตั้งชื่อให้มัน แถมอาจยังต้องใส่ช่องว่างรอบชื่ออีกด้วย
    • ผมยังไม่ค่อยเห็นว่ามันมีข้อได้เปรียบเหนือ Haskell อย่างไร dot = (sum.) . zipWith (*), p = [2, 3, 4], q = [1, 0, 2], p `dot` q ก็เขียนได้เหมือนกัน
      ในสายตาผม ความต่างมีแค่การใช้ชื่อ sum กับ zipWith และการยกโครงสร้างหรือแปลงโครงสร้างไม่ได้เกิดขึ้นแบบ “เวทมนตร์” เท่านั้นเอง
    • ใน KlongPy inner product เขียนเป็น dot::{+/x*y} ใช้ในรูป P::[2 3 4], Q::[1 0 2], dot(P;Q)
  • ดูจากตัวอย่างแล้วไม่ค่อยเข้าใจว่ามันมีความหมายหรือประโยชน์อะไร แบบนี้ประสิทธิภาพดีกว่าจริงหรือ?
    ไวยากรณ์การคูณเมทริกซ์สั้นกว่าก็จริง แต่ดูเหมือนว่านั่นเป็นเพราะต้องมีบริบทที่ฝังมากับภาษา K จำนวนมากอยู่ในหัวว่ามันทำงานอย่างไร

    • ความที่มัน กระชับกว่า นั้นมีคุณค่าในตัวเอง โดยเฉพาะถ้าคิดว่าคณิตศาสตร์คือกระบวนการที่ค่อย ๆ บีบอัดแนวคิดจำนวนมากให้กลายเป็นนิยามระดับสูงขึ้นเรื่อย ๆ เมื่อแนวคิดระดับสูงกลายเป็นองค์ประกอบพื้นฐาน เราก็จะคิดได้เร็วขึ้นและสร้างสิ่งที่ซับซ้อนขึ้นได้
    • มันอาจมีประสิทธิภาพดีกว่าก็ได้ คอมพิวเตอร์ทำงานแบบไล่อ่านอาร์เรย์ได้เร็วมาก โดยเฉพาะถ้าใช้ SIMD ได้ แต่ก็ไม่ใช่ทั้งหมดของเรื่อง
      มันคุ้มที่จะลองเล่นกับภาษาอาร์เรย์จนกว่าจะเข้าใจพาราไดม์นี้ เพราะบ่อยครั้งโค้ดเชิงคำสั่งจะถ่ายทอดออกมาในสไตล์อาร์เรย์ได้ดีกว่า และฟังก์ชันที่ยาวและจุกจิกก็มักถูกทำให้ง่ายลงได้มากด้วยการใช้งานอาร์เรย์ล้วน ๆ หรือใช้ร่วมกับสไตล์อื่น
    • ความเยิ่นเย้อก็มีต้นทุน และถ้าเชื่อว่าสิทธิ์ที่จะเยิ่นเย้อควรสงวนไว้ให้ฟังก์ชันที่ซับซ้อนจริง ๆ ก็จะเห็นความหมายของเรื่องนี้ได้ง่าย
      ลองเทียบ (+) <$> Just 1 <*> Just 2 กับ do x <- Just 1; y <- Just 2; Just (x + y) ใน Haskell; สำหรับความซับซ้อนระดับนี้ ฉันจะเลือกอันแรกเสมอ
      อันหลังใช้พื้นที่มากกว่าเลยทำให้รู้สึกเหมือนมีเรื่องที่ซับซ้อนกว่ากำลังเกิดขึ้น
      ถ้างานซับซ้อนกว่านี้ ฉันก็ยังอยากแยกมันออกเป็นฟังก์ชันย่อย ๆ เพื่อให้รูปแบบแรกที่ดัดแปลงแล้วยังสมเหตุสมผล มากกว่าจะไปใช้รูปแบบที่สอง นี่คือการแลกเปลี่ยนจากการทำให้ “มือใหม่บางคนอ่านได้เร็ว” ไปเป็นการทำให้ “คนที่เกินระดับมือใหม่อ่านได้”
      ถ้าตั้งเป้าเพิ่มประสิทธิภาพให้กับ “มือใหม่บางคนอ่านได้” ฉันคิดว่าจะเจอกับผลตอบแทนที่ลดลงอย่างมาก จึงเลือกตั้งเป้าให้ “คนที่เกินระดับมือใหม่” หรือในบางกรณีก็ “ระดับกลางขึ้นไป” อ่านได้แทน
  • ไม่ว่าภาษาไหนก็มีทั้งเหตุผลที่จะใช้และเหตุผลที่จะไม่ใช้ แต่ประเด็นสำคัญไม่ใช่แค่การเขียนให้สั้น ความชัดเจนเชิงเปรียบเทียบ หรือความสามารถในการคอมไพล์เป็นโค้ดที่เร็ว สิ่งสำคัญคือโปรแกรมเมอร์ที่มาทีหลังจะสามารถ แก้ไขและบำรุงรักษา โค้ดนั้นเพื่อใช้งานจริงได้หรือไม่
    บ่อยเกินไปที่โปรแกรมเมอร์อยากโชว์ฝีมือ leet ของตัวเอง โดยไม่คำนึงถึงคนที่น่าสงสารซึ่งต้องมารับช่วงโค้ดนั้นต่อทีหลัง ในโลกความเป็นจริง โค้ดแบบ leet จำนวนมากสุดท้ายต้องถูกทิ้งหรือเขียนใหม่ทั้งหมด เพื่อให้ได้สิ่งที่พอจะรองรับในระยะยาวได้
    ฉันใช้เวลานานกว่าจะเข้าใจเรื่องนี้ และหลังจากนั้นก็พยายามเขียนโค้ดที่สะอาด เรียบง่าย และเข้าใจได้ เพื่อให้คนอื่นดูแลต่อได้ มีหลายครั้งเกินไปที่โค้ดซึ่งเดิมตั้งใจเขียนแล้วทิ้งกลับแข็งตัวกลายเป็นโครงสร้างพื้นฐานหลักขององค์กร และกลายเป็นสิ่งที่คนรุ่นถัดไปไม่อาจทำความเข้าใจได้