평가 순서

평가 순서

프로그램을 읽다 보면 "이 함수 호출과 저 함수 호출, 어느 쪽이 먼저 실행될까?" 하고 궁금해질 때가 있어요. 대부분의 명령형 언어처럼 Nim도 계산 순서에 엄격한 규칙을 정해 두는데요, 이 규칙을 알면 실험 코드가 왜 그런 결과를 내는지 예측하기 쉬워져요. 이번 장에서는 Nim이 표현식을 어떤 순서로 평가하는지 확인해 볼게요.

출처: Nim Manual

본문

평가 순서(Order of evaluation) 는 명령형 언어에서 흔히 그렇듯 왼쪽에서 오른쪽으로, 안쪽에서 바깥쪽으로 진행돼요:

var s = ""
proc p(arg: int): int =
  s.add $arg
  result = arg
discard p(p(1) + p(2))
doAssert s == "123"

여기서 p(p(1) + p(2))를 보면, 바깥 p의 인자 안쪽부터 계산돼요. p(1)이 먼저 실행되어 s"1"이 쌓이고, 그다음 p(2)가 실행되어 "2"가 쌓여요. 마지막으로 바깥 p(3)이 실행되면서 "3"이 붙어 최종적으로 s == "123"이 되는 거죠. 안쪽부터, 그리고 같은 깊이에서는 왼쪽부터라는 규칙이 그대로 드러나요.

대입(assignment) 도 특별할 게 없어요. 오른쪽 값보다 왼쪽 대상 표현식이 먼저 평가돼요:

var v = 0
proc getI(): int =
  result = v
  inc v
var a, b: array[0..2, int]
proc someCopy(a: var int; b: int) = a = b
a[getI()] = getI()
doAssert a == [1, 0, 0]
v = 0
someCopy(b[getI()], getI())
doAssert b == [1, 0, 0]

첫 번째 대입 a[getI()] = getI()에서는 왼쪽의 getI()가 먼저 호출되어 0을 돌려주고 v가 1이 돼요. 그래서 a[0]이 오른쪽 getI()의 값 1을 받아 a == [1, 0, 0]이 되는 거예요. 두 번째의 someCopy(b[getI()], getI())도 마찬가지 순서로 동작해서 같은 결과를 내요.

이렇게 왼쪽을 먼저 평가하는 이유는 간단해요. 오버로딩된 대입이나 대입 비슷한 연산(assignment-like operation)과 일관성을 유지하기 위해서예요. a = bperformSomeCopy(a, b)처럼 읽을 수 있게 하려면, 함수 호출에서 인자가 왼쪽부터 평가되는 규칙과 맞아야 하거든요.

한 가지 꼭 짚고 넘어가야 할 게 있어요. "평가 순서"라는 개념은 코드가 정규화(normalized)된 뒤에만 적용돼요. 정규화에는 템플릿 확장과, **named parameter(이름 있는 인자)**로 넘겨진 인자의 재배치가 포함돼요:

var s = ""
proc p(): int =
  s.add "p"
  result = 5
proc q(): int =
  s.add "q"
  result = 3
# 평가 순서는 템플릿 확장의 의미에 따라 'b'가 'a'보다 앞선다.
template swapArgs(a, b): untyped =
  b + a
doAssert swapArgs(p() + q(), q() - p()) == 6
doAssert s == "qppq"
# named parameter는 평가 순서에 영향을 주지 않는다.
proc construct(first, second: int) =
  discard
# 'p'가 'q'보다 먼저 평가된다!
construct(second = q(), first = p())
doAssert s == "qppqpq"

첫 번째 예시에서 swapArgs(a, b)는 몸통이 b + a인 템플릿이에요. 템플릿이 확장되면서 인자 순서가 뒤바뀌므로, 평가도 처음 호출 순서가 아니라 확장된 형태(q() - p()가 먼저)를 따라 진행돼요. 그래서 s"qppq"가 되죠. 반면 두 번째 예시처럼 인자를 second = q(), first = p()로 써도, named parameter는 평가 순서를 바꾸지 않아요. 덕분에 여전히 pq보다 먼저 실행되고 결과적으로 s == "qppqpq"가 돼요.

정규화 이후의 순서에만 신경 쓰는 이유는, 가상의 대안들보다 구현이 훨씬 쉽기 때문이에요.

더 알아보기 (Learn more)