\usepackage{expl3}이라고 써도 지금의 LaTeX에서는 아무것도 읽히지 않습니다. expl3 —— 한때 “LaTeX3”라 불리던 작업에서 나온 LaTeX의 프로그래밍 계층 —— 은 이미 포맷의 일부이기 때문입니다. 실제로 커널 소스 latex.ltx의 1125행에는 Skipping: expl3 code already part of the format이라는 로그 출력이 있고, expl3.sty는 53행에서 “이미 들어 있는가”를 확인해 들어 있으면 코드 읽기를 통째로 버립니다. 이 페이지에서는 \ExplSyntaxOn이 문자의 취급을 어떻게 바꾸는지 실측하고, \module_function:nn이라는 독특한 이름을 읽는 법, 인수 지정자, 그리고 tl, seq, prop, int, fp 같은 자료형을 차례로 봅니다.
expl3는 이미 커널 안에 있습니다 — \fpeval이 그 증거입니다
TeX은 본래 매크로 처리기로, \def나 \newcommand로 명령을 정의해 가는 도구입니다. 그러나 큰 패키지를 쓰려 하면 날것의 프리미티브는 일관성이 없고, 전개 제어와 변수 처리가 장인의 기술이 되었습니다. expl3는 TeX과 e-TeX의 프리미티브에 새 이름을 주고, 함수와 변수를 체계적으로 이름 붙이며, 각 인수의 타입을 이름 자체에 적어 넣는 일관된 인터페이스입니다. LaTeX Project가 오랜 세월에 걸쳐 정비한, 말하자면 LaTeX을 위한 표준 라이브러리이자 프로그래밍 언어이며, xparse・siunitx・fontspec・l3keys2e가 모두 그 위에 서 있습니다.
그리고 그 계층은 더 이상 별매가 아닙니다. expl3.sty를 열면 53행부터 59행에 걸쳐 \tex_let:D가 이미 정의되어 있는지 살피고, 정의되어 있으면 \input expl3-code.tex를 \@gobble로 통째로 삼켜 버리는 몇 줄이 보입니다. 패키지로 남은 것은 이름뿐인 껍데기이며(자기 신고도 L3 programming layer (loader)입니다), 알맹이는 포맷 안에 있습니다. 실제로 아무것도 읽지 않은 맨 article에서도 \ExplSyntaxOn도 \tl_new:N도 \ProvidesExplPackage도 모두 정의되어 있습니다. 가장 알기 쉬운 증거는 latex.ltx 1167행부터의 네 줄입니다. \fpeval, \inteval, \dimeval, \skipeval이 \cs_new_eq:NN으로 \fp_eval:n, \int_eval:n, \dim_eval:n, \skip_eval:n의 별칭으로 정의되어 있습니다. 곧 문서 수준의 \fpeval{sqrt(2)}는 expl3의 함수 자체를 다른 이름으로 부르는 것일 뿐입니다(카운터와 길이 페이지 참조).
\ExplSyntaxOn이 실제로 바꾸는 것 — 공백과 ~의 교대
expl3 코드는 \ExplSyntaxOn과 \ExplSyntaxOff로 둘러싼 구간에 씁니다. 이 구간에서 바뀌는 것은 정확히 네 문자의 카테고리 코드뿐입니다. 커널에 \the\catcode를 찍게 해 앞뒤를 비교하면 이렇습니다. 공백은 10(공백)에서 9(무시되는 문자)로, _는 8(아래 첨자)에서 11(문자)로, :는 12(기타)에서 11(문자)로, 그리고 ~는 13(액티브)에서 10(공백)으로 바뀝니다. \ExplSyntaxOff를 쓰면 10・8・12・13으로 정확히 돌아옵니다. 카테고리 코드 자체는 카테고리 코드 페이지가 자세하지만, 여기서 재미있는 것은 공백과 ~가 자리를 바꿨다는 점입니다. 공백은 카테고리 코드 10을 내주고 “무시되는 문자”인 9번으로 옮겨 가고, 비어 있는 10번에 ~가 앉습니다. 그래서 expl3 안에서 ~는 “공백을 내는 명령”이 아니라 말 그대로 공백 문자 그 자체입니다.
| 문자 | 보통의 LaTeX | ExplSyntaxOn 구간 안 |
|---|---|---|
(space) | 10 — 공백 | 9 — 무시되는 문자. 들여쓰기도 줄바꿈도 출력에 전혀 영향을 주지 않습니다 |
~ | 13 — 액티브(비분리 공백) | 10 — 공백 그 자체. 출력에 여백을 넣고 싶을 때 이것을 씁니다 |
_ | 8 — 아래 첨자 | 11 — 문자. 명령 이름 안에 쓸 수 있습니다 |
: | 12 — 기타 문자 | 11 — 문자. 인수 시그니처의 구분에 쓸 수 있습니다 |
이 교대가 실무에서 갖는 의미는 하나입니다. expl3 안에서 맨 공백을 써도 출력에는 나오지 않습니다. “Fruit: apple”을 조판하려면 Fruit:~#1이라고 써야 하고, ~를 맨 공백으로 바꾸면 “Fruit:apple”로 붙어 버립니다. 뒤집어 말하면, 코드를 얼마든지 들여쓰고 토큰 사이에 여백을 넣어 읽기 좋게 써도 출력은 전혀 달라지지 않는다는 뜻입니다. 보통의 LaTeX이 공백에 신경을 쓰는 언어임을 생각하면 이것은 상당한 해방입니다.
% no \usepackage{expl3} is needed: it is in the format
\ExplSyntaxOn
% spaces and newlines here are catcode 9 (ignored); _ and : are letters
\tl_new:N \l_greeting_tl
\tl_set:Nn \l_greeting_tl { Hello,~world! } % ~ is the real space
\tl_use:N \l_greeting_tl
\ExplSyntaxOff\seq_put_right:Nn을 읽는 법 — 이름이 타입을 말합니다
expl3의 함수 이름은 \⟨module⟩_⟨description⟩:⟨arg-signature⟩ 형태입니다. 첫 _까지가 모듈 이름(자료형이나 기능 분류), : 앞까지가 설명적인 이름, : 뒤가 인수 시그니처입니다. \seq_put_right:Nn이라면 모듈은 seq(시퀀스), 설명은 put_right(오른쪽 끝에 추가), 시그니처는 Nn입니다. 시그니처의 각 문자는 그 자리의 인수를 넘기기 전에 어떻게 처리할지를 나타냅니다. 곧 이름만 읽으면 인수를 몇 개 받고 각각을 어떻게 다루는지 알 수 있습니다. 문서를 찾지 않아도 타입을 읽어 낼 수 있다는 것이 이 명명 규칙의 목적입니다.
| 지정자 | 의미 |
|---|---|
N | 가공하지 않음. 단일 토큰(대개 제어철 하나). |
n | 가공하지 않음. 중괄호로 묶은 토큰열. |
c | 내용을 \csname으로 제어철로 변환한 뒤 넘김. |
V / v | 변수의 값을 꺼내 넘김(V는 단일 토큰, v는 이름을 먼저 구성). |
o | 인수를 한 번만 전개한 뒤 넘김. |
x / e | 완전 전개(x는 \edef 유사이며 전개 불가, e는 \expanded 사용). |
f | 왼쪽에서 오른쪽으로 첫 번째 전개 불가능 토큰까지 전개. |
p | TeX의 매개변수 텍스트(#1#2…). 함수를 정의할 때 사용. |
T / F | 조건이 참일 때 / 거짓일 때 실행할 코드. \tl_if_empty:nTF처럼 TF로 짝짓는 일이 많고, T만・F만인 판도 있습니다. |
변수도 같은 방식으로 이름 붙이지만, 앞에 스코프를 나타내는 한 글자가 붙습니다. l_은 지역(현재 TeX 그룹 안에서만 변경), g_는 전역, c_는 상수입니다. 끝에는 타입 식별자가 붙어 _tl(토큰 리스트), _int(정수), _seq(시퀀스), _prop(프로퍼티 리스트), _clist(콤마 리스트), _fp(부동소수점), _str(문자열), _bool(참거짓) 등이 됩니다. 따라서 \l_my_name_tl은 “지역 토큰 리스트 변수”, \g_counter_int는 “전역 정수 변수”로 한눈에 읽힙니다. 각 모듈은 \l_tmpa_tl, \l_tmpb_int 같은 스크래치 변수(쓰고 버리는 임시 변수)도 마련해 둡니다.
:Nn과 :Nx의 차이를 눈으로 보기
n은 쓴 것을 그대로, x는 “지금”의 값을 구워 넣어 저장합니다. 추상적으로 설명하기보다 \tl_show:N으로 내용을 로그에 찍게 하는 편이 빠릅니다. 같은 { [ \l_src_tl ] }을 \tl_set:Nn과 \tl_set:Nx로 두 변수에 넣으면 로그에는 이렇게 나옵니다. > \l_a_tl=[\l_src_tl ].와 > \l_b_tl=[FIRST].. n 판은 \l_src_tl이라는 토큰을 그대로 지니고, x 판은 대입 시점의 값 FIRST로 바뀌어 있습니다. 이후 \l_src_tl을 SECOND로 바꾸고 둘을 출력하면 n 판은 “[SECOND]”, x 판은 “[FIRST]”가 됩니다. 곧 n은 참조를, x는 스냅숏을 저장합니다. 전개 제어가 어렵다고 말하는 상황의 대부분은 이 선택을 거꾸로 한 것입니다. \tl_show:N은 막혔을 때의 첫 수로 기억해 둘 가치가 있습니다.
\ExplSyntaxOn
\tl_new:N \l_src_tl \tl_set:Nn \l_src_tl { FIRST }
\tl_new:N \l_a_tl \tl_set:Nn \l_a_tl { [ \l_src_tl ] } % reference
\tl_new:N \l_b_tl \tl_set:Nx \l_b_tl { [ \l_src_tl ] } % snapshot
\tl_show:N \l_a_tl % log: > \l_a_tl=[\l_src_tl ].
\tl_show:N \l_b_tl % log: > \l_b_tl=[FIRST].
\tl_set:Nn \l_src_tl { SECOND }
Nn~stored:~\tl_use:N \l_a_tl \par % prints [SECOND]
Nx~stored:~\tl_use:N \l_b_tl \par % prints [FIRST]
\ExplSyntaxOff자료형 — 만들기, 설정하기, 사용하기
expl3는 자료형별 모듈로 나뉘며, 모두 “만들기, 설정하기, 사용하기”라는 같은 리듬으로 다룰 수 있습니다. \⟨type⟩_new:N으로 선언하고, \⟨type⟩_set:Nn으로 값을 넣고, \⟨type⟩_use:N 같은 것으로 꺼냅니다. 형이 달라져도 손놀림이 달라지지 않으므로 하나를 익히면 나머지는 짐작이 됩니다. 함수 정의만 형태가 조금 다릅니다. \cs_new:Npn은 새 함수를 정의하고, 같은 이름의 함수가 이미 있으면 오류를 냅니다(cs는 control sequence). \cs_set:Npn도 정의하지만 현재 TeX 그룹 안에 한정되고, 재정의해도 오류가 나지 않습니다. 둘 다 :Npn을 쓰며 N=정의할 함수 이름, p=매개변수 텍스트(#1#2…), n=본문(치환 텍스트)을 뜻합니다.
| 모듈 | 다루는 것 | 자주 쓰는 함수와 실측값 |
|---|---|---|
tl | 토큰 리스트. 문자열처럼 쓸 수 있는 가장 기본적인 변수 | \tl_new:N / \tl_set:Nn(이전 내용은 사라짐) / \tl_use:N / \tl_show:N |
str | 문자열. 토큰이 아니라 문자로 비교하고 싶을 때 | \str_if_eq:nnTF { a~b } { a~b }는 same을 돌려줍니다 |
int | 정수. 사칙과 나머지의 정수 연산 | \int_eval:n { 2 + 3 * 4 }는 14(\inteval이 그 별칭) |
fp | 부동소수점. sin, sqrt, pi 같은 과학 함수도 쓸 수 있습니다 | \fp_eval:n { sqrt(2) }는 1.414213562373095, \fp_eval:n { 2 * pi }는 6.283185307179586 |
seq | 시퀀스. 양끝에서 넣고 뺄 수 있는 리스트(스택으로도 사용) | \seq_put_right:Nn으로 추가, \seq_map_inline:Nn으로 모든 요소를 #1로 순회, \seq_use:Nn으로 구분자를 넣어 연결 |
prop | 프로퍼티 리스트. 이른바 딕셔너리(키와 값의 대응표) | \prop_put:Nnn으로 “변수・키・값”을 저장, \prop_item:Nn으로 꺼냄 |
clist | 쉼표로 구분된 값들의 나열 | \clist_set:Nn \l_c_clist { 1,2,3 } 뒤에 \clist_use:Nn \l_c_clist { ~+~ }는 “1 + 2 + 3” |
bool | 참거짓 값. \newif 대신 씁니다 | \bool_new:N / \bool_set_true:N / \bool_if:NTF로 분기 |
\documentclass{article}
\begin{document}
\ExplSyntaxOn
\seq_new:N \l_example_fruits_seq
% define a function that appends one item
\cs_new:Npn \example_add:n #1
{ \seq_put_right:Nn \l_example_fruits_seq {#1} }
\example_add:n { apple }
\example_add:n { banana }
\example_add:n { cherry }
% each item arrives as #1; ~ is a real space
\seq_map_inline:Nn \l_example_fruits_seq
{ Fruit:~#1 \par }
\ExplSyntaxOff
\end{document}이것을 컴파일하면 “Fruit: apple”, “Fruit: banana”, “Fruit: cherry”가 세 줄로 나옵니다. \cs_new:Npn의 #1은 정의되는 함수의 인수이고, \seq_map_inline:Nn의 #1은 순회 중인 각 요소이며, 둘 다 n 타입(중괄호 내용)으로 취급됩니다. 명령 이름에 _와 :가 들어 있어도 괜찮은 것은 바로 \ExplSyntaxOn 구간이기 때문입니다.
l3keys와 패키지를 쓸 때의 사용법
key = value 형식의 옵션을 자기 명령이나 패키지에 달고 싶을 때의 표준은 l3keys입니다. \keys_define:nn { module } { ... }로 키를 선언하고 \keys_set:nn { module } { key = value }로 설정합니다. 키 선언에는 .tl_set:N(토큰 리스트 변수에 넣기), .bool_set:N(참거짓으로 만들기), .code:n(임의의 코드 실행), .initial:n(기본값) 같은 프로퍼티를 뒤에 붙입니다. 이 장치는 본래 l3keys2e라는 패키지를 거쳐 패키지 옵션에 이어졌지만, 그 핵심은 현재 LaTeX2ε 커널에 흡수되어 패키지 없이 \DeclareKeys와 \ProcessKeyOptions로 쓸 수 있습니다(패키지 작성 페이지에서 실제로 써 보입니다).
보통의 문서 작성에서 expl3를 직접 쓸 일은 거의 없습니다. 하지만 패키지나 클래스를 만드는 단계가 되면 expl3는 이제 사실상의 표준입니다. 사용자용 명령은 \NewDocumentCommand(xparse)로 받고 그 내부를 expl3로 구현하는 조합이 자주 쓰입니다. 두 가지를 구분해 두세요. xparse의 인수 지정(m, O{...}, s 같은 문서 수준 인수)과 expl3의 인수 시그니처(N, n 같은 프로그래밍 수준 처리 지정)는 다른 것입니다. 앞의 것은 xparse 페이지에서 자세히 다룹니다. 또한 .sty의 첫 줄을 \ProvidesExplPackage로 하면 그 시점부터 자동으로 expl3 문법이 됩니다. \ExplSyntaxOn을 쓸 필요가 전혀 없습니다.
- 코드는
\ExplSyntaxOn…\ExplSyntaxOff로 감쌉니다. 구간 안에서 맨 공백은 카테고리 코드 9(무시)가 되며, 출력에 여백을 넣고 싶을 때는~를 씁니다. \usepackage{expl3}는 필요 없습니다. 커널에 들어 있고expl3.sty는 읽기를 스스로 버립니다..sty에서는\ProvidesExplPackage를 쓰면\ExplSyntaxOn도 필요 없습니다.- 함수는
\⟨module⟩_⟨description⟩:⟨signature⟩, 변수는\⟨scope⟩_⟨name⟩_⟨type⟩입니다. 스코프는l_/g_/c_. n과x를 혼동하지 마세요. 참조를 저장할지, 지금 값을 구워 넣을지에 따라 고릅니다. 망설여지면\tl_show:N으로 로그에 찍어 확인합니다.- 확신 없는 명령 이름을 만들지 않습니다. 명명은 엄격하며, 공식 interface3 매뉴얼(
texdoc interface3)이 1차 자료입니다.