expl3 の標準データ構造 (2) prop, clist

2018-08-26   #expl3 

前回は expl3 のシークエンスについて紹介しました.今回は,シークエンスほど標準関数が充実していたり処理が高速であったりするわけではないものの,それぞれ特色ある標準データ構造であるプロパティリスト(prop 型)とカンマ区切りリスト(clist 型)について見ていくことにしましょう.

プロパティリスト(prop 型)

expl3 のプロパティリストは key-value の組によって情報を格納するデータ構造で,巷でいうところの連想配列(辞書,ハッシュテーブル)に相当するものです.シークエンスと違い順番は保証されませんが,任意要素へのアクセスはシークエンスよりは高速です.

prop を構成する key と value にはいずれも任意の <balanced text> を用いることができます.1つのプロパティリスト内では key はユニークである必要があり1,もし既に存在する key を使用して要素を追加しようとすると,同名 key の要素は上書きされてしまいます.

基本操作

プロパティリストを使用するためには,もちろん

\prop_new:N <property list>

によって prop 型の変数を宣言する必要があります.宣言はグローバルで <property list> が存在する場合はエラーになります2

\prop_new:N により宣言した <property list> は自動的に空のプロパティリストに初期化されます.プロパティリストの内容は \prop_show:N\prop_log:N でそれぞれコンソール,ログファイルに出力することができます.

【入力】

% プロパティリストを宣言(空プロパティリストに初期化)
\prop_new:N \l_test_prop  % \l_test_prop = {}

% コンソール出力
\prop_show:N \l_test_prop

【コンソール出力】

The property list \l_my_prop is empty
> .

シークエンスに値を追加するには \prop_put:Nnn を使用します.既に存在する key の要素を追加しようとした場合,元の要素は上書きされます.

【入力】

% 要素を追加
\prop_put:Nnn \l_test_prop { key } { value }  % \l_test_prop = {key => value}
\prop_put:Nnn \l_test_prop { foo } { bar }    % \l_test_prop = {key => value, foo => bar}

% 既に key が存在する場合は上書き
\prop_put:Nnn \l_test_prop { foo } { baz }    % \l_test_prop = {key => value, foo => baz}

% コンソール出力
\prop_show:N \l_test_prop

【コンソール出力】

The property list \l_test_prop contains the pairs (without outer braces):
>  {key}  =>  {value}
>  {foo}  =>  {baz}.

\prop_put:Nnn の代わりに \prop_put_if_new:Nnn を使うと,要素の上書きが起こりません(重複する key の要素を追加しようとした場合は,何もしません).

要素を取り出す

プロパティリストから要素を取り出す場合,基本的には key を指定して対応する value を取り出すことになります.要素を取り出す方法は例によって「tl 型の変数に取り出す」方法と「入力ストリームに取り出す」方法の2つに大別できますが,まずはより効率のよい前者から見ていきます.

なお,シークエンスには先頭に近い要素ほど高速に取り出すことができるという性質がありましたが,プロパティリストの場合はどの要素を取り出してもかかる時間に差はないようです(そして,そもそも順序は保証されていません).

% プロパティリストを宣言 & 値を設定
\prop_new:N \l_snowman_prop
\prop_put:Nnn \l_snowman_prop { muffler } { red }
\prop_put:Nnn \l_snowman_prop { arms } { brown }
\prop_put:Nnn \l_snowman_prop { hat } { yellow }
  % \l_snowman_prop = {muffler => red, arms => brown, hat => yellow}

% 指定した key に対応する value を取り出し,プロパティリストからは削除
\prop_pop:NnN \l_snowman_prop { muffler } \l_tmpa_tl
  % \l_tmpa_tl = red, \l_snowman_prop = {arms => brown, hat => yellow}

% 指定した key に対応する value を取り出す
\prop_get:NnN \l_snowman_prop { arms } \l_tmpa_tl
  % \l_tmpa_tl = brown, \l_snowman_prop = {arms => brown, hat => yellow}

\prop_pop:NnN によるプロパティリストからの要素の削除はローカルに行われるので,グローバルな削除をしたい場合には \prop_gpop:NnN を用います.すべてのケースにおいて tl 型変数への要素の代入はローカルに行われます.また,存在しない key に対応する要素を取り出そうとした場合,取り出し先の tl 型変数には \q_no_value という特別な値が入ります.

一方で,プロパティリストの要素 (value) を入力ストリームに取り出したい場合は \prop_item:Nn が使えます.

% 指定した key に対応する value を入力ストリームに取り出す
\prop_item:Nn \l_snowman_prop { hat }  %=>yellow

なお,存在しない key の要素を入力ストリームに取り出そうとした場合は,特に警告やエラーも発生しませんが,単に何も返りません(空トークン列に展開されます).

複製と要素の削除

もちろん,プロパティリストを複製したり,要素を削除したりする関数もあります.

% プロパティリストを宣言
\prop_new:N \l_nohat_snowman_prop

% プロパティリストを複製
\prop_set_eq:NN \l_nohat_snowman_prop \l_snowman_prop
  % \l_nohat_snowman_prop = {arms => brown, hat => yellow}

% プロパティリストから指定要素を削除
\prop_remove:Nn \l_nohat_snowman_prop { hat }
  % \l_nohat_snowman_prop = {arms => brown}

条件分岐

プロパティリストの状態によって分岐する関数.

% プロパティリストを宣言&値を設定
\prop_new:N \l_lang_prop
\prop_put:Nnn \l_lang_prop { en } { English }
\prop_put:Nnn \l_lang_prop { ja } { Japanese }
  % \l_lang_prop = {en => English, ja => Japanese}

% 空プロパティリストか否か
\prop_if_empty:NTF \l_lang_prop { true } { false } %=>false

% 指定したキー (en) を持つか
\prop_if_in:NnTF \l_lang_prop { en } { true } { false } %=>true

マップ操作

プロパティリストについてもマップ操作を適用することができます.プロパティリストのマップ操作では,各イテレーション中でキー (#1) と値 (#2) の両方にアクセスすることができます.

【入力】

% プロパティリストを初期化 & 値を設定
\prop_clear_new:N \l_snowman_prop
\prop_put:Nnn \l_snowman_prop { muffler } { red }
\prop_put:Nnn \l_snowman_prop { arms } { brown }
\prop_put:Nnn \l_snowman_prop { hat } { yellow }
  % \l_snowman_prop = {muffler => red, arms => brown, hat => yellow}

% すべての要素にインライン関数 “\scsnowman [ scale = 10, #1 = #2 ]” を適用
\prop_clear_new:N \l_snowman_prop
\prop_put:Nnn \l_snowman_prop { muffler } { red }
\prop_put:Nnn \l_snowman_prop { arms } { brown }
\prop_put:Nnn \l_snowman_prop { hat } { yellow }

\prop_map_inline:Nn \l_snowman_prop {
  \scsnowman [ scale = 10, #1 = #2 ]
}

【PDF 出力】

snowmen

上のコード例ではインライン関数によるマップ処理しか示していませんが,指定した関数をマップ適用する \prop_map_function:NN もあります.この場合,第2引数に与える関数は2つ引数(第1引数がキー,第2引数が値)を取る関数である必要があります.

また,マップ処理の中断には \prop_map_break:\prop_map_break:n も使えます.

カンマ区切りリスト(clist 型)

その名の通り,各要素をカンマ , 区切りで並べたデータ構造です.しくみとインターフェースが直感的ではありますが,各種操作の効率や安全性3は基本的に先に述べた seq 型に劣るので,LaTeX2e 向きのインターフェースを手軽に実装したい場合を除いては,原則として seq 型を使う方が得策でしょう

clist にもやはりほとんどの <balanced text> を含めることができますが,, を含む場合はブレース {} で囲む必要があるほか,#\q_mark\q_stop を含められないなど細かく複雑な制約が多数あります.

基本操作

カンマ区切りリストを使用するには,もはやお馴染みですが

\clist_new:N <comma list>

による clist 型の変数の宣言が必要です.宣言はグローバルで <comma list> が存在する場合はエラーになります4

\clist_new:N により宣言した <comma list> は自動的に空のプロパティリストに初期化されます.カンマ区切りリストの内容は \clist_show:N\clist_log:N でそれぞれコンソール,ログファイルに出力することができます.

【入力】

% カンマ区切りリストを宣言(空カンマ区切りリストに初期化)
\clist_new:N \l_test_clist  % \l_test_clist = ()

% コンソール出力
\clist_show:N \l_test_clist

【コンソール出力】

The comma list \l_test_clist is empty
> .

シークエンスなどの場合と違い,カンマ区切りリストは簡単に複数の値を設定できるのが特徴です.その最も基本的な方法として

\clist_set:Nn <comma list> {<item 1>, ..., <item n>}

を用いると <comma list> の内容を一発で <item 1>, ..., <item n> に設定することができます.なお,\clist_set:Nn<comma list> が元々持っている内容を完全に上書きします.

ここで <item> をカンマ区切りで列挙する部分でいくつか細かな注意事項があります:

  • <item> 両端のスペース ~ は除去される5
  • <item> を囲う最外のブレース {} は除去される6
  • カンマ(や一部の特殊なトークン)を含む <item> はブレース {} で囲む必要がある
  • 空の <item> は無視される7

ちょっと小難しく感じるかもしれませんが,LaTeX2e で「カンマ区切りの引数」が出てくる場合にはよくある仕様だと思います.以下に具体例を挙げておくので,合わせて参考してください.

【入力】

% 値を設定
\clist_set:Nn \l_test_clist {
  ~a~ , ~{b}~, c~d , ~{e~}~ , , {{f}} , {} , {{}} ,
}  % \l_test_clist = (a, b, c~d, e~, {f}, , {})

% コンソール出力
\clist_show:N \l_test_clist

【コンソール出力】

The comma list \l_test_clist contains the items (without outer braces):
>  {a}
>  {b}
>  {c d}
>  {e }
>  {{f}}
>  {}
>  {{}}.

また,シークエンスと同様に,clist を上書き一括設定するのではなく,先頭または末尾に要素を加える方法も存在します.この場合も,<item> をカンマ区切りにして複数一度に設定することが可能です(スペースやブレースの処理については \clist_set:Nn と同じ).

% カンマ区切りリストを宣言&値を設定
\clist_new:N \l_number_clist
\clist_set:Nn \l_number_clist { 2 }  % \l_number_clist = (2)

% 先頭に複数要素を追加
\clist_put_left:Nn \l_number_clist { ~0~, {1}, }
  % \l_number_clist = (0, 1, 2)

% 末尾に複数要素を追加
\clist_put_right:Nn \l_number_clist { , ~~3, ~{4}~ }
  % \l_number_clist = (0, 1, 2, 3, 4)

シークエンスへの変換

expl3 におけるカンマ区切りリストのほぼ唯一の利点は「多数の値を簡単に設定できること」なので,一度データ構造にしてしまってからはシークエンスに変換してから種々の操作を行うのが賢明です.都合の良いことに l3seq パッケージ側に clist を seq に変換するためのユーティリティ \seq_set_from_clist:NN が用意されています.

% シークエンスを初期化
\seq_clear_new:N \l_number_seq  % \l_number_seq = []

% カンマ区切りリストをシークエンスに変換
\seq_set_from_clist:NN \l_number_seq \l_number_clist
  % \l_number_seq = [0, 1, 2, 3, 4]

また \seq_set_from_clist:Nn を用いると clist 型の変数を介さずとも “直書きのカンマ区切りリスト” から直接シークエンスを設定することもできます.

% シークエンスを宣言
\seq_new:N \l_essential_packages_seq

% シークエンスを直書きカンマ区切りリストから設定
\seq_set_from_clist:Nn \l_essential_packages_seq {
  scsnowman, scwrapfig, scpremiumfriday, scmessages
}  % \l_essential_packages_seq = [scsnowman, scwrapfig, scpremiumfriday, scmessages]

定数

シークエンスは「逐次的に値を追加していく」方法で値を設定するのが普通なので,いわゆる定数(値が変更されることのない変数)を作ることが難しかったですが8,カンマ区切りリストの場合は一気に値を設定することができるため,定数も自然に定義することができます.

\clist_const:Nn \c_essential_and_friends_clist {
  snowman, duck, marten
}  % \c_essential_and_friends_clist = (snowman, duck, marten)

その他の操作

既に何度も述べているように,カンマ区切りリスト上での各種操作の性能・安全性はシークエンスのそれに劣るので9,基本的には一度値を設定したら \seq_set_from_clist:* 系の関数を用いてシークエンスに変換してしまうのが得策です.

とはいえ,l3clist パッケージにもさまざまなユーティリティ関数が用意されているので,以下にそれを簡単に紹介していこうと思います.それぞれの関数の使い方や機能は l3seq パッケージの “相当” 関数とほとんど同じなので,ここでは具体的な使用例や詳しい説明は省きます.適宜,前回の記事を参照するといいでしょう.

要素を取り出す

カンマ区切りリストもまた,シークエンスと同様「スタック」のように使うことができます10.例によって,基本的には tl 型の変数に値を取り出すことになります.

シークエンスの場合と違い,カンマ区切りリストの場合は専用関数は先頭要素を取り出すものしかなく,末尾要素を取り出す専用の関数はありません.

% 先頭要素を <tl var> に取り出す
\clist_get:NN <comma list> <tl var>

% 先頭要素を <tl var> に取り出し,カンマ区切りリストからは削除
\clist_pop:NN <comma list> <tl var>

また,指定した任意位置の要素を入力ストリームに取り出す \clist_item:Nn もあります.

\clist_item:Nn <comma list> {<integer expression>}

複製と連結

l3clist パッケージもカンマ区切りリストの複製(コピー)や連結を行うための関数も提供しています.

% <comma list 2> を <comma list 1> に複製
\clist_seq_eq:NN <comma list 1> <comma list 2>

% <comma list 2> と <comma list 3> を連結し,結果を <comma list 1> に代入
\clist_concat:NNN <comma list 1> <comma list 2> <comma list 3>

変更操作

カンマ区切りリストの内容を変更する操作.

% 反転
\clist_reverse:N <comma list>

% 重複を削除
\clist_remove_duplicates:N <comma list>

% 特定要素を全削除
\clist_remove_all:Nn <comma list> {<item>}

% ソート
\clist_sort:Nn <comma list> {<comparison code>}

条件分岐

カンマ区切りリストの状態によって条件分岐する関数.

% 空カンマ区切りリストか否か
\clist_if_empty:NTF <comma list> {<true code>} {<false code>}

% 指定した要素を含むか
\clist_if_in:nnTF <comma list> {<item>} {<true code>} {<false code>}

なお \clist_if_in:nnTF に与える <item>{, }, # が含まれると TeX レベルのエラーが発生する場合があるので注意してください11

カンマ区切りリストを直接利用する

カンマ区切りリストの全要素を入力ストリームに吐き出すには \clist_use:Nnnn または \clist_use:Nn 関数を用います.

% セパレータを細かく設定
\clist_use:Nnnn  <comma list> {<separator between two>}
  {<separator between more than two>} {<separator between final two>}

% 単一のセパレータ
\clist_use:Nn  <comma list> {<separator>}

マップ操作

マップ操作は例によって「関数で指定する」方法と「インライン関数を指定する」方法があります.カンマ区切りリストの各要素が単一の引数 (#1) として与えられます.

% 関数を指定してマップ操作
\clist_map_function:NN <comma list> <function>

% インライン関数を指定してマップ操作
\clist_map_inline:Nn <comma list> {<inline function>}

% 要素を <variable> に代入した上で <code> を実行
\clist_map_variable:NNn <comma list> <variable> {<code>}

またマップ処理の中断には \clist_map_break:\clist_map_break:n {<code>} が使えます.


  1. key の同一性は expl3 の文字列型 (str) ベースで判定されます(\str_if_eq:nn の判定結果と一致).すなわち,カテゴリーコードの違い等は無視されます. [return]
  2. プロパティリストが存在しない場合には宣言と初期化,存在する場合は初期化のみを実行する \prop_clear_new:N もあります. [return]
  3. 公式ドキュメント (source3.pdf) に “safety” とあるので直訳して「安全性」と書きましたが,要するにカンマ区切りリストはその要素中に , が出てきたときに意図せぬところで「カンマ区切り」が発生してしまう可能性があるということでしょう(もちろん {} によって要素全体を囲っておけば回避できます). [return]
  4. カンマ区切りリストが存在しない場合には宣言と初期化,存在する場合は初期化のみを実行する \clist_clear_new:N もあります. [return]
  5. expl3 における「空白トークン」は ~ のように明示的に書かない限りは発生しないため,<item> 両端スペースの除去は不要なようにも思われますが,この仕様は \ExplSyntaxOn\ExplSyntaxOff 外の環境(主に LaTeX2e)から引数を受け取る場合の利便性を考えてのことのようです. [return]
  6. ここで「最外のブレース」は,両端のスペース ~ を除去した後に判定されます. [return]
  7. この規則のおかげで {a, b, c,} のような,いわゆる「ケツカンマ」が許容されます. [return]
  8. 2018年8月現在,l3candidates に \seq_const_from_clist:Nn という関数があるので,将来的には「seq の定数」という概念が普通に存在するようになるかも知れません. [return]
  9. 例外的に \clist_if_in:NnTF\clist_remove_duplicates:N はカンマ区切りリストで行った方が速度的に有利な場合があるようです. [return]
  10. 公式マニュアル (interface3.pdf) の l3clist の章7節 Comma lists as stacks (p. 111) に “The stack functions for comma lists are not intended to be mixed with the general ordered data functions detailed in the previous section: a comma list should either be used as an ordered data type or as a stack, but not in both ways.”(大意:カンマ区切りリストは一般の順序付きデータ構造としてもスタックとしても使えるが,1つのカンマ区切りリストについて両方の使い方を混用するのは避けなさい)という注意書きがあるのですが,この文の真意が不明です.\clist_sort:Nn した後に \clist_pop:NN などをしても特におかしな挙動になったりはしないようですが…… [return]
  11. もちろん,通常の TeX のカテゴリーコード設定の場合の話です. [return]