Elixir 가드의 단락 평가: 조건 순서가 결과를 바꿈
(hauleth.dev)- Elixir의 가드(guard) 에서는
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:is_integer(x) or is_map_key(x, :foo)순서로 가드를 검사함b/1:is_map_key(x, :foo) or is_integer(x)순서로 가드를 검사함- 가드가 매칭되면
true, 아니면 다음 절에서false를 반환함
-
a/1: 안전한 조건이 먼저 오는 경우Foo.a(%{foo: 21})는true가 됨is_integer(x)는falseis_map_key(x, :foo)는trueor결과가true라 첫 번째 절이 매칭됨
Foo.a(37)도true가 됨is_integer(x)가trueor가 단락 평가되므로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를 반환하는 대신 실패함 - 가드 함수 하나의 실패는
false로 변환되지 않고 전체 가드 표현식을 실패시킴 - 뒤의
is_integer(x)는 호출되지 않고 첫 번째 절도 매칭되지 않음
- 첫 조건
단락 평가와 경고 부재
- 많은 Elixir 개발자에게 이 동작은 불리언 연산자의 교환법칙이 깨진 것처럼 보일 수 있음
- 하지만
or는 단락 평가를 하므로 두 조건의 위치를 바꿔도 항상 같은 결과가 나온다고 볼 수 없음 - 기준 환경은 Elixir 1.20.1, OTP 29이며, 이 문제에 대해 Elixir가 경고하지 않는 것으로 보임
댓글과 토론
Lobste.rs 의견들
-
Elixir 프로그래머는 아니지만, 마지막 예제에서 가장 놀라운 건 가드 표현식의 오류가 호출자에게 전파되지 않고 그 가드가 “건너뛰어진다”는 점임
왜 그렇게 만들었는지는 알 것 같지만, 직관에 어긋나는 결과가 나오는 것도 놀랍지 않음 -
Erlang의 API 설계가 Armstrong의 Erlang thesis p109/s4.5에서 말한 의도적 프로그래밍을 돕기 위한 것이었다는 점을 생각하면 아이러니함
논문에서는dict:fetch(Key, Dict),dict:search(Key, Dict),dict:is_key(Key, Dict)처럼 프로그래머가 “키가 반드시 있어야 한다”, “있을 수도 있으니 흐름을 나눈다”, “존재 여부만 검사한다”는 의도를 드러내는 함수들을 나눠 설명함
그런데 Elixir의is_map_key/2는 “dict” 인자가 dict가 아니면 예외를 내고, 그 예외 실패가 전체 가드 절 실패로 이어져 이 구분을 깨는 것처럼 보임
반대로or가 예외를 잡아false로 합치는 언어가 있다면 다른 경우에는 더 놀라울 것 같기도 함is_map_key/2는 사실 완전히 평범한 Erlang 함수임
https://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
-
예전에 봤던 이 토론 덕분에 이번 퀴즈를 풀 준비가 되어 있었고, 그때 몇 가지를 배웠음
- 그 토론에서 영감을 받아 이 글을 쓰게 됐음
-
배운 건 있었지만, 왜 Pratchett 참조를 피했는지 아쉬움
Death가 어디선가 이마를 짚고 있을 듯함
여기서 흥미로운 점은 두 가지인데,false가 아니라 실패한 가드는 전체 표현식을 실패하게 만들고, 다소 직관과 다르게is_map_key가is_map검사를 내포하지 않는다는 것임
is_map(x) and is_map_key(x, :corporal)처럼 세 번째 변형을 추가하면 기대한 대로 동작함
is_map_key의 동작은 조금 일관성이 없어 보이고 그래서 놀랍게 느껴지며, 다른is_...가드들도 어떤 것은 안전하고 어떤 것은 타입 기대를 깔고 평가해야 하는지 확인해 보면 흥미로울 듯함- Pratchett 참조에는 동의하지만, 지금 폭염이라 뇌가 기대한 대로 동작하지 않음
- 궁금해서 몇 가지를 직접 확인해 봤는데, 대략 보기로는
is_map_key가 특정 종류의 인자를 요구하는 유일한is_가드인 것 같음
다른is_함수들은 불리언 성격을 내포하고 항상true | false를 반환하며 실패하지 않음
-
여기서 흥미로운 Elixir 스타일 질문이 생김
예제는 재미있고 설명도 잘 되지만, 개인적으로는 가능하면 가드보다 패턴 매칭을 선호함
물론 예외는 있지만, 이런 함수들은 보통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의 가드는 조금 다름
Haskell에서는 가드 안에서 임의의 함수를 호출할 수 있지만, Erlang은 그 안에서 허용되는 함수 집합을 제한함
- Haskell의 가드는 조금 다름