빈 리스트에 대한 all? 함수의 리턴 값은? - 공허참(vacuous truth)

3 minute read

Enum.all?/1 함수를 사용한 함수 테스트할 때였던 걸로 기억한다. 비어 있는 리스트를 넣어서 테스트했을 때, 기대하지 않은 값이 나와서 의아했다. 비어 있는 리스트를 인자로 넘겼을 때, false 값이 나올 것으로 기대했다. 하지만 true 로 나오는 것이었다. Enum.any?/2 함수를 사용해 nil인 값이 있냐로 변경하고 관련 로직도 수정했다. 왜 빈 리스트에 대해서 Enum.all/1 함수는 true 값을 리턴하는 걸까?

빈 리스트에 대한 Elixir의 Enum.all?, Enum.any? 함수의 리턴 값은?

비어있는 리스트가 아닐 때는 Enum.all?/1 함수와 Enum.any?/1 함수 동작은 명확하다. Enum.all?/1 함수는 모든 요소(element)가 참같을(truthy) 때 true를 리턴하고 아니면 false를 리턴한다. Enum.any?/1 함수는 한 요소라도 참같으면 true를 리턴한다. 여기서 참같은(truthy) 값은 false와 nil이 아닌 모든 값을 말한다.

그렇다면 비어있는 리스트는?

IO.puts Enum.all?([:a, 1, true]) # => true
IO.puts Enum.all?([nil, false, :ok]) #=> false
IO.puts Enum.all?([]) # => true

true 값을 리턴한다. 우선 Enum.any?/1 함수는 어떤지 보자.

IO.puts Enum.any?([:a, 1, true]) # => true
IO.puts Enum.any?([nil, false, :ok]) # => true
IO.puts Enum.any?([]) # => false

false 값을 리턴한다.

Enum.any?/1 동작은 이해가 된다. 비어있어서 참같은 값이 하나도 없기 때문이다. 하지만 Enum.all?/1 동작은 헷갈린다. false 값을 리턴해야 하는 거 아닌가? 비어있어서 모두 참같은 값이 하나도 없기 때문이다.

이렇게 이해가 안 가면 뭘 봐야 한다? 수학이다. 공허참(vacuous truth)을 이해해야 한다. 바로 공허참으로 가는 길은 없으니 실질 조건문(material conditional)을 들렀다 가보자.

실질 조건문(material conditional)

The material conditional (also known as material implication) is an operation commonly used in logic.

실질 조건문은 논리학에서 일반적으로 사용하는 연산이다.

Material conditional - en.wikipedia.org

p → q는 ’만약 P라면 Q다’로 읽는다. 수학 시간에 배운 기억이 있다.

논리학이 나오면 진리표(truth table)도 나와야지.

p q p → q
True True True
True False False
False True True
False False True

여기서 주목해야 할 게 p가 거짓이면 p → q는 항상 참이라는 점이다. “자유의 여신상이 서울에 있다면 에펠탑은 포항에 있다”는 참이 된다. p인 “자유의 여신상이 서울에 있다”가 거짓이기 때문이다. p가 거짓이면 무조건 참인데, 이걸 공허참(vacuous truth)이라고 한다.

그럼 이 공허한 게 왜 비어있는 리스트와 관련이 있을까?

공허참(vacuous truth)과 비어있는 리스트

실질 조건문 p → q 에서 p가 모든 원소에 대해 거짓이면 실질 조건문은 참이라는 것을 봤다.

  • \(\forall x: P(x) \Rightarrow Q(x)\), where it is the case that \(\forall x: \neg P(x)\)
  • \(\forall x \in A: Q(x)\), where the Set \(A\) is empty set

Vacuous truth - en.wikipedia.org

첫 번째 정의가 바로 그것이다. 실질 조건문 진리표에서 살펴본 내용이다. 두 번째가 중요하다. 비어있는 집합(set)에 대해서는 어떠한 명제도 참이 된다. 예제를 하나 보자.

“all cell phones in the room are turned off”, it can be formally written as \(\forall x \in A: Q(x)\) where \(A\) is the set of all cell phones in the room and \(Q(x)\) is “\(x\) is turned off”. This can be written to a material conditional statement \(\forall x \in B: P(x) \Rightarrow Q(x)\) where \(B\) is the set of all things in the room (including cell phones if they exist in the room), the antecedent \(P(x)\) is “\(x\) is a cell phone”, and the consequent \(Q(x)\) is “\(x\) is turned off”.

Vacuous truth - en.wikipedia.org

“방 안의 모든 휴대폰이 꺼져 있다”를 \(\forall x \in A: Q(x)\) 로 보편 양화(universal quantification)시킬 수 있다. 여기서 \(A\) 는 “방 안의 모든 휴대폰”이고 \(Q(x)\) 는 “\(x\) 는 꺼져 있다.”가 된다. P이면 Q이다 형식을 가진 실질 조건문(material conditional)으로 써볼 수도 있다. \(\forall x \in B: P(x) \Rightarrow Q(x)\) 가 되는데, \(B\) 는 방에 있는 모든 것(만약 방 안에 휴대폰이 있다면 그 휴대폰도 포함), \(P(x)\) 는 “\(x\) 는 휴대폰이다.”, \(Q(x)\) 는 “\(x\) 는 꺼져 있다”가 된다. \(A\) 를 그대로 쓰지 않고 \(B\) 로 바꿔 쓴 이유는 방에 있는 모든 것을 포함하는 조건으로 바뀌어서 집합 자체가 다르기 때문이다.

Enum.all?([])

그렇다면 이 함수에 대해 실질 조건문을 작성해 보자.

  • \(\forall x \in A: P(x) \Rightarrow Q(x)\)
  • \(A\): integer, float, nil과 같이 프로그램으로 표현할 수 있는 모든 요소다.
  • \(P(x)\): x는 리스트 요소다
  • \(Q(x)\): x는 참같은(truthy) 값이다

\(P(x)\) 는 거짓이 된다. 리스트가 비어있기 때문에 리스트 요소가 아니다. 그래서 어떠한 \(Q(x)\) 가 오더라도 \(\forall x \in A: P(x) \Rightarrow Q(x)\) 명제는 참이 된다.

마치며

Enum.all?([])
#=> true

비어있는 리스트에 대한 Enum.all?/1 함수가 왜 true 값을 리턴할까? 구현 편의상 그렇게 했다면 쉽게 이해된다. true && f(elem1) && f(elem2) ... 로 구현하면 되기 때문이다. 그래도 뭔가 찝찝했고 찾아보니 공허참까지 나왔다. 정리하려고 적다 보니 믿음이 아니라 머리로도 이해가 되는 것 같다.

Prelude> all (const True) []
True

>>> all([])
True

> (every? identity [])
true

참고로 Haskell, Python, Clojure 같은 다른 언어도 모두 비어 있는 리스트에 대한 all 연산 리턴 값으로 true를 리턴한다.

링크