- Trong guard của Elixir, chỉ cần đổi thứ tự điều kiện
orcũng có thể khiến kết quả của đoạn code trông như cùng một biểu thức logic bị thay đổi - Thứ tự
is_integer(x) or is_map_key(x,:foo)sẽ thực hiện đánh giá ngắn mạch trước với đầu vào là số nguyên, bỏ qua phép kiểm tra nguy hiểm - Ngược lại,
is_map_key(x,:foo) or is_integer(x)với đầu vào là số nguyên sẽ không trả vềfalseở điều kiện đầu mà thất bại, nên không đi tiếp tới điều kiện sau - Vì khác biệt này,
Foo.a(%{foo: 21}),Foo.a(37),Foo.b(%{foo: 21})làtrue, nhưngFoo.b(37)làfalse - Trông như tính giao hoán của phép toán Boolean bị phá vỡ, nhưng
orcó đánh giá ngắn mạch vốn chịu ảnh hưởng bởi thứ tự điều kiện; theo Elixir 1.20.1 và OTP 29 thì không có cảnh báo
Ví dụ thứ tự điều kiện làm thay đổi kết quả
- Module ví dụ
Foođịnh nghĩa hai hàma/1vàb/1a/1: kiểm tra guard theo thứ tựis_integer(x) or is_map_key(x, :foo)b/1: kiểm tra guard theo thứ tựis_map_key(x, :foo) or is_integer(x)- Nếu guard khớp thì trả về
true, nếu không thì mệnh đề tiếp theo trả vềfalse
-
a/1: khi điều kiện an toàn đứng trướcFoo.a(%{foo: 21})trở thànhtrueis_integer(x)làfalseis_map_key(x, :foo)làtrue- Kết quả của
orlàtrue, nên mệnh đề đầu tiên khớp
Foo.a(37)cũng trở thànhtrueis_integer(x)làtrue- Vì
orđược đánh giá ngắn mạch,is_map_key(x, :foo)không được thực thi
-
b/1: khi điều kiện có thể thất bại đứng trướcFoo.b(%{foo: 21})trở thànhtrueis_map_key(x, :foo)làtrueis_integer(x)phía sau không được thực thi
Foo.b(37)làfalse- Điều kiện đầu tiên
is_map_key(x, :foo)thất bại thay vì trả vềfalse - Sự thất bại của một hàm guard không được chuyển thành
false, mà làm toàn bộ biểu thức guard thất bại is_integer(x)phía sau không được gọi và mệnh đề đầu tiên cũng không khớp
- Điều kiện đầu tiên
Đánh giá ngắn mạch và việc không có cảnh báo
- Với nhiều lập trình viên Elixir, hành vi này có thể trông như tính giao hoán của toán tử Boolean bị phá vỡ
- Tuy nhiên, vì
orthực hiện đánh giá ngắn mạch, không thể cho rằng đổi vị trí hai điều kiện thì luôn cho cùng kết quả - Môi trường tham chiếu là Elixir 1.20.1, OTP 29, và có vẻ Elixir không cảnh báo về vấn đề này
1 bình luận
Các ý kiến trên Lobste.rs
Tôi không phải lập trình viên Elixir, nhưng điều gây ngạc nhiên nhất ở ví dụ cuối là lỗi trong biểu thức guard không được truyền ngược về caller, mà guard đó bị “bỏ qua”
Tôi nghĩ mình hiểu vì sao họ thiết kế như vậy, nhưng cũng không ngạc nhiên khi nó dẫn đến kết quả trái trực giác
Thật mỉa mai nếu nghĩ rằng thiết kế API của Erlang vốn nhằm hỗ trợ lập trình có chủ đích như Armstrong nói trong luận án Erlang p109/s4.5
Trong luận án, ông giải thích việc tách các hàm như
dict:fetch(Key, Dict),dict:search(Key, Dict),dict:is_key(Key, Dict)để thể hiện ý định của lập trình viên: “khóa bắt buộc phải tồn tại”, “khóa có thể tồn tại nên rẽ nhánh luồng xử lý”, “chỉ kiểm tra sự tồn tại”Nhưng
is_map_key/2của Elixir lại ném ngoại lệ nếu đối số “dict” không phải dict, và lỗi ngoại lệ đó khiến toàn bộ mệnh đề guard thất bại, dường như phá vỡ sự phân biệt nàyNgược lại, nếu có một ngôn ngữ trong đó
orbắt ngoại lệ rồi gộp thànhfalse, thì ở các trường hợp khác có lẽ còn gây ngạc nhiên hơnis_map_key/2thực ra là một hàm Erlang hoàn toàn bình thườnghttps://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
Nhờ cuộc thảo luận này mà tôi từng xem trước đây, tôi đã sẵn sàng giải câu đố lần này, và khi đó đã học được vài điều
Có học được điều mới, nhưng tôi vẫn tiếc vì sao lại tránh tham chiếu tới Pratchett
Chắc đâu đó Death đang ôm trán
Điểm thú vị ở đây có hai phần: một guard thất bại, chứ không phải
false, sẽ làm toàn bộ biểu thức thất bại; và hơi trái trực giác làis_map_keykhông bao hàm kiểm trais_mapNếu thêm biến thể thứ ba như
is_map(x) and is_map_key(x, :corporal)thì nó hoạt động đúng như mong đợiHành vi của
is_map_keytrông hơi thiếu nhất quán nên gây bất ngờ, và có lẽ sẽ thú vị nếu kiểm tra các guardis_...khác để xem cái nào an toàn, cái nào phải được đánh giá với kỳ vọng về kiểu dữ liệuis_map_keylà guardis_duy nhất yêu cầu một loại đối số cụ thểCác hàm
is_khác đều mang tính boolean và luôn trả vềtrue | false, không thất bạiỞ đây nảy sinh một câu hỏi thú vị về phong cách Elixir
Ví dụ thì vui và phần giải thích cũng tốt, nhưng cá nhân tôi nếu có thể sẽ thích pattern matching hơn guard
Tất nhiên vẫn có ngoại lệ, nhưng những hàm kiểu này thường tôi sẽ viết thành nhiều mệnh đề hàm như
def a(%{foo: _x}), do: true,def a(x) when is_integer(x), do: true,def a(_), do: falseCũng đáng xem kèm: https://learnyouahaskell.github.io/syntax-in-functions.html/…
Trong Haskell, bạn có thể gọi hàm tùy ý bên trong guard, còn Erlang thì giới hạn tập hàm được phép dùng trong đó