TeX 言語者のための expl3 入門

2018-04-28 (updated: 2021-12-12)  #expl3  #TeX 

TeX 言語は,組版用の言語としては今のところデファクト・スタンダードな地位にありますが,周知の通りお世辞にも「読み書きしやすい言語」ではありません.そのため最近は TeX ではない別の言語体系・処理系を提案して,より幸せに組版・開発を行う流れもありますが,一方でかつてより既存の TeX 処理系の上で動作する「マトモなプログラミング言語」の開発も行われています

その代表格として挙げられるのが expl3 ですが,残念ながら今のところ日本語の文献はかなり不足しています.昨年開催された TeXConf 2017 で私は『expl3 入門』という講演を行い,その発表資料も公開していますが,スライドだけでは不十分と思われるので,今回は改めてその講演内容に沿った内容をブログ記事の形でまとめてみようと思います.

なお,上記の講演資料と本稿の両者に共通する注意事項として,想定読者は既に TeX 言語を習得している方です.いずれは一切の事前知識を要求しない文章を書きたいと思っておりますが,現時点では一定レベルの「TeX 言語の知識」を仮定させていただくことを予めご了承ください.

expl3 とは

expl3 は LaTeX3 チームが開発を行っているプログラミング言語です.TeX マクロとして実装されていて,2017年現在日本で使用されているほとんどの TeX 処理系で動作します1.expl3 は元々 “experimental” に由来していましたが,今はやや強引ながら “LaTeX3 Programming Language” の略ということになっており,読み方は「エクスペル・スリー」(x-pel-3)です.

主な特徴

expl3 は TeX 言語がもつ問題点を軽減し,より高い可読性と安全性をもつように設計されています.具体的には,expl3 には以下に挙げるような TeX 言語にはない特徴があります.

分類と命名規則

TeX 言語では,何らかの「操作を行うモノ」と「データを格納するモノ」がいずれも「マクロ」という形でひとまとめに扱われています.expl3 ではこれらはそれぞれ「関数」と「変数」に呼び分けられ,シンタックスの上でも明確に区別されています.

  • 関数:引数を取ることができ,展開または実行される
  • 変数:値を代入でき,関数の引数として利用される

expl3 がプリミティブに提供する関数と変数は,機能や役割に応じて「モジュール」という単位に分類されています.また変数に対しては,格納するデータの種類に応じた「型」が定義されています.関数および変数の命名規則もこうしたモジュールや型に基いて,体系的なものになっています2

展開制御

展開制御はある意味において TeX 言語の醍醐味の1つですが,一般には TeX 言語を学習したり,他人の書いた TeX 言語コードを読み解いたりする上で大きな障壁として認識されてきました.expl3 も TeX 上に実装されているため「展開制御」の概念自体はなくなりませんが,これをある程度簡単に記述するための機構が導入されています.

充実した公式ドキュメント

TeX on LaTeX の大きな問題点の1つは,LaTeX の内部マクロについて(公式の)十分なドキュメントが用意されていないことです.これに対して expl3 については,LaTeX3 チームがすべての関数について詳細なドキュメントを作成し,誰でも無料で読めるようになっています(ただし,当然のことながら英語です).

TeX Live の利用者であれば,次のようにすると expl3 の公式リファレンスを簡単に開くことができます34

$ texdoc interface3

expl3 のキホン

expl3 の文法解説を始める前に,通常の LaTeX2e ソースで expl3 を使用する方法を確認しておきます.これはいたって簡単で \usepackage{expl3} によって expl3 パッケージを読み込むだけで準備が完了します5.expl3 パッケージによって \ExplSyntaxOn\ExplSyntaxOff が定義されるので,これらの間で自由に expl3 コードを書くことができるようになります.

何はともあれ,まずはお決まりの Hello World 的なプログラムを動かしてみましょう.

\documentclass{article}
\usepackage{expl3}
\begin{document}

\ExplSyntaxOn

\cs_new:Npn \my_hello:n #1 {
  Hello~#1!
}

\my_hello:n { expl3 }

\ExplSyntaxOff

\end{document}

Hello expl3!

シンタックス

expl3 のシンタックスは TeX on LaTeX とそれほど大きくは違いませんが,2つほど重要な違いがあります.1つ目は,英文字に加えて _: を制御綴に用いることができる点です.TeX on LaTeX では(\makeatletter な環境下で)@ を制御綴に使用しましたが,expl3 では @ を制御綴に用いることは基本的にはありません.

もう1つの重要な違いは,半角スペースはすべて無視されるという点です.expl3 コード中で「空白」が必要な場合は明示的に ~ を書きます.このことだけを考えると不便なようですが,この性質のおかげで TeX on LaTeX コーディングでは神経質に気を使う必要のあった「行末のコメントアウト」をする必要がなくなります.

変数のカタチ

さて,先述したように expl3 には関数と変数の区別がありますが,その違いはシンタックス(見た目)の上でも明確になっています.

expl3 における変数は,形式的には以下のような形をしています.

\〈スコープ〉_〈モジュール〉_〈説明〉_〈型〉

expl3 の変数も関数も実体は TeX の制御綴なので,もちろん最初は \ で始まり,そこから〈スコープ〉・〈モジュール〉・〈説明〉・〈型〉が _ 区切りで連結されます.

まず〈スコープ〉は,その変数にアクセスできる範囲を示す1文字で,具体的には

  • g: グローバル変数
  • l: ローカル変数
  • c: 定数

の3種のうちのいずれかです.当然のことながら,グローバル変数にはグローバルな代入,ローカル変数にはローカルな代入を行うことになります6.定数は一度定義したら以降値を変更すべきでない変数で,定義されていればどこからでも参照することができます.別の言い方をすると,値を変更されないグローバル変数,ぐらいに理解できます.

続く〈モジュール〉は既に説明したように,変数と関数を役割・機能ごとにまとめる単位で,他の言語でいう名前空間に相当する文字列です.モジュール名は英文字(アルファベット)のみで構成されている必要があり,_ などを含めることはできません.基本的なモジュールは expl3 のカーネルで定義されていますが,必要ならパッケージ作成時に自分で作成することもできます7

変数の3つ目の構成要素〈説明〉は,変数の名称・役割などを表す文字列で,ここには英文字と _ を含めることができます

最後の〈型〉はデータの種類を表す文字列で,expl3 標準の型は下図に示す15種類です.

説明 説明 説明
bool 真偽値 int 整数 fp 浮動小数点数
str 文字列 ior 入力 iow 出力
dim 厳格な寸法 skip 伸縮可能な寸法 muskip 数式用の寸法
box ボックス coffin 高級なボックス tl トークンリスト
clist カンマ区切りリスト seq シークエンス prop プロパティリスト

ここで,いくつか具体的な変数の例を挙げてみます.

  1. \l_scfoo_number_of_buttons_int
  2. \c_pi_fp

1つ目の例 \l_scfoo_number_of_buttons_int は模範的な expl3 の変数で

  • l が〈スコープ〉
  • scfoo が〈モジュール名〉
  • number_of_buttons が〈説明〉
  • int が〈型〉

です.変数を見るだけで「scfoo モジュールに属し『ボタンの数』を表すローカルで int 型の変数」ということがわかります.

ところで,expl3 カーネルが標準で提供している定数やユーザ用一時変数では,一部モジュール名を含まないものもあります.2つ目に挙げた \c_pi_fp がそれで,$\pi$ の値を格納している fp 型の定数です.こうした定数・変数は,特にパッケージなどを作成する場合には自分では定義しない方がよいでしょう.

関数のカタチ

続いて expl3 の関数は,形式的には以下のような形をしています.

\〈モジュール〉_〈説明〉:〈引数指定〉

関数も実体は TeX マクロなので,変数と同様 \ に始まり _ で連結された〈モジュール〉・〈説明〉が続き,最後は : と〈引数指定〉という構成になっています.

関数についても,〈モジュール〉と〈説明〉の部分の制約と役割は変数の場合とまったく同じです.

最後の〈引数指定〉だけは関数に特有で,それぞれの関数がとる引数の形を指定するもので「引数指定子を引数の数だけ並べたもの」です.主な引数指定子には以下のようなものがあります.

  • n: LaTeX における普通の引数 {foo}
  • N: 単一トークン(典型的には制御綴)
  • p: パラメタテキスト
  • T, F: 真偽に応じて利用される特殊な n
  • w: 例外的なもの

なお,引数を1つもとらない関数の場合は〈引数指定〉は空文字列となります(例:\scan_stop:).また,展開制御を行う特殊な引数指定子も存在します(後述).

変数と関数のカタチについて説明したので,少し実際のコードを覗いて見ることにしましょう.先に挙げた Hello World コードの中では \cs_new:Npn という関数が使用されていました(都合により改行は除いています).

\cs_new:Npn \my_hello:n #1 { Hello~#1! }

これは TeX 言語における \def に相当する関数で,新しい関数を定義する関数です.引数は〈引数指定〉Npn の形からわかるように3つで,上のコード例では

  • 第一引数 N: \my_hello:n
  • 第二引数 p: #1
  • 第三引数 n: { Hello~#1! }

です.これにより \my_hello:n 関数が定義されています.

もう1つ別の例を見てみます.

\cs_if_exist:NTF \foo_bar: { true } { false }

関数名から推測できるように,\cs_if_exist:NTF は第一引数に与えた制御綴が存在するかどうかで条件分岐を行う関数です.引数指定子 TF は形の上では普通の n と変わりませんが,引数 T は「条件」が真のとき,引数 F は「条件」が偽のときのみ展開結果に現れます.

すなわち,上記の例では,このコードが実行された時点で \foo_bar: が定義されていれば true が,そうでなければ false が紙面に印刷されることになります.

簡単なコーディング例

ここまでの内容で,既に単純な expl3 コードを読むのには十分です.ということで,ここで簡単な FizzBuzz コード例を挙げておきます.

% 変数宣言
\int_new:N \g_fzbz_cnt_int

% メインループ(100回)
\int_do_while:nn { \g_fzbz_cnt_int < 100 } {
  % インクリメント(グローバル)
  \int_gincr:N \g_fzbz_cnt_int
  
  % 剰余の有無を判定
  \int_compare:nNnTF { \int_mod:nn { \g_fzbz_cnt_int } { 15 } } = { 0 }
    {
      FizzBuzz \\
    }
    {
      \int_compare:nNnTF { \int_mod:nn { \g_fzbz_cnt_int } { 3 } } = { 0 }
        {
          Fizz \\
        }
        {
          \int_compare:nNnTF { \int_mod:nn { \g_fzbz_cnt_int } { 5 } } = { 0 }
            {
              Buzz \\
            }
            {
              \int_use:N \g_fzbz_cnt_int \\
            }
        }
    }
}

特に難しいところはないので,コード中のコメントを参考に読めると思います.コード(関数名・変数名)から意図が明確に見て取れるのが(TeX on LaTeX に対する)expl3 の特徴です.

注意が必要なのは,冒頭の「変数宣言」です.各型(モジュール)には,原則として \<type>_new:N という形の関数が用意されているので,変数は必ず使用前に宣言するようにします.int 型の場合は \int_new:N を用いて変数宣言をしますが,この関数は同時に宣言した変数を0に初期化してくれます.

また,\g_fzbz_cnt_int は形からわかるようにグローバル変数なので,ループ内では \int_gincr:N によってグローバルにインクリメントしています.int 型変数のインクリメントには \int_incr:N というローカルにインクリメントする関数もありますが,グローバル変数には必ずグローバルな代入・変更を適用するようにしましょう.

そのほか,各関数の詳しい使い方については,公式リファレンスを参照してください.expl3 のドキュメントでは,各関数はモジュールごとにまとめられているので,公式リファレンスは expl3 コードを書く際にも大変役立ちます(他の多くのプログラミング言語でもそうだと思いますが).

展開制御と関数定義

特殊な引数指定子

これまでに紹介した,基本的な引数指定子のほかに,expl3 には展開制御などに関わる特殊な引数指定子があります.これらの特殊な引数指定子は expl3 の重要な特徴の1つで,これらを用いると TeX 言語では大きな障壁として認識されている「展開制御」をとても簡潔に記述することができます.

特殊な引数指定子を以下に列挙してみます.ここでは,比較のため従来の TeX (on LaTeX) で等価なコードを書くとどうなるかも続けて示しています.

  • c: 制御綴化
% expl3
\foo_bar:c { baz }

% TeX on LaTeX
\expandafter\foo@bar\csname baz\endcsname
  • x: 完全展開
% expl3
\foo_bar:x { \baz }

% TeX on LaTeX
\edef\tmp{\noexpand\foo@bar{\baz}}\tmp
  • o: 1回展開
% expl3
\foo_bar:o { \baz }

% TeX on LaTeX
\expandafter\foo@bar\expandafter{\baz}
  • f: 先頭完全展開
% expl3
\foo_bar:f { \baz }

% TeX on LaTeX
\expandafter\foo@bar\expandafter{\romannumeral-`0\baz}
  • V: 変数の値を取り出して使用
  • v: 制御綴化の上,変数の値を取り出して使用(c + V

具体例から,expl3 を使用すると TeX on LaTeX よりはるかにシンプルに展開制御が記述できることがわかると思います.上記の例はすべて引数が1つの場合ですが,引数が複数あるような状況を考えると,その差は歴然です.

関数定義と変種の生成

既に何度か触れましたが,expl3 で関数定義をする最も基本的な方法は \cs_new:Npn を用いることです.この関数のシンタックスは TeX 言語の \def とほぼ同一ですが

  • デフォルトで \long が有効であること(定義した関数の引数に \par トークンを含めることができる(2021-04-04 更新:\long の説明が誤っていたので修正しました))
  • 既に定義済みの関数を定義しようとするとエラーになる

という点はむしろ LaTeX2e の \newcommand に近いです.

ところで「関数を定義する関数」には多数の変種があり,例えば

  • \cs_new:cpn: 動的に制御綴を生成して関数を定義
  • \cs_new_nopar:Npn: \long が無効
  • \cs_set:Npn: 定義済みの関数を上書き(ローカル)

などがあります.

また「関数をコピーする関数」として \cs_new_eq:NN というものも用意されています.これは TeX 言語における \let に相当する関数です.

もちろん,この \cs_new_eq:NN にも \cs_new_eq:cc などの変種が存在していて,関数 \my_ナントカ_bar: の定義を \my_ナントカ_foo: にコピーするような場合に大変便利です.TeX 言語でそうした処理をしようと思うと

\expandafter\expandafter\expandafter\global
\expandafter\expandafter\expandafter
\let\expandafter\expandafter
  \csname my@#1@foo\endcsname
  \csname my@#1@bar\endcsname

などとなり \expandafter がたくさん登場する煩雑で可読性の低いコードになりますが,expl3 なら

\cs_new_eq:cc { my_#1_foo: } { my_#1_bar: }

のように極めて簡潔に記述することができます.

ところで,上述したように expl3 では〈引数指定〉だけが異なる “変種” が多数存在していて,使う側としてはとても便利なのですが,一見すると定義する側にとっては1つ1つ変種を用意する手間が増えそうです.しかし,実は expl3 ではその点もちゃんと考えられていて,\cs_generate_variant:Nn という関数を用いていとも簡単に “変種” を生成することができます.

例えば,\demo_cmd:Nnn という3引数をとる関数が定義されているとき

\cs_generate_variant:Nn \demo_cmd:Nnn { cnx }

を記述するだけで \demo_cmd:Nnn と同様の機能をもつ変種 \demo_cmd:cnx が自動的に用意されます.

コーディング作法

さて,以上で既に expl3 文法のあらましを説明し終えたので,ここで実際に expl3 コードを書く上での実践的な注意について述べておきます.これらは expl3 における「コーディング作法」とでも呼ぶべきもので,コードの安全性・可読性・保守性を維持するためにも,是非とも守って欲しいルールです.

1. TeX プリミティブを使わない

expl3 は TeX 言語ではないので,当然 TeX プリミティブをそのままの形で使うべきではありません.

一方で,機能・実体は TeX プリミティブであっても,expl3 カーネルによって関数化されているもの(例:\exp_after:wN)は使用しても構いません8

2. 各モジュールの内部変数や内部関数は使わない

expl3 では,各モジュールが内部処理に用いるプライベートな要素には〈モジュール〉の前に __ を前置することになっています.

  • プライベート変数の例:\l__foo_bar_tl
  • プライベート関数の例:\__foo_bar:Nn

言うまでもなく,これらの内部関数をモジュール外で使用するべきではありません.

3. 多少冗長であっても,可読性を重視する

これは expl3 に限ったことではありませんが,なるべく可読性の高いコードを書くことは保守性の観点からも重要です.具体的には

  • 変数名や関数名を十分説明的にすること
  • 積極的にスペーシングや改行を行うこと

を心がけるといいでしょう.

4. 内部関数はなるべく \long 有効にする

先ほども説明したように \cs_new:Npn はデフォルトで \long 有効です.特別な理由がある場合を除き,むやみに \cs_new_nopar:Npn を使うべきではありません.

5. 変数は必ず宣言してから使用する

すべての変数は \<type>_new:N のような関数を用いて先に宣言をしてから使用するようにしましょう.実装上の関係で,いくつかの型の変数については宣言を省略しても使えてしまいますが,expl3 ではこのような使い方はサポートされていません

便利なモジュール

expl3 には文法的なメリット(可読性や展開制御の書きやすさ)のほかに,l3kernel によって提供されるいくつかの非常に便利なモジュール(ライブラリ)を利用できることが挙げられます.そうしたモジュール1つ1つの詳しい説明は公式リファレンスや他の解説記事に譲りますが,いくつか筆者が便利だと思う具体的なモジュールを挙げておきます.

l3keys

LaTeX2e でも graphicx パッケージなどを筆頭として key=value の形でオプションを与える命令を目にしたことがあるかと思います.TeX on LaTeX でこうした機能を実装するためには,専用のパッケージを読み込む必要がありましたが,expl3 では標準モジュールとして keyval 形式をサポートするライブラリが組み込まれています.このライブラリはかなり柔軟性が高く,また keyval の設定自体を keyval 形式のシンタックスで記述できるので可読性も高く便利です.

l3regexp

expl3 には正規表現をサポートする l3regexp モジュールがあり,これがかなり強力です.LaTeX 上のプログラミングでは文字列の検索・置換といった操作をしたい場面は当然多いので,このモジュールの存在はとても心強いです.

LaTeX2e パッケージの作成

LaTeX3 という「プロダクト」はまだ存在していないので,expl3 を用いてパッケージを開発するとしたら普通は LaTeX2e パッケージということになるでしょう.expl3 で LaTeX2e パッケージを作成する際には,ファイル冒頭で \ProvidesExplPackage を用いてパッケージ宣言をします.

\ProvidesExplPackage{<name>}

これにより,そのパッケージファイル (*.sty) では自動的に expl3 シンタックスが有効になるので,明示的な \ExplSyntaxOn は必要ありません.

開発支援ツール

expl3 の文法はいわゆる TeX 言語と大差がないので,TeX 用の開発支援ツールをそのまま使用してもある程度の利便性は得られると思います.しかし,expl3 では関数名・変数名に _: が入るので,シンタックス・ハイライトぐらいは専用のものがあると便利です.

expl3 はまだ開発段階にあることもあり,専用の開発支援ツールはそれほど多くはありませんが,以下のようなものが既に存在しています.

  • TeXworkds: LaTeX3 チームの Joseph によるシンタックス・ハイライトあり
  • WinEdit: 標準でサポート
  • Emacs: AUCTeX が expl3 に対応している
  • Vim: 拙作 vim-expl3 がシンタックス・ハイライトを含む基礎的なサポート機能を提供

おわりに

expl3 を使うことの最大の利点は,その特徴的な命名規則による意図の明確化と名前空間の分離,そして展開制御を簡潔に記述するための仕組みによって,TeX 言語よりも一段と可読性・保守性を高めることができる点です.加えて開発したいアプリケーション(LaTeX パッケージ)の種類によっては,l3kernel の提供する強力なライブラリ機能が利用できることも強みです.

上記の利点が活かせそうな LaTeX パッケージ(特に,ある程度大規模または複雑な処理を行う必要が想定されるもの)を開発したいと思い至ったときは,是非 expl3 の採用を考えてみてください.

LaTeX3 プロジェクトの中では,expl3 は最初の一段回に過ぎないので,今後 expl3 を用いて LaTeX2e に替わる LaTeX3 のマクロ体系そのものが実装されていくことが計画されています.LaTeX の文化が続く限り,expl3 で書かれたコードは LaTeX2e でも(将来的にリリースされるだろう)LaTeX3 でも動作することが期待され,その産物は長く利用できる可能性を秘めています.

参考文献

expl3 の第三の特徴として挙げたように,expl3 には非常に充実した公式のリファレンスがあります.したがって,本稿の参考文献もすべて LaTeX3 チームによる公式ドキュメントです.

[1] The expl3 package and LaTeX3 programming (expl3.pdf)
[2] The l3build Package (l3build.pdf)
[3] The LaTeX3 Interfaces (interface3.pdf)
[4] The LaTeX3 kernel: style guide for code authors (l3styleguide.pdf)


  1. 正確には「TeX90 の全プリミティブと e-拡張のほとんどのプリミティブ,および pdfTeX の \pdfstrcmp 相当の機能」を有する TeX 処理系である必要があります.v1.40 以降の pdfTeX,v0.9994 以降の XeTeX,v0.70 以降の LuaTeX そして mid-2012 以降の e-(u)pTeX はこれに該当します. ↩︎

  2. 普通の言語ではこの程度は「当たり前のコト」かもしれませんが,TeX 言語ではまったくこのようになっていませんでした.例えば \romannumeral は本来は(文字通り)数値をローマ数字の「文字列化」するための命令ですが,TeX 言語者によってしばしば本来の目的とはまったく異なる邪悪な使い方をされていました. ↩︎

  3. texdoc expl3 で開くことのできる The expl3 package and LaTeX3 programming (expl3.pdf) は,リファレンスというよりは expl3 の簡単な紹介をする文書になっています. ↩︎

  4. 一方 texdoc source3 とすると The LaTeX3 Sources (source3.pdf) という,interface3.pdf の内容に加えて内部実装そのものを含む文書を開くことができます.これらの文書名・ファイル名は LaTeX2e の実装を紹介する公式ドキュメント The LaTeX2e Sources (source2e.pdf) に由来しているようです. ↩︎

  5. 内部的に expl3 パッケージを読み込む xparse パッケージを \usepackage してももちろん OK です.ところで,若干紛らわしい感じもしますが「expl3 パッケージ」は LaTeX2e 用のパッケージです. ↩︎

  6. ローカル変数にグローバルな代入をしないのは当たり前ですが,グローバル変数にローカルな代入をすることも推奨されません. ↩︎

  7. 既存のモジュール名は LaTeX3 チームの管理する l3prefixes.csv というファイルにリストアップされています.新たにモジュール名を作成するときは,このリストにあるモジュール名と衝突しないように注意しましょう. ↩︎

  8. l3names では LaTeX3 チームがカーネルを記述するためにすべての TeX プリミティブを \tex_〈プリミティブ〉:D という expl3 式の命令にコピーしていますが,これらは一般の expl3 ユーザは使用すべきでありません.そもそも,ここで登場している〈引数指定〉D は “Do NOT use” の意味です. ↩︎