- ใน 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/1a/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})จะเป็นtrueis_integer(x)เป็นfalseis_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})จะเป็นtrueis_map_key(x, :foo)เป็นtrueis_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 ความคิดเห็น
ความคิดเห็นจาก 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ก็คงน่าประหลาดใจกว่าในกรณีอื่น ๆ เช่นกันis_map_key/2เป็น ฟังก์ชัน Erlang ธรรมดาอย่างสมบูรณ์https://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
ด้วย การถกเถียงนี้ ที่เคยเห็นก่อนหน้านี้ ทำให้ผมพร้อมตอบควิซครั้งนี้ และตอนนั้นก็ได้เรียนรู้อะไรอยู่หลายอย่าง
แม้จะได้เรียนรู้อะไร แต่ก็เสียดายว่าทำไมถึงเลี่ยง การอ้างอิงถึง Pratchett
Death คงกำลังกุมขมับอยู่ที่ไหนสักแห่ง
จุดที่น่าสนใจตรงนี้มีสองอย่าง คือไม่ใช่
falseแต่เป็น guard ที่ล้มเหลว ที่ทำให้นิพจน์ทั้งหมดล้มเหลว และค่อนข้างขัดสัญชาตญาณที่is_map_keyไม่ได้รวมการตรวจis_mapไว้ในตัวถ้าเพิ่มรูปแบบที่สามอย่าง
is_map(x) and is_map_key(x, :corporal)ก็จะทำงานตามที่คาดไว้พฤติกรรมของ
is_map_keyดูไม่ค่อยสม่ำเสมออยู่บ้าง จึงรู้สึกน่าประหลาดใจ และก็น่าจะน่าสนใจหากลองตรวจดูว่า guard อื่น ๆ ที่เป็นis_...ตัวไหนปลอดภัย และตัวไหนต้องประเมินโดยมีความคาดหวังเรื่องชนิดข้อมูลแฝงอยู่is_map_keyจะเป็น guardis_ตัวเดียวที่ต้องการอาร์กิวเมนต์ชนิดเฉพาะฟังก์ชัน
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/…
ใน Haskell สามารถเรียกฟังก์ชันใดก็ได้ภายใน guard แต่ Erlang จำกัดชุดฟังก์ชันที่อนุญาตให้ใช้ภายในนั้น