10장 파서
10장 파서 (Parser)
이 장은 R이 코드를 어떻게 이해하는지, 즉 화면에 적은 텍스트가 어떻게 컴퓨터가 실행할 수 있는 내부 표현으로 바뀌는지 설명해요. 파서(parser)는 코드의 문자를 읽어 들여 구문 분석(syntax analysis)을 수행하고, 그 결과를 평가기(evaluator)에 넘기는 역할을 합니다. 여기서는 파싱이 일어나는 세 가지 경로와, 토큰(token)·식(expression) 같은 기본 개념, 그리고 값의 저장 방식까지 차근차근 다뤄볼게요.
출처: R 공식 매뉴얼
본문
파서는 R 코드의 문자 표현을 내부 형태로 바꾸는 역할을 해요. 그 내부 형태는 다시 R 평가기에 전달되어 지시된 작업이 실제로 수행되죠. 그리고 이 내부 형태 자체가 하나의 R 객체이기 때문에, 저장하고 조작하는 것도 가능합니다.
10.1 파싱 과정 (The parsing process)
파싱 과정은 크게 세 가지 모드와, 내부 표현, 그리고 역파싱(deparsing)까지 이어지는 흐름으로 볼 수 있어요.
10.1.1 파싱의 모드 (Modes of parsing)
R에서 파싱은 세 가지 방식으로 일어납니다.
- read-eval-print 루프(대화형 명령줄)
- 텍스트 파일의 파싱
- 문자(string)의 파싱
read-eval-print 루프는 R의 기본 명령줄 인터페이스예요. 텍스트 입력은 하나의 완전한 R 식이 될 때까지 계속 읽히고, 식은 여러 줄에 걸쳐 나눠질 수도 있어요. 기본 프롬프트(기본값 > )는 파서가 새 식을 받을 준비가 됐다는 뜻이고, 연속 프롬프트(기본값 + )는 아직 덜 끝난 식의 나머지를 기다린다는 의미예요. 입력 과정에서 식은 내부 형태로 변환되고, 파싱된 식은 평가기에 넘겨진 뒤 그 결과가 출력됩니다(특별히 invisible 처리되지 않는 한). 만약 파서가 언어 문법과 맞지 않는 상태에 빠지면 "Syntax Error"를 표시하고, 스스로 초기화한 뒤 다음 입력 줄의 처음부터 다시 받기 시작해요.
텍스트 파일은 parse 함수로 파싱할 수 있어요. 특히 source 함수를 실행할 때 이 작업이 일어나는데, source는 외부 파일에 저장된 명령을 마치 키보드로 입력한 것처럼 실행해 줍니다. 다만 이 경우 전체 파일이 먼저 파싱되고 문법 검사까지 끝난 뒤에야 평가가 시작된다는 점을 기억해야 해요.
문자열(또는 문자열 벡터)은 parse의 text= 인자를 통해 파싱할 수 있어요. 이 문자열들은 입력 파일의 줄과 똑같이 취급됩니다.
10.1.2 내부 표현 (Internal representation)
파싱된 식은 파스 트리(parse tree)를 담은 R 객체에 저장돼요. 이런 객체에 대한 자세한 설명은 Language objects와 Expression objects에서 볼 수 있고, 간단히 말하면 모든 기본 R 식은 함수 호출 형태로 저장됩니다. 즉 첫 번째 요소가 함수 이름, 나머지가 인자들을 담는 리스트인 셈이죠. 인자들은 다시 또 다른 R 식이 될 수 있어요. 리스트 요소에는 이름을 붙일 수 있고, 이는 형식 인자와 실제 인자의 이름 기반 매칭(tagged matching)과 이어집니다. R의 모든 문법 요소가 이런 방식으로 처리된다는 점이 핵심이에요. 예를 들어 대입문 x <- 1은 "<-"(x, 1)로 인코딩됩니다.
10.1.3 역파싱 (Deparsing)
어떤 R 객체든 deparse를 사용하면 R 식으로 바꿀 수 있어요. 이는 결과를 출력할 때, 예를 들어 그래프에 라벨을 붙일 때 자주 쓰입니다. 다만 유의할 점은, 역파싱한 결과를 다시 파싱했을 때 그대로 유지되는 것이 보장되는 건 오직 "expression" 모드의 객체뿐이라는 거예요. 예를 들어 숫자 벡터 1:5는 "c(1, 2, 3, 4, 5)"로 역파싱되는데, 이건 다시 파싱하면 c 함수 호출이 되어버리죠. 가능한 한, 역파싱 후 재파싱한 식을 평가한 결과가 원래 식을 평가한 결과와 같아지도록 노력하지만, 어색한 예외가 몇 가지 있습니다 — 대부분 처음부터 텍스트 표현에서 만들어진 게 아닌 식들에서요.
10.2 주석 (Comments)
R의 주석은 파서가 무시해요. # 문자부터 그 줄의 끝까지는 주석으로 처리되는데, 단 # 문자가 따옴표로 감싼 문자열 안에 있으면 예외입니다. 예를 들어 볼게요.
> x <- 1 # This is a comment...
> y <- " #... but this is not."
위 코드에서 첫 줄의 # 뒤는 주석이지만, 두 번째 줄의 "#...는 문자열 안에 있으니 주석이 아니에요.
10.3 토큰 (Tokens)
토큰은 프로그래밍 언어의 가장 기본적인 구성 요소예요. 파서가 수행하는 구문 분석보다 앞서서 (개념적으로는) 일어나는 어휘 분석(lexical analysis) 단계에서 토큰이 인식됩니다.
10.3.1 상수 (Constants)
상수에는 정수(integer), 논리(logical), 숫자(numeric), 복소수(complex), 문자열(string)의 다섯 가지 종류가 있어요.
여기에 더해 특별한 상수 NULL이 있고, 숫자형의 Inf, NaN, 논리형의 NA, 그리고 NA_character_, NA_integer_, NA_real_, NA_complex_ 같은 것들도 언급할 만합니다. 이에 대한 내용은 NA handling을 참고해요.
NULL은 빈 객체를 나타내는 데 쓰이고, NA는 "없는(Not Available)" 데이터 값에 사용돼요. Inf는 무한대를 뜻하며, NaN은 IEEE 부동소수점 연산에서 "숫자가 아님(not-a-number)"을 의미합니다(예를 들어 각각 1/0과 0/0의 결과처럼요).
논리 상수는 TRUE, FALSE, NA 중 하나예요.
숫자 상수는 C 언어와 비슷한 문법을 따라요. 0개 이상의 숫자로 이뤄진 정수부, 선택적으로 .과 0개 이상의 숫자로 이뤄진 소수부, 그리고 선택적으로 E 또는 e로 시작하는 지수부(부호와 1개 이상의 숫자)로 구성됩니다. 소수부와 정수부 중 하나는 비어 있어도 되지만, 둘 다 동시에 비면 안 돼요.
Valid numeric constants: 1 10 0.1 .2 1e-7 1.2e+7
숫자 상수는 16진수일 수도 있는데, 0x 또는 0X로 시작하고 뒤에 0개 이상의 숫자, a-f 또는 A-F가 따라옵니다. 16진수 부동소수점 상수는 C99 문법을 지원하는데, 예를 들어 0x1.1p1 같은 형태예요.
이제는 정수 상수라는 별도의 종류도 있어요. 숫자 끝에 L이라는 한정자(qualifier)를 붙이면 만들어집니다. 예를 들어 123L은 숫자 값이 아니라 정수 값을 만들어내죠. L 접미사는 복소수가 아닌 어떤 수에든 붙여 정수를 만들려는 의도로 사용할 수 있어요. 그래서 16진수나 과학적 표기법으로 주어진 수에도 쓸 수 있습니다. 다만 그 값이 유효한 정수가 아니라면 경고가 나오고 숫자 값이 만들어져요. 아래는 유효한 정수 상수, 경고가 나오면서 숫자 상수가 되는 값, 그리고 문법 오류의 예시들입니다.
Valid integer constants: 1L, 0x10L, 1000000L, 1e6L
Valid numeric constants: 1.1L, 1e-3L, 0x1.1p-2
Syntax error: 12iL 0x1.1
불필요한 소수점이 들어간 십진 값, 예를 들어 1.L 같은 경우에는 경고가 발생해요. 그리고 16진수 상수에서 이진 지수 없이 소수점을 쓰는 것은 오류입니다.
또한 앞에 붙는 부호(+ 또는 -)는 상수의 일부가 아니라 단항 연산자로 취급된다는 점도 알아두면 좋아요.
현재 허용되는 형식에 대한 최신 정보는 ?NumericConstants로 확인할 수 있습니다.
복소수 상수는 십진 숫자 상수 뒤에 i가 붙는 형태예요. 여기서 실제 상수인 것은 순수 허수뿐이라는 점을 주의해야 해요. 그 외의 복소수는 숫자와 허수에 대한 단항 또는 이항 연산으로 파싱됩니다.
Valid complex constants: 2i 4.1i 1e-2i
문자열 상수는 한 쌍의 작은따옴표(')나 큰따옴표(")로 감싸며, 그 외의 인쇄 가능한 모든 문자를 담을 수 있어요. 문자열 안의 따옴표나 특수 문자는 이스케이프 시퀀스로 표현합니다.
- ' — 작은따옴표
- " — 큰따옴표
- \n — 줄바꿈(일명 line feed, LF)
- \r — 캐리지 리턴(CR)
- \t — 탭 문자
- \b — 백스페이스
- \a — 벨
- \f — 폼 피드
- \v — 수직 탭
- \ — 백슬래시 자체
- \nnn — 주어진 8진수 코드의 문자 (1~3자리 숫자, 범위
0 ... 7허용) - \xnn — 주어진 16진수 코드의 문자 (1~2자리 16진수,
0 ... 9 A ... F a ... f) - \unnnn \u{nnnn} — (멀티바이트 로케일을 지원할 때만; 그렇지 않으면 오류) 주어진 16진수 코드의 유니코드 문자 (최대 4자리 16진수). 현재 로케일에서 유효한 문자여야 해요.
- \Unnnnnnnn \U{nnnnnnnn} — (멀티바이트 로케일을 지원할 때만; 그렇지 않으면 오류) 주어진 16진수 코드의 유니코드 문자 (최대 8자리 16진수).
작은따옴표는 큰따옴표로 감싼 문자열 안에 바로 들어갈 수 있고, 그 반대도 마찬가지예요.
NUL(\0)은 문자열 안에 허용되지 않아요. 그래서 문자열 상수에서 \0을 쓰면 그 상수가 끝나버립니다(보통 경고와 함께). 닫는 따옴표까지의 나머지 문자들은 스캔은 되지만 무시돼요.
10.3.2 식별자 (Identifiers)
식별자는 문자, 숫자, 마침표(.), 밑줄(_)의 연속으로 이뤄져요. 그런데 숫자나 밑줄로 시작하면 안 되고, 마침표 뒤에 숫자가 오는 형태로 시작해도 안 됩니다.
"문자"의 정의는 현재 로케일에 따라 달라져요. 허용되는 문자 집합은 C 표현식 (isalnum(c) || c == '.' || c == '_')로 주어지며, 서유럽 로케일 여러 곳에서는 발음 구별 기호가 붙은 문자도 포함됩니다.
마침표로 시작하는 식별자는 기본적으로 ls 함수에 목록으로 나타나지 않는다는 점, 그리고 ...과 ..1, ..2 등은 특별하다는 점도 알아두세요.
또한 객체는 식별자가 아닌 이름을 가질 수도 있어요. 이런 객체는 일반적으로 get과 assign으로 접근하지만, 모호함이 없는 한정된 상황에서는 텍스트 문자열로도 표현될 수 있어요 (예: "x" <- 1). get과 assign은 식별자가 아닌 이름에도 제약이 없기 때문에, 서브스크립트 연산자나 치환 함수를 인식하지 못합니다. 아래의 두 표현들은 서로 같지 않아요.
x$a<-1assign("x$a",1)x[[1]]get("x[[1]]")names(x)<-nmassign("names(x)",nm)
10.3.3 예약어 (Reserved words)
다음 식별자들은 특별한 의미를 가지며 객체 이름으로 쓸 수 없습니다.
if else repeat while function for in next break
TRUE FALSE NULL Inf NaN
NA NA_integer_ NA_real_ NA_complex_ NA_character_
... ..1 ..2 etc.
10.3.4 특수 연산자 (Special operators)
R은 사용자가 정의한 **중위 연산자(infix operator)**를 허용해요. 이런 연산자는 % 문자로 감싼 문자들의 연속으로 이뤄진 형태를 가집니다. 그 문자열에는 %를 제외한 모든 인쇄 가능한 문자가 들어갈 수 있고, 문자열의 이스케이프 시퀀스는 여기 적용되지 않아요.
미리 정의된 연산자들은 다음과 같습니다.
%% %*% %/% %in% %o% %x% %||%
10.3.5 분리자 (Separators)
엄밀히 토큰은 아니지만, 공백 문자(스페이스, 탭, 폼 피드, 그리고 Windows와 UTF-8 로케일에서는 그 외 유니코드 공백 문자도)는 모호한 경우 토큰을 구분하는 역할을 해요. x<-5와 x < -5를 비교해 보면 그 차이가 확실해지죠.
줄바꿈은 토큰 분리자와 식 종결자(expression terminator)의 결합 기능을 해요. 식이 줄의 끝에서 끝날 수 있다면 파서는 그렇게 끝난다고 가정하고, 그렇지 않으면 줄바꿈을 공백으로 취급합니다. 세미콜론(;)은 같은 줄의 기본 식들을 구분하는 데 쓸 수 있어요.
else 키워드에는 특별한 규칙이 적용됩니다. 복합 식 안에서는 else 앞의 줄바꿈이 버려지지만, 최외곽 수준에서는 줄바꿈이 if 구문을 끝내고 그 뒤에 오는 else는 문법 오류를 일으켜요. 이 다소 이상해 보이는 동작은 R이 대화형 모드에서 사용 가능해야 하기 때문에 생기는 건데요. 사용자가 Enter를 누르는 순간 입력 식이 완전한지, 불완전한지, 아니면 잘못됐는지를 결정해야 하기 때문이에요.
쉼표(,)는 함수 인자와 여러 인덱스를 구분하는 데 사용됩니다.
10.3.6 연산자 토큰 (Operator tokens)
R은 다음의 연산자 토큰들을 사용해요.
+ - * / %% %/% ^arithmetic (산술) > >= < <= == !=relational (관계) `! & ` ~model formulae (모델 공식) -> <-assignment (대입) $list indexing (리스트 인덱싱) :sequence (수열)
(이 연산자들 중 몇몇은 모델 공식 안에서 다른 의미를 가집니다.)
10.3.7 그룹화 (Grouping)
보통의 괄호, 즉 (와 )는 식 안에서 명시적 그룹화와, 함수 정의 및 함수 호출의 인자 목록을 구분하는 데 쓰여요.
중괄호, 즉 {와 }는 함수 정의, 조건식, 반복 구문에서 여러 식의 블록을 구분합니다.
10.3.8 인덱싱 토큰 (Indexing tokens)
배열과 벡터의 인덱싱은 단일 대괄호 []와 이중 대괄호 [[]]로 수행돼요. 그리고 태그가 붙은 리스트의 인덱싱은 $ 연산자로 할 수 있습니다.
10.4 식 (Expressions)
R 프로그램은 일련의 R 식으로 구성돼요. 식은 상수나 식별자만으로 이뤄진 단순 식일 수도 있고, 다른 부분(그 자체로 식이 될 수 있는)들로 만들어지는 복합 식일 수도 있습니다.
아래 절들에서 사용 가능한 다양한 구문 구조들을 자세히 다룰게요.
10.4.1 함수 호출 (Function calls)
함수 호출은 함수 참조 뒤에 괄호 안의 쉼표로 구분된 인자 목록이 이어지는 형태를 가집니다.
function_reference ( arg1, arg2, ...... , argn )
함수 참조는 다음 중 하나가 될 수 있어요.
- 식별자 (함수의 이름)
- 텍스트 문자열 (마찬가지지만, 함수 이름이 유효한 식별자가 아닐 때 유용함)
- 식 (함수 객체로 평가되어야 함)
각 인자는 태그를 붙일 수 있고(tag=expr), 그냥 단순 식일 수도 있어요. 또 비어 있을 수도 있고, 특수 토큰인 ..., ..2 등일 수도 있습니다.
태그는 식별자 또는 텍스트 문자열이 될 수 있어요.
예시:
f(x)
g(tag = value, , 5)
"odd name"("strange tag" = 5, y)
(function(x) x^2)(5)
10.4.2 중위 및 전위 연산자 (Infix and prefix operators)
연산자의 우선순위(높은 것부터)는 다음과 같습니다.
::
$ @
^
- + (unary)
:
%xyz% |>
* /
+ - (binary)
> >= < <= == !=
!
& &&
| ||
~ (unary and binary)
-> ->>
<- <<-
= (as assignment)
:이 이항의 +/-보다는 앞서지만 ^보다는 뒤에 있다는 점에 주의하세요. 그래서 1:3-1은 0 1 2가 되지만, 1:2^3은 1:8이 됩니다.
거듭제곱 연산자 ^와 왼쪽 대입 계열 연산자들(<- - = <<-)은 오른쪽에서 왼쪽으로 결합하고, 그 외의 연산자는 모두 왼쪽에서 오른쪽으로 결합해요. 즉 2 ^ 2 ^ 3은 4^3이 아니라 2^8이고, 1 - 1 - 1은 1이 아니라 -1입니다.
정수 나머지와 나눗셈을 위한 %%와 %/% 연산자는 곱셈·나눗셈보다 우선순위가 높다는 점도 알아두세요.
엄밀히 연산자는 아니지만, = 기호가 함수 호출의 인자 태그를 붙이고 함수 정의의 기본값을 지정하는 데 쓰인다는 점도 언급할 필요가 있어요.
$ 기호는 어떤 의미에서 연산자이긴 하지만 임의의 오른쪽 피연산자를 허용하지 않으며, 인덱스 구조에서 다루게 됩니다. 그리고 다른 어떤 연산자보다 우선순위가 높아요.
단항 또는 이항 연산의 파싱된 형태는, 연산자가 함수 이름이고 피연산자가 함수 인자인 함수 호출과 완전히 동등합니다.
괄호는 이름이 "("인 단항 연산자와 동등한 것으로 기록됩니다. 연산자 우선순위에서 괄호를 유추할 수 있는 경우에도 마찬가지예요 (예: a * (b + c)).
대입 기호도 산술·관계·논리 연산자처럼 그냥 하나의 연산자라는 점을 주목하세요. 파서의 관점에서는 대입의 왼쪽(대상)에도 어떤 식이든 허용됩니다 (2 + 2 <- 5는 파서 관점에서 유효한 식이에요. 다만 평가기는 반대할 거예요). 모델 공식 연산자에도 비슷한 이야기가 적용됩니다.
10.4.3 인덱스 구조 (Index constructions)
R에는 세 가지 인덱싱 구조가 있는데, 그중 둘은 문법적으로 비슷하지만 의미는 다소 다릅니다.
object [ arg1, ...... , argn ]
object [[ arg1, ...... , argn ]]
object는 형식적으로는 어떤 유효한 식이든 될 수 있지만, 부분집합(subsetting)이 가능한 객체를 가리키거나 그렇게 평가된다고 이해해야 해요. 인자들은 일반적으로 숫자 또는 문자 인덱스로 평가되지만, 다른 종류의 인자도 가능합니다 (특히 drop = FALSE 같은 것요).
내부적으로 이 인덱스 구조들은 함수 이름이 각각 "["와 "[["인 함수 호출로 저장됩니다.
세 번째 인덱스 구조는 다음과 같아요.
object $ tag
여기서 object는 위와 같고, tag는 식별자 또는 텍스트 문자열이에요. 내부적으로는 이름이 "$"인 함수 호출로 저장됩니다.
10.4.4 복합 식 (Compound expressions)
복합 식은 다음 형태를 가집니다.
{ expr1 ; expr2 ; ...... ; exprn }
세미콜론은 줄바꿈으로 대체될 수 있어요. 내부적으로 이것은 함수 이름이 "{"이고 식들이 인자인 함수 호출로 저장됩니다.
10.4.5 흐름 제어 요소 (Flow control elements)
R은 특별한 구문 구조로 다음의 제어 구조들을 가지고 있어요.
if ( cond ) expr
if ( cond ) expr1 else expr2
while ( cond ) expr
repeat expr
for ( var in list ) expr
이 구조들 안의 식은 보통 복합 식이 됩니다.
루프 구조(while, repeat, for) 안에서는 break(루프를 끝내기 위해)와 next(다음 반복으로 건너뛰기 위해)를 쓸 수 있어요.
내부적으로 이 구조들은 함수 호출로 저장됩니다.
"if"(cond, expr)
"if"(cond, expr1, expr2)
"while"(cond, expr)
"repeat"(expr)
"for"(var, list, expr)
"break"()
"next"()
10.4.6 함수 정의 (Function definitions)
함수 정의는 다음 형태를 가집니다.
function ( arglist ) body
함수 본문은 하나의 식이며, 보통 복합 식이에요. arglist는 쉼표로 구분된 항목들의 목록인데, 각 항목은 식별자, 또는 identifier = default 형태, 또는 특수 토큰 ...가 될 수 있어요. default는 어떤 유효한 식이든 가능합니다.
함수 인자는 리스트 태그 등과 달리 텍스트 문자열로 "이상한 이름"을 가질 수 없다는 점에 유의하세요.
내부적으로 함수 정의는 함수 이름이 function이고 인자가 두 개(arglist와 body)인 함수 호출로 저장됩니다. arglist는 태그가 인자 이름이고 값이 기본 표현인, 태그가 붙은 페어리스트(pairlist)로 저장돼요.
10.5 지시어 (Directives)
파서는 현재 하나의 지시어 #line만 지원합니다. 이는 같은 이름의 C 전처리기 지시어와 비슷한데, 문법은 다음과 같아요.
#line nn [ "filename" ]
여기서 nn은 정수 줄 번호이고, 선택 사항인 filename(반드시 큰따옴표로 감싸야 함)은 원본 파일을 가리킵니다.
C 지시어와 달리, #line은 줄의 첫 다섯 글자로 나타나야 해요. C에서처럼 nn과 "filename" 항목은 공백으로 구분될 수 있고, C와 달리 그 줄에 이어지는 모든 텍스트는 주석으로 처리되어 무시됩니다.
이 지시어는 파서에게 다음 줄이 파일 filename의 nn번째 줄이라고 가정하라고 알려줍니다. (파일 이름이 주어지지 않으면 이전 지시어와 같은 파일로 가정해요.) 사용자가 직접 쓰는 경우는 드물지만, 전처리기가 진단 메시지가 원본 파일을 가리키도록 하기 위해 쓸 수 있어요.
더 알아보기
- R 공식 매뉴얼의 다른 챕터에서 파서와 맞닿는 개념을 이어서 볼 수 있어요. 특히 파싱 결과로 생성되는 객체의 내부 구조는 6장 "Computing on the language"에서, 함수와 인자 평가는 4장 "Functions"에서 더 자세히 다룹니다.
- 실제 입력 형식의 최신 사항은 R 콘솔에서
?NumericConstants,?Quotes,?Reserved도움말을 확인해 보세요.