1 คะแนน โดย GN⁺ 3 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ใน guard ของ Elixir แค่สลับเงื่อนไข or ก็อาจทำให้ผลลัพธ์ของโค้ดที่ดูเหมือนเป็นนิพจน์ตรรกะเดียวกันเปลี่ยนไปได้
  • ลำดับ is_integer(x) or is_map_key(x,:foo) จะเกิด การประเมินแบบลัดวงจร ก่อนเมื่ออินพุตเป็นจำนวนเต็ม ทำให้ข้ามการตรวจสอบที่เสี่ยงไป
  • ในทางกลับกัน is_map_key(x,:foo) or is_integer(x) เมื่ออินพุตเป็นจำนวนเต็ม เงื่อนไขแรกไม่ได้เป็น false แต่ล้มเหลว จึงไปไม่ถึงเงื่อนไขถัดไป
  • เพราะความแตกต่างนี้ Foo.a(%{foo: 21}), Foo.a(37), Foo.b(%{foo: 21}) จึงเป็น true แต่ Foo.b(37) เป็น false
  • แม้จะดูเหมือน กฎการสลับที่ของตัวดำเนินการบูลีน ถูกทำลาย แต่ or ที่มีการประเมินแบบลัดวงจรนั้นขึ้นกับลำดับเงื่อนไขอยู่แล้ว และตาม Elixir 1.20.1 กับ OTP 29 ยังไม่มีคำเตือน

ตัวอย่างที่ลำดับเงื่อนไขเปลี่ยนผลลัพธ์

  • โมดูลตัวอย่าง Foo นิยามฟังก์ชันสองตัวคือ a/1 และ b/1
    • a/1: ตรวจ guard ตามลำดับ is_integer(x) or is_map_key(x, :foo)
    • b/1: ตรวจ guard ตามลำดับ is_map_key(x, :foo) or is_integer(x)
    • หาก guard ตรงเงื่อนไขจะคืนค่า true มิฉะนั้น clause ถัดไปจะคืนค่า false
  • a/1: กรณีที่เงื่อนไขที่ปลอดภัยมาก่อน

    • Foo.a(%{foo: 21}) จะเป็น true
      • is_integer(x) เป็น false
      • is_map_key(x, :foo) เป็น true
      • ผลลัพธ์ของ or เป็น true จึง match กับ clause แรก
    • Foo.a(37) ก็เป็น true เช่นกัน
      • is_integer(x) เป็น true
      • เนื่องจาก or ถูก ประเมินแบบลัดวงจร จึงไม่เรียกใช้ is_map_key(x, :foo)
  • b/1: กรณีที่เงื่อนไขที่อาจล้มเหลวมาก่อน

    • Foo.b(%{foo: 21}) จะเป็น true
      • is_map_key(x, :foo) เป็น true
      • is_integer(x) ที่อยู่ด้านหลังจะไม่ถูกเรียกใช้
    • Foo.b(37) จะเป็น false
      • เงื่อนไขแรก is_map_key(x, :foo) ล้มเหลว แทนที่จะคืนค่า false
      • ความล้มเหลวของฟังก์ชัน guard หนึ่งตัวไม่ได้ถูกแปลงเป็น false แต่ทำให้นิพจน์ guard ทั้งหมดล้มเหลว
      • is_integer(x) ที่อยู่ด้านหลังจึงไม่ถูกเรียกใช้ และ clause แรกก็ไม่ match

การประเมินแบบลัดวงจรและการไม่มีคำเตือน

  • สำหรับนักพัฒนา Elixir หลายคน พฤติกรรมนี้อาจดูเหมือน กฎการสลับที่ ของตัวดำเนินการบูลีนถูกทำลาย
  • แต่ or มีการประเมินแบบลัดวงจร ดังนั้นจึงไม่อาจถือได้ว่าการสลับตำแหน่งของสองเงื่อนไขจะให้ผลลัพธ์เหมือนกันเสมอ
  • สภาพแวดล้อมอ้างอิงคือ Elixir 1.20.1, OTP 29 และดูเหมือนว่า Elixir จะไม่เตือนเกี่ยวกับปัญหานี้

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

 
GN⁺ 3 시간 전
ความคิดเห็นจาก Lobste.rs
  • ผมไม่ใช่โปรแกรมเมอร์ Elixir แต่สิ่งที่น่าประหลาดใจที่สุดในตัวอย่างสุดท้ายคือ ข้อผิดพลาดในนิพจน์ guard ไม่ถูกส่งต่อไปยังผู้เรียก แต่ guard นั้นถูก “ข้ามไป”
    พอเข้าใจได้ว่าทำไมถึงออกแบบแบบนั้น แต่ก็ไม่น่าแปลกใจที่ผลลัพธ์จะออกมาตรงข้ามกับสัญชาตญาณ

  • ถ้าคิดว่าการออกแบบ API ของ Erlang มีไว้เพื่อช่วยเรื่อง การเขียนโปรแกรมแบบแสดงเจตนา ตามที่ Armstrong กล่าวไว้ใน วิทยานิพนธ์ Erlang p109/s4.5 ก็ถือว่าน่าขันอยู่
    ในวิทยานิพนธ์อธิบายแยกฟังก์ชันอย่าง dict:fetch(Key, Dict), dict:search(Key, Dict), dict:is_key(Key, Dict) ซึ่งเผยเจตนาของโปรแกรมเมอร์ว่า “คีย์ต้องมีอยู่แน่นอน”, “อาจมีอยู่ จึงแยก flow”, “ตรวจแค่ว่ามีอยู่หรือไม่”
    แต่ is_map_key/2 ของ Elixir ถ้าอาร์กิวเมนต์ “dict” ไม่ใช่ dict ก็จะโยน exception และความล้มเหลวจาก exception นั้นทำให้ทั้ง guard clause ล้มเหลว เหมือนจะทำลายการแบ่งแยกนี้
    ในทางกลับกัน ถ้ามีภาษาที่ or จับ exception แล้วรวมเป็น false ก็คงน่าประหลาดใจกว่าในกรณีอื่น ๆ เช่นกัน

  • ด้วย การถกเถียงนี้ ที่เคยเห็นก่อนหน้านี้ ทำให้ผมพร้อมตอบควิซครั้งนี้ และตอนนั้นก็ได้เรียนรู้อะไรอยู่หลายอย่าง

    • ผมได้แรงบันดาลใจจากการถกเถียงนั้นจนมาเขียนบทความนี้
  • แม้จะได้เรียนรู้อะไร แต่ก็เสียดายว่าทำไมถึงเลี่ยง การอ้างอิงถึง Pratchett
    Death คงกำลังกุมขมับอยู่ที่ไหนสักแห่ง
    จุดที่น่าสนใจตรงนี้มีสองอย่าง คือไม่ใช่ false แต่เป็น guard ที่ล้มเหลว ที่ทำให้นิพจน์ทั้งหมดล้มเหลว และค่อนข้างขัดสัญชาตญาณที่ is_map_key ไม่ได้รวมการตรวจ is_map ไว้ในตัว
    ถ้าเพิ่มรูปแบบที่สามอย่าง is_map(x) and is_map_key(x, :corporal) ก็จะทำงานตามที่คาดไว้
    พฤติกรรมของ is_map_key ดูไม่ค่อยสม่ำเสมออยู่บ้าง จึงรู้สึกน่าประหลาดใจ และก็น่าจะน่าสนใจหากลองตรวจดูว่า guard อื่น ๆ ที่เป็น is_... ตัวไหนปลอดภัย และตัวไหนต้องประเมินโดยมีความคาดหวังเรื่องชนิดข้อมูลแฝงอยู่

    • เห็นด้วยเรื่องการอ้างอิงถึง Pratchett แต่ตอนนี้เป็นช่วง คลื่นความร้อน สมองเลยไม่ทำงานอย่างที่คาดไว้
    • สงสัยเลยลองตรวจดูเองหลายอย่าง คร่าว ๆ แล้วดูเหมือน is_map_key จะเป็น guard is_ ตัวเดียวที่ต้องการอาร์กิวเมนต์ชนิดเฉพาะ
      ฟังก์ชัน is_ อื่น ๆ มีลักษณะเป็นบูลีนในตัว และคืนค่า true | false เสมอโดยไม่ล้มเหลว
  • ตรงนี้เกิดคำถามเรื่อง สไตล์ของ Elixir ที่น่าสนใจขึ้นมา
    ตัวอย่างสนุกและอธิบายได้ดี แต่โดยส่วนตัวแล้วถ้าเป็นไปได้จะชอบ pattern matching มากกว่า guard
    แน่นอนว่ามีข้อยกเว้น แต่ฟังก์ชันแบบนี้ปกติผมน่าจะเขียนเป็นหลาย function clause เช่น def a(%{foo: _x}), do: true, def a(x) when is_integer(x), do: true, def a(_), do: false

  • น่าอ่านประกอบ: https://learnyouahaskell.github.io/syntax-in-functions.html/…

    • guard ของ Haskell ต่างออกไปเล็กน้อย
      ใน Haskell สามารถเรียกฟังก์ชันใดก็ได้ภายใน guard แต่ Erlang จำกัดชุดฟังก์ชันที่อนุญาตให้ใช้ภายในนั้น