1P by GN⁺ | ★ favorite | 댓글 1개
  • 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 기준 경고가 없음

조건 순서가 결과를 바꾸는 예시

  • 예시 모듈 Fooa/1b/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)false
      • is_map_key(x, :foo)true
      • or 결과가 true라 첫 번째 절이 매칭됨
    • 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를 반환하는 대신 실패
      • 가드 함수 하나의 실패는 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로 합치는 언어가 있다면 다른 경우에는 더 놀라울 것 같기도 함

  • 예전에 봤던 이 토론 덕분에 이번 퀴즈를 풀 준비가 되어 있었고, 그때 몇 가지를 배웠음

    • 그 토론에서 영감을 받아 이 글을 쓰게 됐음
  • 배운 건 있었지만, 왜 Pratchett 참조를 피했는지 아쉬움
    Death가 어디선가 이마를 짚고 있을 듯함
    여기서 흥미로운 점은 두 가지인데, false가 아니라 실패한 가드는 전체 표현식을 실패하게 만들고, 다소 직관과 다르게 is_map_keyis_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은 그 안에서 허용되는 함수 집합을 제한함