언어 — 표현식

언어 — 표현식 (The Language — Expressions)

루아의 표현식(expression)은 값들을 계산해요. 기본 표현식은 다음과 같아요.

exp ::=  nil | false | true | Numeral | LiteralString
       | functiondef | prefixexp | tableconstructor | '...'
       | exp binop exp | unop exp

즉 표현식은 리터럴 상수, 함수 정의, 접두사 표현식(= 변수·함수 호출·테이블 접근), 테이블 생성자, 가변 인자(...), 이항·단항 연산의 조합이에요.

산술 연산자 (Arithmetic Operators)

이항 산술 연산자는 +(덧셈), -(뺄셈), *(곱셈), /(나눗셈), //(바닥 나눗셈), %(나머지), ^(거듭제곱), 단항 -(부정)이에요.

x = 10 + 5        --> 15
y = 7 // 2        --> 3  (바닥 나눗셈, 정수)
r = 7 % 3         --> 1  (나머지)
p = 2 ^ 10        --> 1024
  • /는 항상 실수 나눗셈(부동소수점)을 수행해요.
  • //는 피연산자 중 하나가 실수면 실수로, 둘 다 정수면 정수 바닥 나눗셈이에요.
  • %는 나머지 연산으로, a % b == a - floor(a/b) * b와 같아요. 두 피연산자가 정수면 정수 나머지, 그 외엔 실수로 동작해요.
  • ^는 거듭제곱. 우결합(right associative)이라 2^3^22^(3^2)로 해석돼요.
  • 단항 -는 부호 반전이에요.

산술 연산은 기본적으로 숫자 피연산자에 적용돼요. 숫자가 아니면 __add, __sub 등의 메타메서드를 시도해요 (2.4 절 참조).

비트 연산자 (Bitwise Operators)

비트 연산자는 &(AND), |(OR), ~(XOR), ~(단항 NOT), <<(왼쪽 시프트), >>(오른쪽 시프트)예요.

a = 0b1100       -- 12
b = 0b1010       -- 10
print(a & b)     --> 8   (0b1000)
print(a | b)     --> 14  (0b1110)
print(a ~ b)     --> 6   (0b0110)
print(~a)        --> -13 (비트 NOT, 부호 포함)
print(1 << 4)    --> 16
print(16 >> 2)   --> 4

비트 연산은 정수 피연산자를 요구해요. 실수(정수 값)가 들어오면 정수로 변환되고, 변환 불가능하면 오류가 발생해요. 음수 시프트 횟수나 범위를 벗어나는 시프트는 0이 돼요. 비트 연산에도 __band, __bor, __bxor, __bnot, __shl, __shr 메타메서드가 있어요.

강제 변환과 변환 (Coercions and Conversions)

루아는 특정 상황에서 자동으로 값을 변환(강제 변환, coercion)해요.

  • 문자열 → 숫자: 산술 연산에서 문자열 피연산자에 "숫자와 일치하는" 텍스트가 있으면(예: "10", "3.5", "0xff") 자동으로 숫자로 변환돼요. "10" + 111이 돼요. 변환 불가능한 문자열이면 오류가 발생해요. tonumber를 사용하면 명시적으로 변환할 수 있고, "10" == 10은 거짓이지만 "10" + 1은 가능해요.
  • 정수 ⇄ 실수: 산술 연산에서 정수와 실수가 섞이면 실수로 승격돼요. 1 + 0.51.5예요. tonumber, math.type 등으로 구분할 수 있어요.
  • 숫자 → 문자열: 문자열 연결(..)에서 숫자는 문자열로 변환돼요. 10 .. "개""10개"가 돼요. tostring으로 명시적으로 변환할 수도 있어요.

루아에서 문자열 연결 연산자 ..는 숫자를 자동으로 문자열로 변환하지만, 산술 연산은 문자열을 자동으로 숫자로 변환해요. 다만, #(길이) 연산자는 문자열에만 적용되고, == 비교는 타입이 다른 두 값(숫자와 문자열)을 같다고 판단하지 않아요.

관계 연산자 (Relational Operators)

관계 연산자는 ==(등호), ~=(부등호), <(작음), >(큼), <=(작거나 같음), >=(크거나 같음)이에요.

print(1 == 1)      --> true
print(1 ~= 2)      --> true
print("a" < "b")   --> true   (사전식 비교)
  • ==~=는 다른 타입의 두 값을 비교할 때 (테이블/함수 등 __eq가 없는 경우) 그 값이 같지 않다고 판단해요. 즉 1 == "1"false예요. 같은 타입이면 nil은 자기 자신과 같고, 불변 값(숫자·문자열)은 값으로, 가변 값(테이블 등)은 참조(identity)로 비교해요.
  • <, >, <=, >=는 숫자와 문자열에 적용되고, 둘 다 숫자거나 둘 다 문자열이어야 해요. 사전식 문자열 비교는 바이트 단위로 이뤄져요. 다른 타입이면 __lt, __le 메타메서드를 시도해요.

논리 연산자 (Logical Operators)

논리 연산자는 and, or, not이에요.

local a = true and 10      -- a는 10 (true and 값)
local b = nil or "기본값"   -- b는 "기본값"
local c = not nil          -- c는 true
  • not은 항상 boolean을 반환해요.
  • andor단락(short-circuit) 평가를 해서, 첫 번째 피연산자만으로 결과가 정해지면 두 번째는 평가하지 않아요. a and ba가 거짓이면 a를, 아니면 b를 반환해요. a or ba가 참이면 a를, 아니면 b를 반환해요.
  • nilfalse만 거짓이고 나머지는 참이라는 점에서, and/or는 값 자체를 반환하므로 a or fallback 같은 관용구(패턴)가 널리 쓰여요.

연결 연산자 (Concatenation)

..는 문자열 연결 연산자예요. 피연산자 중 하나가 숫자면 문자열로 변환된 뒤 연결돼요.

print("루아" .. " 5.4")   --> 루아 5.4
print(10 .. 2)           --> 102
print(1 .. 2 .. 3)       --> 123   (좌결합)

..는 좌결합(left associative)이에요. 피연산자에 __concat 메타메서드가 있으면 그것이 호출돼요.

길이 연산자 (The Length Operator)

#는 길이 연산자예요.

  • 문자열에 적용하면 그 바이트 수(길이)를 반환해요. UTF-8 문자열의 "문자 수"가 아니라 바이트 수라는 점에 주의하세요.
  • 테이블에 적용하면 "테이블의 길이"를 반환하는데, 루아는 #t를 최대 인덱스라고 정의하지 않고 "배열" 부분의 경계로 정의해요. 구체적으로 정수 키 중 연속된 1, 2, ... 중 어디까지 실제 값이 있는지에 따라 결정되는, 경계가 될 수 있는 정수 n을 반환해요. 즉 #tt[n] ~= nil이고 t[n+1] == nil인 어떤 n을 주기 때문에, 희소 배열(sparse array)이나 정수 키가 구멍(hole) 있는 테이블에서는 "정의되지 않은 동작"이 아니라 "경계가 불명확"해서 아무 값일 수 있어요. 배열(1부터 연속) 테이블에서는 항상 원소 수가 돼요.
  • 그 외 값에 적용하면 __len 메타메서드를 시도하고, 없으면 오류가 발생해요.

우선순위 (Precedence)

루아의 연산자 우선순위는 (높은 것부터), 괄호로 묶인 표현식이 가장 높아요:

or            (가장 낮음)
and
<     >     <=    >=    ~=    ==
|
~
&
<<    >>
..    (우결합)
+     -
*     /     //    %
unary operators  (not   #     -     ~)
^     (우결합, 단항 연산자보다 높음)
  • ..^는 우결합(right associative)이에요.
  • ^는 단항 연산자보다 우선순위가 높아서 -x^2-(x^2)로 해석돼요.
  • 다른 이항 연산자는 모두 좌결합(left associative)이에요.
  • 괄호 ( ... )로 그룹화할 수 있고, 우선순위를 명시적으로 제어할 수 있어요.

테이블 생성자 (Table Constructors)

테이블 생성자는 테이블 값을 만드는 표현식이에요. {}로 빈 테이블을 만들고, 필드를 나열해 초기화할 수 있어요. 필드는 세 가지 형태가 있어요.

local t = {}                      -- 빈 테이블
local t2 = { x = 10, y = 20 }     -- 레코드 스타일 (키: 값)
local t3 = { "a", "b", "c" }      -- 배열 스타일 (1, 2, 3 키)
local t4 = { [1] = "일", [3] = "삼" }  -- 명시적 키
  • 레코드 스타일: { name = value, ... } — 키가 문자열.
  • 배열 스타일: { value, value, ... } — 키가 자동으로 1, 2, 3...
  • 명시적 키: { [exp] = exp } — 어떤 표현식이든 키가 될 수 있음.

생성자에서 필드 뒤에 콤마(,)나 세미콜론(;)을 쓸 수 있고, 마지막 필드 뒤에는 생략해도 돼요. 배열 스타일과 레코드 스타일은 섞을 수 있어요.

local t = {
  "첫 번째",           -- t[1]
  name = "루아",        -- t["name"]
  [10] = "열 번째"      -- t[10]
}

생성자에서 필드 값으로 함수를 바로 정의하는 것도 흔해요.

local obj = {
  value = 0,
  add = function(self, n) self.value = self.value + n end
}

생성자에서 __index = self 같은 고급 패턴으로 메타테이블을 설정할 수도 있어요.

함수 호출 (Function Calls)

함수 호출은 prefixexp(args) 형태이고, 메서드 호출은 콜론(:)을 써요. 호출은 __call 메타메서드가 있으면 테이블에서도 가능해요.

f(1, 2)          -- 함수 호출
obj:method(3)    -- 메서드 호출 (obj.method(obj, 3)과 같음)
t["fn"](4)       -- 필드 함수 호출

인자를 몇 개 넣든 호출할 수 있고, 반환값은 호출 위치에 따라 하나 또는 여러 개로 조정돼요. 함수 호출이 문장의 마지막 표현식이나 return의 마지막 표현식이면 모든 반환값이 전달돼요. 그 외 위치에서는 첫 번째 값만 쓰여요 (3.4.12 "표현식 목록" 참조).

함수 정의 (Function Definitions)

함수 정의는 function 키워드로 만들고, functiondef 표현식 또는 function ... end 문장 형태예요.

local function add(a, b)   -- 지역 함수 정의
  return a + b
end

function greet(name)
  return "안녕, " .. name
end

-- 익명 함수 (값으로)
local double = function(x) return x * 2 end
  • 함수 본문은 블록이고, local 변수를 만들 수 있고, return 값은 여러 개일 수 있어요.
  • 함수는 다른 함수 안에서 정의될 수 있고, 그 경우 바깥 함수의 지역 변수(업밸류, upvalue)를 포획(clos체ure)해요. 이런 함수를 클로저(closure) 라고 해요.
  • 매개변수는 함수 지역 변수로 취급돼요. 가변 인자(vararg) 함수는 ...로 받아요.
  • 메서드 정의 function t:f(...) ... end는 자동으로 self라는 첫 번째 파라미터를 추가해요.
local function counter()
  local n = 0
  return function()
    n = n + 1
    return n
  end
end
local c = counter()
print(c())  --> 1
print(c())  --> 2

표현식 목록, 다중 결과, 조정 (Lists of expressions, multiple results, and adjustment)

루아의 함수 호출은 여러 값을 반환할 수 있어요. 표현식 목록(explist)에서 함수 호출이 목록의 마지막 표현식이면 그 모든 반환값이 사용돼요. 그 외 위치에서는 첫 번째 값만 사용돼요.

local a, b = f()      -- f의 반환값이 a, b로 들어감
local x = f()         -- f의 첫 반환값만 x로

할당, return, 함수 인자, 테이블 생성자 등에서 조정(adjustment) 이 일어나요.

  • 할당에서 값이 변수 수보다 많으면 잘리고, 적으면 nil로 채워져요.
  • 함수 호출 인자에서, 마지막 인자가 함수 호출이면 그 반환값들이 인자로 펼쳐져요.
  • return은 함수의 반환값이 조정돼요 — return이 여러 값이면 호출자가 받아요.
function f() return 1, 2, 3 end
local a, b, c = f()    -- a=1, b=2, c=3
print(f())             --> 1 2 3
local d = f()          -- d=1 (첫 값만)

...(vararg)도 여러 값을 표현하는데, select("#", ...)로 개수를, {...}로 테이블로 묶을 수 있어요.

출처: 표현식 (Expressions)

본문

이항·단항 연산자 표

모든 이항 연산자는 exp binop exp 형태, 단항 연산자는 unop exp 형태예요. 단항 연산자는 not, #(길이), -(부정), ~(비트 NOT)이에요. 이항 연산자와 우선순위는 위 "우선순위" 절을 참조해요.

여러 결과의 조정 규칙 다시 보기

함수 호출이 마지막에 오는 표현식 목록에서만 그 함수의 전체 반환값들이 전달된다는 규칙이 핵심이에요. 예를 들어 테이블 생성자 { f() }f의 반환값이 하나라면 그 값이 t[1]이 되고, 여러 개면 그 값들이 배열 필드로 펼쳐져요. return f()f의 전체 반환을 그대로 되돌려줘요.

더 알아보기