루아 레퍼런스 매뉴얼 — 언어: 표현식
루아 레퍼런스 매뉴얼 — 언어: 표현식 (Expressions)
표현식은 루아 프로그램에서 값을 만들어내는 기본 단위예요. 이번 구간에서는 루아가 어떤 표현식을 제공하는지, 그리고 연산자들이 어떤 우선순위로 결합되는지를 하나씩 살펴볼게요. 어떤 값이라도 만들 수 있는 조각들부터 시작해서, 산술·비트·비교·논리 같은 연산자, 그리고 테이블 생성자와 함수 호출·정의까지 다뤄요.
출처: Lua 5.4 Reference Manual, 섹션 3.4 "Expressions" (https://www.lua.org/manual/5.4/manual.html)
본문
3.4 Expressions — 루아의 기본 표현식
루아에서 기본이 되는 표현식은 다음과 같아요. 이 문법 표를 보면 루아의 표현식이 어떤 모양들로 구성되는지 한눈에 들어와요.
exp ::= prefixexp
exp ::= nil | false | true
exp ::= Numeral
exp ::= LiteralString
exp ::= functiondef
exp ::= tableconstructor
exp ::= '...'
exp ::= exp binop exp
exp ::= unop exp
prefixexp ::= var | functioncall | '(' exp ')'
숫자(Numeral)와 리터럴 문자열은 §3.1에서, 변수는 §3.2에서 각각 설명해요. 함수 정의는 §3.4.11, 함수 호출은 §3.4.10, 테이블 생성자는 §3.4.9에서 다루죠. 점 세 개(...)로 나타내는 가변 인자 표현식(vararg expression)은 오직 가변 인자 함수 안에서만 쓸 수 있고, 이 역시 §3.4.11에서 설명할게요.
이항 연산자에는 산술 연산자(§3.4.1), 비트 연산자(§3.4.2), 관계 연산자(§3.4.4), 논리 연산자(§3.4.5), 그리고 연결 연산자(§3.4.6)가 있어요. 단항 연산자로는 단항 마이너스(§3.4.1), 단항 비트 NOT(§3.4.2), 단항 논리 not(§3.4.5), 그리고 단항 길이 연산자(§3.4.7)가 있죠.
3.4.1 산술 연산자 (Arithmetic Operators)
루아가 지원하는 산술 연산자는 다음과 같아요.
+: 덧셈-: 뺄셈*: 곱셈/: 부동소수점 나눗셈//: 내림 나눗셈(floor division)%: 나머지(modulo)^: 거듭제곱-: 단항 마이너스
거듭제곱과 부동소수점 나눗셈을 빼면, 산술 연산자는 이렇게 동작해요. 두 피연산자가 모두 정수면 정수 연산으로 처리되어 결과도 정수예요. 둘 다 숫자라면 부동소수점으로 변환해서 머신의 부동소수점 규칙(보통 IEEE 754 표준)을 따라 연산하고, 결과는 부동소수점이죠. 여기서 문자열 라이브러리가 산술 연산에서 문자열을 숫자로 강제 변환(coerce)한다는 점이 있는데, 자세한 내용은 §3.4.3에서 볼게요.
거듭제곱(^)과 부동소수점 나눗셈(/)은 항상 피연산자를 부동소수점으로 변환하고, 결과도 항상 부동소수점이에요. 거듭제곱은 ISO C의 pow 함수를 사용하므로 정수가 아닌 지수에도 잘 동작해요.
내림 나눗셈(//)은 몫을 마이너스 무한대 방향으로 내림하는 나눗셈이에요. 다시 말해 두 피연산자를 나눈 값의 내림(floor)이 결과가 되죠.
나머지(%)는 몫을 마이너스 무한대 방향으로 내림하는 나눗셈(즉 내림 나눗셈)에서 생기는 나머지로 정의돼요.
정수 연산에서 오버플로가 나면, 모든 연산은 감싸서(wrap around) 처리돼요. 즉 넘친 값이 다시 최솟값 쪽으로 순환하는 방식이죠.
3.4.2 비트 연산자 (Bitwise Operators)
루아가 지원하는 비트 연산자는 다음과 같아요.
&: 비트 AND|: 비트 OR~: 비트 배타적 OR(XOR)>>: 오른쪽 시프트<<: 왼쪽 시프트~: 단항 비트 NOT
모든 비트 연산은 피연산자를 정수로 변환하고(§3.4.3 참고), 그 정수의 모든 비트에 대해 연산한 뒤 결과를 정수로 돌려줘요.
왼쪽 시프트와 오른쪽 시프트 모두 비워진 자리를 0으로 채워요. 이동량(displacement)이 음수면 반대 방향으로 시프트되고, 절댓값이 정수의 비트 수보다 크거나 같으면 모든 비트가 밀려나가서 결과는 0이 되죠.
3.4.3 강제 변환과 변환 (Coercions and Conversions)
루아는 실행 시점에 일부 타입과 표현 사이의 자동 변환을 제공해요. 비트 연산자는 항상 부동소수점 피연산자를 정수로 변환하고, 거듭제곱과 부동소수점 나눗셈은 항상 정수 피연산자를 부동소수점으로 변환해요. 그 외에 정수와 부동소수점이 섞인 산술 연산은 정수 피연산자를 부동소수점으로 변환하죠. C API도 필요에 따라 정수를 부동소수점으로, 부동소수점을 정수로 변환해 줘요. 그리고 문자열 연결은 문자열뿐 아니라 숫자도 인자로 받아들여요.
정수에서 부동소수점으로 변환할 때, 정수 값이 부동소수점으로 정확히 표현되면 그 값이 결과예요. 아니라면 가장 가까운 더 큰 값이나 가장 가까운 더 작은 값으로 변환돼요. 이런 변환은 절대 실패하지 않아요.
반대로 부동소수점에서 정수로 변환할 때는, 그 부동소수점이 정수로 정확히 표현될 수 있는지(즉 정수 값이고 정수 표현 범위 안인지) 확인해요. 정확히 표현되면 그 값이 결과고, 아니면 변환이 실패해요.
루아의 여러 곳에서 필요할 때 문자열을 숫자로 강제 변환해요. 특히 문자열 라이브러리는 모든 산술 연산에서 문자열을 숫자로 강제 변환하려는 메타메서드를 설정해 둬요. 그런데 변환이 실패하면, 라이브러리는 다른 피연산자의 메타메서드를 호출하거나(존재한다면) 오류를 일으켜요. 비트 연산자는 이런 강제 변환을 하지 않는다는 점을 기억해 두세요.
문자열에서 숫자로의 암묵적 강제 변환은 항상 적용되는 게 아니므로, 여기에 의존하지 않는 게 좋아요. 특히 "1"==1은 거짓이고 "1"<1은 오류를 일으켜요(§3.4.4 참고). 이런 강제 변환은 주로 하위 호환성을 위해 존재하며, 앞으로의 언어 버전에서는 제거될 수 있어요.
문자열은 그 문법과 루아 어휘 분석기(lexer)의 규칙을 따라 정수나 부동소수점으로 변환돼요. 문자열은 앞뒤 공백과 부호 부호를 가질 수 있어요. 문자열에서 숫자로의 모든 변환은 소수점 문자로 점(.)과 현재 로케일의 소수 표시를 모두 받아들여요. 다만 루아 어휘 분석기는 점만 인정하죠. 문자열이 유효한 숫자가 아니면 변환이 실패해요. 필요하다면 이 첫 단계의 결과가 앞서 말한 정수·부동소수점 간 변환 규칙에 따라 특정 숫자 하위 타입으로 다시 변환돼요.
숫자에서 문자열로의 변환은 사양이 정해지지 않은 사람이 읽기 좋은 형식을 사용해요. 숫자를 문자열로 특정 방식으로 변환하고 싶다면 string.format 함수를 쓰면 돼요.
3.4.4 관계 연산자 (Relational Operators)
루아가 지원하는 관계 연산자는 다음과 같아요.
==: 같다~=: 같지 않다<: 작다>: 크다<=: 작거나 같다>=: 크거나 같다
이 연산자들은 항상 false 또는 true를 결과로 돌려줘요.
==(같음 비교)는 먼저 두 피연산자의 타입을 비교해요. 타입이 다르면 결과는 false예요. 타입이 같으면 값 자체를 비교하죠. 문자열은 바이트 내용이 같으면 같다고 판정되고, 숫자는 같은 수학적 값을 나타내면 같다고 판정돼요.
테이블, userdata, 스레드는 참조(reference)로 비교돼요. 두 객체가 같은 객체일 때만 같다고 보는 거죠. 테이블, userdata, 스레드 같은 새 객체를 만들 때마다, 그 객체는 기존의 모든 객체와 다르게 취급돼요. 함수는 항상 자기 자신과 같고, 감지 가능한 차이(다른 동작, 다른 정의)가 있는 함수는 항상 다르다고 판정돼요. 다른 시점에 만들어졌지만 감지 가능한 차이가 없는 함수는 같게 분류될 수도 있고 아닐 수도 있어요(내부 캐싱 세부 사항에 따라 달라져요).
테이블과 userdata를 비교하는 방식을 바꾸고 싶다면 __eq 메타메서드를 쓰면 돼요(§2.4 참고).
같음 비교는 문자열을 숫자로, 숫자를 문자열로 변환하지 않아요. 그래서 "0"==0은 false로 평가되고, t[0]과 t["0"]은 테이블에서 서로 다른 항목을 가리켜요.
~= 연산자는 정확히 같음(==)의 부정이에요.
순서 연산자(<, >, <=, >=)는 이렇게 동작해요. 두 인자가 모두 숫자면 하위 타입과 무관하게 수학적 값으로 비교해요. 둘 다 문자열이면 현재 로케일을 기준으로 값을 비교하죠. 그 외의 경우에는 __lt 또는 __le 메타메서드를 호출해 봐요(§2.4 참고). a > b는 b < a로, a >= b는 b <= a로 변환돼요.
IEEE 754 표준에 따라, 특수한 값 NaN은 자신을 포함해 어떤 값보다 작지도 같지도 크지도 않은 것으로 간주돼요.
3.4.5 논리 연산자 (Logical Operators)
루아의 논리 연산자는 and, or, not이에요. 제어 구조와 마찬가지로(§3.3.4 참고), 모든 논리 연산자는 false와 nil을 거짓으로, 그 외의 모든 값을 참으로 간주해요.
부정 연산자 not은 항상 false 또는 true를 돌려줘요. 논리곱 and는 첫 번째 인자가 false나 nil이면 그 값을, 그렇지 않으면 두 번째 인자를 돌려줘요. 논리합 or는 첫 번째 인자가 nil과 false 모두가 아니면 그 값을, 그렇지 않으면 두 번째 인자를 돌려주죠. and와 or는 모두 단락 평가(short-circuit)를 사용해요. 즉 두 번째 피연산자는 필요할 때만 평가돼요. 몇 가지 예를 볼게요.
10 or 20 --> 10
10 or error() --> 10
nil or "a" --> "a"
nil and 10 --> nil
false and error() --> false
false and nil --> false
false or nil --> nil
10 and 20 --> 20
3.4.6 연결 (Concatenation)
루아에서 문자열 연결 연산자는 점 두 개('..')로 나타내요. 두 피연산자가 모두 문자열이거나 숫자면, 숫자는 사양이 정해지지 않은 형식으로 문자열로 변환돼요(§3.4.3 참고). 그 외의 경우에는 __concat 메타메서드가 호출돼요(§2.4 참고).
3.4.7 길이 연산자 (The Length Operator)
길이 연산자는 단항 접두 연산자 #로 나타내요.
문자열의 길이는 그 바이트 수예요. 각 문자가 1바이트일 때의 일반적인 문자열 길이 의미와 같죠.
테이블에 적용된 길이 연산자는 그 테이블의 *경계(border)*를 돌려줘요. 테이블 t에서 경계란 다음 조건을 만족하는 음이 아닌 정수예요.
(border == 0 or t[border] ~= nil) and
(t[border + 1] == nil or border == math.maxinteger)
말로 풀면, 경계는 테이블에 존재하는 양의 정수 인덱스 중 그다음 인덱스가 비어 있는 인덱스예요. 여기에 두 가지 극단적인 경우가 더해져요. 인덱스 1이 비어 있으면 0이 경계이고, 정수의 최댓값이 인덱스로 존재하면 그 값이 경계죠. 양의 정수가 아닌 키는 경계에 영향을 주지 않아요.
경계가 정확히 하나인 테이블을 *시퀀스(sequence)*라고 불러요. 예를 들어 {10,20,30,40,50}은 경계가 5 하나뿐이므로 시퀀스예요. {10,20,30,nil,50}은 경계가 3과 5 두 개라서 시퀀스가 아니죠. (인덱스 4의 nil을 *구멍(hole)*이라고 불러요.) {nil,20,30,nil,nil,60,nil}은 경계가 0, 3, 6 세 개라서 이것도 시퀀스가 아니에요. {}는 경계 0인 시퀀스죠.
t가 시퀀스일 때 #t는 유일한 경계를 돌려주는데, 이는 시퀀스 길이에 대한 직관적인 개념과 일치해요. t가 시퀀스가 아니면 #t는 경계들 중 아무거나 돌려줄 수 있어요. (정확히 어떤 값이 나올지는 테이블의 내부 표현 세부 사항에 달려 있는데, 이는 테이블이 어떻게 채워졌는지와 숫자가 아닌 키들의 메모리 주소에 따라 달라질 수 있어요.)
테이블 길이의 계산은 최악의 경우에도 O(log n) 시간이 보장돼요. 여기서 n은 테이블에서 가장 큰 정수 키예요.
길이 연산자의 동작은 문자열을 제외한 어떤 값에 대해서도 __len 메타메서드로 바꿀 수 있어요(§2.4 참고).
3.4.8 우선순위 (Precedence)
루아의 연산자 우선순위는 아래 표를 따르는데, 아래에서 위로 갈수록 우선순위가 높아져요.
or
and
< > <= >= ~= ==
|
~
&
<< >>
..
+ -
* / // %
unary operators (not # - ~)
^
늘 그렇듯이 괄호로 표현식의 우선순위를 바꿀 수 있어요. 연결('..')과 거듭제곱('^') 연산자는 오른쪽 결합(right associative)이고, 나머지 모든 이항 연산자는 왼쪽 결합(left associative)이에요.
3.4.9 테이블 생성자 (Table Constructors)
테이블 생성자는 테이블을 만드는 표현식이에요. 생성자가 평가될 때마다 새로운 테이블이 만들어져요. 생성자는 빈 테이블을 만들 수도 있고, 테이블을 만들면서 일부 필드를 초기화할 수도 있어요. 생성자의 일반적인 문법은 다음과 같아요.
tableconstructor ::= '{' [fieldlist] '}'
fieldlist ::= field {fieldsep field} [fieldsep]
field ::= '[' exp ']' '=' exp | Name '=' exp | exp
fieldsep ::= ',' | ';'
[exp1] = exp2 형태의 각 필드는 새 테이블에 키 exp1, 값 exp2인 항목을 추가해요. name = exp 형태의 필드는 ["name"] = exp와 같아요. exp 형태의 필드는 [i] = exp와 같은데, 여기서 i는 1부터 시작하는 연속된 정수예요. 다른 형식의 필드는 이 카운팅에 영향을 주지 않아요. 예를 들어,
a = { [f(1)] = g; "x", "y"; x = 1, f(x), [30] = 23; 45 }
는 다음과 동일해요.
do
local t = {}
t[f(1)] = g
t[1] = "x" -- 1st exp
t[2] = "y" -- 2nd exp
t.x = 1 -- t["x"] = 1
t[3] = f(x) -- 3rd exp
t[30] = 23
t[4] = 45 -- 4th exp
a = t
end
생성자 안의 할당 순서는 정의되어 있지 않아요. (이 순서는 키가 반복될 때만 의미가 있어요.)
리스트의 마지막 필드가 exp 형태이고 그 표현식이 다중 결과(multires) 표현식이라면, 그 표현식이 돌려주는 모든 값이 리스트에 연속으로 들어와요(§3.4.12 참고).
필드 리스트는 선택적인 끝 구분자(trailing separator)를 가질 수 있는데, 이는 기계가 생성한 코드를 위한 편의예요.
3.4.10 함수 호출 (Function Calls)
루아에서 함수 호출의 문법은 다음과 같아요.
functioncall ::= prefixexp args
함수 호출에서는 먼저 prefixexp와 args가 평가돼요. prefixexp의 값이 함수 타입이면 주어진 인자로 그 함수를 호출해요. 아니라면 prefixexp의 __call 메타메서드(존재한다면)가 호출되는데, 첫 번째 인자는 prefixexp의 값, 그다음은 원래 호출 인자들이 이어져요(§2.4 참고).
다음 형태는
functioncall ::= prefixexp ':' Name args
메서드를 흉내 내는 데 사용돼요. v:name(args) 호출은 v.name(v,args)의 문법적 설탕(syntactic sugar)인데, 다만 v는 한 번만 평가된다는 차이가 있어요.
인자의 문법은 다음과 같아요.
args ::= '(' [explist] ')'
args ::= tableconstructor
args ::= LiteralString
모든 인자 표현식은 호출 전에 평가돼요. f{fields} 형태의 호출은 f({fields})의 문법적 설탕이에요. 즉 인자 리스트가 단 하나의 새 테이블인 거죠. f'string' 형태의 호출(또는 f"string", f[[string]])은 f('string')의 문법적 설탕이에요. 즉 인자 리스트가 단 하나의 리터럴 문자열인 거죠.
닫힐 예정 변수(to-be-closed)의 범위 밖에 있는 return functioncall 형태의 호출을 *꼬리 호출(tail call)*이라고 불러요. 루아는 정확한 꼬리 호출(proper tail calls) 또는 *정확한 꼬리 재귀(proper tail recursion)*를 구현해요. 꼬리 호출에서는 호출된 함수가 호출한 함수의 스택 항목을 재사용하므로, 프로그램이 실행할 수 있는 중첩 꼬리 호출 수에 제한이 없어요. 다만 꼬리 호출은 호출한 함수에 대한 디버그 정보를 지워버려요. 꼬리 호출은 특정 문법에서만 일어나는데, return이 하나의 함수 호출만을 인자로 갖고 있고 그것이 닫힐 예정 변수의 범위 밖에 있을 때예요. 이 문법은 호출한 함수가 개입 없이 호출된 함수의 반환 값을 그대로 돌려주게 만드는데, 그래서 다음 예들은 모두 꼬리 호출이 아니에요.
return (f(x)) -- results adjusted to 1
return 2 * f(x) -- result multiplied by 2
return x, f(x) -- additional results
f(x); return -- results discarded
return x or f(x) -- results adjusted to 1
3.4.11 함수 정의 (Function Definitions)
함수 정의의 문법은 다음과 같아요.
functiondef ::= function funcbody
funcbody ::= '(' [parlist] ')' block end
다음 문법적 설탕은 함수 정의를 간단하게 만들어 줘요.
stat ::= function funcname funcbody
stat ::= local function Name funcbody
funcname ::= Name {'.' Name} [':' Name]
문장
function f () body end
은 다음과 같이 번역돼요.
f = function () body end
문장
function t.a.b.c.f () body end
은 다음과 같이 번역돼요.
t.a.b.c.f = function () body end
문장
local function f () body end
은 다음과 같이 번역돼요.
local f; f = function () body end
다음처럼 번역되는 게 아니에요.
local f = function () body end
(이 차이는 함수 본문이 f를 참조할 때만 의미가 있어요.)
함수 정의는 실행 가능한 표현식이며, 그 값의 타입은 함수예요. 루아가 청크(chunk)를 미리 컴파일할 때 그 안의 모든 함수 본문도 함께 미리 컴파일되지만, 아직 생성되지는 않아요. 그다음 루아가 함수 정의를 실행할 때 함수가 인스턴스화(instantiated) 또는 닫혀서(closed) 만들어져요. 이 함수 인스턴스, 즉 *클로저(closure)*가 표현식의 최종 값이에요.
매개변수는 인자 값으로 초기화되는 지역 변수처럼 동작해요.
parlist ::= namelist [',' '...'] | '...'
루아 함수가 호출되면, 가변 인자 함수가 아닌 한 인자 리스트를 매개변수 리스트의 길이에 맞게 조정해요(§3.4.12 참고). 가변 인자 함수는 매개변수 리스트 끝의 점 세 개('...')로 표시돼요. 가변 인자 함수는 인자 리스트를 조정하지 않고, 대신 모든 추가 인자를 모아서 *가변 인자 표현식(vararg expression)*으로 함수에 공급하는데, 이것 역시 점 세 개로 써요. 이 표현식의 값은 실제 추가 인자 전부의 리스트로, 다중 결과를 내는 함수와 비슷해요(§3.4.12 참고).
예를 들어 다음 정의들을 생각해 볼게요.
function f(a, b) end
function g(a, b, ...) end
function r() return 1,2,3 end
그러면 인자에서 매개변수와 가변 인자 표현식으로 가는 매핑은 다음과 같아요.
CALL PARAMETERS
f(3) a=3, b=nil
f(3, 4) a=3, b=4
f(3, 4, 5) a=3, b=4
f(r(), 10) a=1, b=10
f(r()) a=1, b=2
g(3) a=3, b=nil, ... --> (nothing)
g(3, 4) a=3, b=4, ... --> (nothing)
g(3, 4, 5, 8) a=3, b=4, ... --> 5 8
g(5, r()) a=5, b=1, ... --> 2 3
결과는 return 문으로 돌려줘요(§3.3.4 참고). 제어 흐름이 return 문을 만나지 않고 함수 끝에 도달하면, 함수는 결과 없이 반환돼요.
함수가 반환할 수 있는 값의 개수에는 시스템 의존적인 제한이 있어요. 이 제한은 1000보다 크다는 것이 보장돼요.
콜론 문법은 함수에 암묵적인 extra 매개변수 self를 추가하면서 메서드를 흉내 내는 데 쓰여요. 즉 문장
function t.a.b.c:f (params) body end
은 다음의 문법적 설탕이에요.
t.a.b.c.f = function (self, params) body end
3.4.12 표현식 리스트, 다중 결과, 그리고 조정 (Lists of expressions, multiple results, and adjustment)
함수 호출과 가변 인자 표현식은 둘 다 여러 값(multiple values)을 결과로 낼 수 있어요. 이런 표현식을 다중 결과(multires) 표현식이라고 불러요.
다중 결과 표현식이 표현식 리스트의 마지막 요소로 쓰이면, 그 표현식의 모든 결과가 표현식 리스트가 만드는 값 리스트에 더해져요. 표현식 리스트가 기대되는 자리의 단일 표현식은 그 (단일 요소) 리스트에서 마지막 표현식이라는 점을 기억해 두세요.
루아가 표현식 리스트를 기대하는 자리는 다음과 같아요.
return문, 예를 들어return e1, e2, e3(§3.3.4 참고).- 테이블 생성자, 예를 들어
{e1, e2, e3}(§3.4.9 참고). - 함수 호출의 인자, 예를 들어
foo(e1, e2, e3)(§3.4.10 참고). - 다중 할당, 예를 들어
a , b, c = e1, e2, e3(§3.3.3 참고). - 지역 선언, 예를 들어
local a , b, c = e1, e2, e3(§3.3.7 참고). - 일반적인
for루프의 초기 값, 예를 들어for k in e1, e2, e3 do ... end(§3.3.5 참고).
마지막 네 경우에는 표현식 리스트가 만든 값 리스트를 특정 길이로 *조정(adjust)*해야 해요. 가변 인자 함수가 아닌 함수 호출이면 매개변수 개수만큼, 다중 할당이나 지역 선언이면 변수 개수만큼, 일반적인 for 루프면 정확히 네 값으로 조정되죠. 조정 규칙은 이래요. 값이 필요한 수보다 많으면 초과 값을 버리고, 필요한 수보다 적으면 nil로 리스트를 늘려요. 표현식 리스트가 다중 결과 표현식으로 끝나면, 그 표현식의 모든 결과가 조정 전에 값 리스트에 들어와요.
다중 결과 표현식이 표현식 리스트에서 마지막 요소가 아닌 자리, 또는 문법이 단일 표현식을 기대하는 자리에 쓰이면, 루아는 그 표현식의 결과 리스트를 한 요소로 조정해요. 특히 문법은 괄호로 감싼 표현식 안에서는 단일 표현식을 기대하므로, 다중 결과 표현식 주위에 괄호를 추가하면 정확히 하나의 결과를 내도록 강제돼요.
문법이 단일 표현식을 기대하는 자리에서 가변 인자 표현식을 쓸 일은 거의 없어요. (보통 가변 인자 부분 앞에 일반 매개변수를 하나 추가하고 그 매개변수를 쓰는 게 더 간단해요.) 그런 필요가 있다면, 가변 인자 표현식을 단일 변수에 할당하고 그 변수를 그 자리에 쓰는 걸 권장해요.
다중 결과 표현식 사용의 예를 몇 가지 볼게요. 모든 경우에서, 구성이 "n번째 결과"를 필요로 하는데 그 결과가 없으면 nil을 사용해요.
print(x, f()) -- prints x and all results from f().
print(x, (f())) -- prints x and the first result from f().
print(f(), x) -- prints the first result from f() and x.
print(1 + f()) -- prints 1 added to the first result from f().
local x = ... -- x gets the first vararg argument.
x,y = ... -- x gets the first vararg argument,
-- y gets the second vararg argument.
x,y,z = w, f() -- x gets w, y gets the first result from f(),
-- z gets the second result from f().
x,y,z = f() -- x gets the first result from f(),
-- y gets the second result from f(),
-- z gets the third result from f().
x,y,z = f(), g() -- x gets the first result from f(),
-- y gets the first result from g(),
-- z gets the second result from g().
x,y,z = (f()) -- x gets the first result from f(), y and z get nil.
return f() -- returns all results from f().
return x, ... -- returns x and all received vararg arguments.
return x,y,f() -- returns x, y, and all results from f().
{f()} -- creates a list with all results from f().
{...} -- creates a list with all vararg arguments.
{f(), 5} -- creates a list with the first result from f() and 5.
더 알아보기
- 루아 5.4 레퍼런스 매뉴얼 전체 — 본문에서 참조한 §3.1~§3.3, §2.4, §6.4 등 이웃한 구간을 함께 보면 표현식이 문장과 어떻게 맞물리는지 이해하기 쉬워요.
- §3.4.3 강제 변환: 숫자·문자열 사이의 자동 변환 규칙을 다시 짚어볼게요.
- §3.4.12 다중 결과:
return과 다중 할당에서 결과가 어떻게 펼쳐지고 조정되는지의 핵심이에요. - 실제로는
lua인터프리터에서 작은 표현식 몇 개를 직접 실행해 보는 게 우선순위 표와 다중 결과 동작을 체감하는 가장 빠른 길이에요.