マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

expl3でFibonacci数列を完全展開可能な形で求める話(1)

某キャンペーン🌸のネタにはならないわけだが、せっかくなのでチョット話してみる。特にexpl3の新機能である“e 引数指定子”について詳しく扱うので「e引数指定子をまだ知らない」というexpl3者にとっては有用な記事になるかもしれない。

※対象読者は「フツーにexpl3できる人」とする🙂

お題

次の2つの完全展開可能な命令を実装したい。

  • \Fibonacci{<整数n>}:[完全展開可能] フィボナッチ数列の第n項の値(の十進表記)。
  • \FibonacciSeq{<整数n>}:[完全展開可能] フィボナッチ数列の第n項までをコンマ区切りで並べた文字列。

完全展開可能であるため、展開限定文脈(\typeoutの中など)でも正常に動作する必要がある。

\typeout{F[10] = \Fibonacci{10}}
%==> "F[10] = 55" (端末表示)
\typeout{\FibonacciSeq{10}}
%==> "1, 1, 2, 3, 5, 8, 13, 21, 34, 55" (端末表示)

とにかく実装し始める話

とりあえず「フィボナッチ数列の値を求める部分」以外の“ガワの部分”をさっさと済ませてしまおう。フツーのexpl3者にとっては初歩的なコード実装のはずだが、「完全展開可能にしたいので完全展開可能でない1ライブラリ関数(\int_step_inline:~等)は使えない」ことに注意する必要がある。

%%<*> \Fibonacci{<整数n>} (完全展開可能)
% フィボナッチ数列の第n項の値.
% ※完全展開可能にしたいので, xparse系のマクロ定義命令を利用するならば
% "Expandable" 版のものを選ぶ必要がある. (\newcommand でもよい.)
\NewExpandableDocumentCommand \Fibonacci { m }
  { \int_to_arabic:n { \__myfib_value:n {#1} } }

%%<*> \FibonacciSeq{<整数n>} (完全展開可能)
% フィボナッチ数列の第n項までをコンマ区切りで並べた文字列.
\NewExpandableDocumentCommand \FibonacciSeq { m }
  % 完全展開可能にしたいので \int_step_inline:~ ではなく
  % \int_step_function:~ を利用する.
  { \int_step_function:nnN { 1 } {#1} \__myfib_seq_iter:n }
% ループの中の処理.
\cs_new:Nn \__myfib_seq_iter:n
  {
    \int_compare:nNnF {#1} =  { 1 } { ,~ } % 先頭以外ではコンマを入れる
    \int_to_arabic:n { \__myfib_value:n {#1} }
  }

%% \__myfib_value:n{<n>}
% フィボナッチ数列の第n項の値.
\cs_new:Nn \__myfib_value:n
  { % TODO:実装する
  }

後は「\__myfib_value:nをいかにして完全展開可能で実装するか」という話になる。

TeX以外で実装してみる話

完全展開可能にするため\int_set:Nn等の「代入操作」は一切使えないことになる。従って再帰を利用した2関数型プログラミング的なロジック”を組む必要がある。

ここでは「どんな感じのコードを書けばいいか」を示すために関数型組版言語であるSATySFiのコードを掲載することにする。

@require: stdjareport

% 不変条件: a が第(n-k)項, b が第(n-k+1)項に等しい.
let-rec myfib-value-aux k a b =
  if k == 1 then b % 第n項の値
  else myfib-value-aux (k - 1) b (a + b) % 再帰する
let myfib-value n =
  if n < 1 then 0
  else myfib-value-aux n 0 1

% ↓これ以降はSATySFi特有の話なのでexpl3者は気にしなくてよい.
let-inline ctx \Fibonacci n =
  read-inline ctx (embed-string (arabic (myfib-value n)))
in
document (|
  author = {}; title = {}; show-title = false; show-toc = false
|) '<
  +p{${F_{10}} = \Fibonacci(10);}
>

もちろんSATySFiなのでこの記事のお題の\Fibonacciに相当する命令も作れる😃

main.satyの出力

expl3で一応実装してみた話

「どういう感じのコードを書けばいいか」がわかったので、\__myfib_value:nを実際にexpl3で書いてみよう。

%% \__myfib_value:n{<n>}
% フィボナッチ数列の第n項の値.
\cs_new:Nn \__myfib_value:n
  {
    \int_compare:nNnTF {#1} < { 1 } { 0 } % n<1なら0を返す
      { \__myfib_value_aux:nnn {#1} { 0 } { 1 } }
  }

%% \__myfib_value_aux:nnn{<k>}{<a>}{<b>}
% \__myfib_value:n の下請け.
% 不変条件: a が第(n-k)項, b が第(n-k+1)項に等しい.
\cs_new:Nn \__myfib_value_aux:nnn
  {
    \int_compare:nNnTF {#1} = { 1 } { #3 } % 第n項の値
      {% 単純に再帰呼出してみた
        \__myfib_value_aux:nnn
          { \int_eval:n { #1 - 1 } }
          { #3 }
          { \int_eval:n { #2 + #3 } }
      }
  }

実際に\Fibonacci\typeoutの中に置いて試してみると、正しく動作しているようにみえる。

\typeout{F[10] = \Fibonacci{10}}
%==> "F[10] = 55" (端末出力)

しかしnの値を少し増やすと爆発してしまう😲

\typeout{F[30] = \Fibonacci{30}}
Runaway argument?
{\int_eval:n {\int_eval:n {\int_eval:n {\int_eval:n {\int_eval:n {\int_eval:n \
ETC.
! TeX capacity exceeded, sorry [main memory size=5000000].
<argument> ...l:n {\int_eval:n {\int_eval:n {\ETC.

l.3 \typeout{F[30]=\Fibonacci{30}}

※第30項の値は832040だからTeXの扱える整数の範囲にはまだ入っているはず。

\int_eval:nが延々と並んでいるのを見れば察しが付くと思うが、要するに「展開制御が足りていない」のが原因である。

\__myfib_value_aux:nnn再帰呼出のところを検討してみよう。

\__myfib_value_aux:nnn{2}{0}{1}
↓(展開を続ける)
\__myfib_value_aux:nnn{\int_eval:n{2-1}}{1}{\int_eval:n{0+1}}

ここで期待する動作は「\__myfib_value_aux:nnn{1}{1}{1}」が実行されることであろう。しかしexpl3の“関数”は所詮はTeXのマクロに過ぎないので、何も展開制御をしなければ\int_eval:n{2-1}等のトークン列がそのままマクロに渡されてしまうことになる。これを何度も繰り返すと、引数の式が

{\int_eval:n {\int_eval:n {\int_eval:n {\int_eval:n {\int_eval:n {\int_eval:n ……

のようなオソロシイ形に肥大化するわけである。このトークン列の長さはnに対して指数関数的に増えるので、少し大きいnで“TeX capacity exceeded”になるのも当然である。

結局、行うべき展開制御の内容は「__myfib_value_aux:nnn再帰呼出の際に第1と第3の引数を完全展開すること」ということになる。

\__myfib_value_aux:nnn{\int_eval:n{2-1}}{1}{\int_eval:n{0+1}}
↓上のコードを下のコードに変えたい
\__myfib_value_aux:nnn{1}{1}{1}

展開制御してみる話

expl3における展開制御は基本的に「展開用の引数指定子(argumente specifier)を指定する」形で行う。今やりたいのは完全展開であるが、expl3に昔からある引数指定子で完全展開の機能をもつものは次の2つである。

  • x: 引数を完全展開3する。ただし、元々n指定の引数を展開制御(\exp_args:N~)によりx指定に転換した場合は完全展開可能性が失われてしまう
  • f: 引数を先頭完全展開する。(展開制御でf指定に転換した場合には完全展開可能性は失われない。)

\__myfib_value_aux:nnnの展開制御でどちらを使うべきかの答えは明らかである。そもそも完全展開可能な命令を実装しようとしているのだから「完全展開可能性が失われる」性質を持つx指定は選択肢になく、f指定を使うしかない。従って、f指定で目的を果たせるかを検討しよう。

今やりたいのは「\__myfib_value_aux:nnnの2つの引数を完全展開する」ことであるが、この2つの引数はいずれも「\int_eval:n {…}」という形である。\int_eval:nは先頭完全展開可能4である(マニュアル(interface3)において★印が付いている)なので、f指定(先頭完全展開を施す)により完全展開されることがわかる。従って引数全体のf指定による展開結果は「整数式の値(を表すトークン列5)」となり、結果的にこれは所望のものと一致している。

f指定で目的が果たせることがわかったので実際にコードを改修してみよう。expl3で展開制御を指定する方法には\cs_generate_variant:Nnを使うものと\exp_args:N~を使うものの2種類がある。

\cs_generate_variant:Nn で頑張る話

今欲しいものは「\__myfib_value_aux:nnnの第1と第3の引数にf指定の展開を施したもの」である。これをexpl3の関数の命名規則では\__myfib_value_aux:fnf(引数指定子の第1と第3の文字をfに変える)と呼ぶ。このように「ある関数の引数指定子を変えたもの」のことをその関数の「変種(variant)」と呼ぶ。

そして、所望の変種\__myfib_value_aux:fnfを既存の\__myfib_value_aux:nnnから自動的に生成してくれるのが\cs_generate_variant:Nnというライブラリ関数である。今の場合は「既存の\__myfib_value_aux:nnnからfnf版を生成したい」ので次のようなコードを実行すればよい。

\cs_generate_variant:Nn \__myfib_value_aux:nnn { fnf }

これで\__myfib_value_aux:fnfが定義されるので、\__myfib_value_aux:nnnの定義本体のコードの中の再帰呼出の部分をこのfnf版の呼出に置き換えよう。

\cs_new:Nn \__myfib_value_aux:nnn
  {
    \int_compare:nNnTF {#1} = { 1 } { #3 }
      {% 再帰呼出では引数を展開する
        \__myfib_value_aux:fnf %←※ここで"fnf"版を使っている
          { \int_eval:n { #1 - 1 } }
          { #3 }
          { \int_eval:n { #2 + #3 } }
      }
  }
\cs_generate_variant:Nn \__myfib_value_aux:nnn { fnf }

これでフツーに動く\Fibonacciが完成したことになる。実際に少し大きいnで動作を試してみよう。

\typeout{F[30] = \Fibonacci{30}}
%==> "F[30] = 832040" (端末出力)

うまくいったようだ😊

\exp_args:N~ で頑張る話

\__myfib_value_aux:nnnfnf版が欲しい」場合に\cs_generate_variant:Nnは実際に\__myfib_value_aux:fnfという関数を定義するのであった。これとは別の方法として、\exp_args:N~という一連のライブラリ関数を利用することもできる。これは「\__myfib_value_aux:nnnfnf版の動作をさせる」ためのものである。

\exp_args:N~の部分には所望の変種の引数指定子を書く。例えば、\__myfib_value_aux:nnnfnf版の動作をさせたい場合は、\exp_args:Nfnfという関数を前に置けばよい。

\exp_args:Nfnf \__myfib_value_aux:nnn { \int_eval:n { 2 - 1 } } { 1 } { \int_eval:n { 0 + 1 } }

\__myfib_value_aux:nnn以下のトークン列は「\exp_args:Nfnfの引数」という位置付けになっていて、だからこそNfnfという引数指定子になっている。

これで完成のはずだが、実際には上記のようなコードを実行すると「\exp_args:Nfnfが未定義である」というエラーになる。\exp_args:N~の部分のパターンは無数にあり、それら全てを予め定義しておくのは無駄であるから「expl3のカーネルでは一部だけを定義しておく」という方針になっているためである。どのパターンがカーネルで定義されているかはマニュアルに書かれていて、例えば\exp_args:Nf\exp_args:Nnffは定義されているが\exp_args:Nfnfはされていない。

カーネルで定義されてない\exp_args:N~のパターンを使用するには、予め\exp_args_generate:nという命令を用いて定義する必要がある6

\exp_args_generate:n { fnf }

\exp_args:Nfnfを使って\__myfib_value_aux:nnnを修正した場合のコードは以下のようになる。

% \exp_args:Nfnf を利用可能にする
\exp_args_generate:n { fnf }

\cs_new:Nn \__myfib_value_aux:nnn
  {
    \int_compare:nNnTF {#1} = { 1 } { #3 }
      {% 再帰呼出では引数を展開する
        \exp_args:Nfnf \__myfib_value_aux:nnn
          { \int_eval:n { #1 - 1 } }
          { #3 }
          { \int_eval:n { #2 + #3 } }
      }
  }

(つづけ)


  1. マニュアル(interface3)において星印(★や☆)が付いていない関数は完全展開可能でない。
  2. もちろん、「フィボナッチ数列の定義をそのまま書いたコード」は「求めるフィボナッチ数の値に比例した時間(nに対いて指数関数的)」がかかってしまうので、それはやってはいけない🙃
  3. この記事での「完全展開」「先頭完全展開」はTeX言語の用法に従う。もしかしたらexpl3では「先頭完全展開」のことを「完全展開(full expansion)」と呼ぶのかもしれないが、今一つ実態をつかめていないので従来の用語を使うことにする。
  4. expl3の用語では「先頭完全展開可能」のことを「完全展開可能(fully expandable)」、単なる「完全展開可能」のことを「制限付展開可能(restricted expandable)」と呼ぶ。ここではTeX言語の用法に従う。
  5. ちなみに、expl3の仕様としては「整数値を返すライブラリ関数」の実際の展開結果である「整数値を表すトークン列」は必ずしも「十進の数字列」とは限らないようである。\Fibonacciの実装コードでわざわざ\int_to_arabic:nを入れているのはこのためである。
  6. なお、既に定義済のパターンについて\exp_args_generate:nを実行しても何も起こらない。今カーネルで定義済のものが将来削除されることもないため、「今のexpl3の版で未定義ならば自分で定義する」という方針に従っても前方・後方互換性は保たれる。

TeXでつくるLLM

というわけで、今年もあの季節🌸がやってきました!

キャンペーンと関係あるのかどうかは不明ですが、どうやらTeX言語🤮のトッテモ有用な記事が公開されているようです😃

というわけで、自分も何か記事を書こうと思い立ちました🙂

さっそく、TeXでつくるLLM」というお題が降ってきました😊

bsky.app

さっそく取り組むことにしましょう。

TeXでLLMする方針について

“LLM”のような頭字語について考える際に重要になるのが「“元の語形”をどう設定するか」です。

“元の語形”を適切に設定できれば、あとはscsnowmanやtikzducksなどのパッケージを活用して素敵な文書(画像)を生成すればネタ⛄🦆が完成します。

TeXでLLMできない問題について

しかし、“LLM”については重大な問題があります。

“LLM”には“S”も“D”も含まれない😢

困りました😢 しかし、TeXのエコシステムにはscsnowmanやtikzducks以外にも有用なパッケージがイロイロ存在するはずです。tikzlingsの目次を見てみましょう。

tikzlingsパッケージの目次(or キャラクタ一覧🙃)

“M”はいるけど“L”がいない😭

※marmot(マーモット)、mouse(ネズミ)、mole(モグラ)が“M”から始まる。

絶望的な状況になってしまいました😭

……あっ!😲

“Mouse”の機能説明から抜粋

……これでいきましょう!🙃

LLM = Leg-Lifting Mouse

TeXでLLMする実装について

方針が決まったので、やってみました。

[texllm.tex]
% 以下のコマンドで処理する
% tcspingif -e pdflatex -t 4 texllm.tex
\documentclass{standalone}
\usepackage{tikz,tikzlings-mice}
\usepackage{ifthen}
\pgfmathsetmacro{\vC}{\the\faStopTicks}
\newcommand*{\cDrawMice}[1]{%
  \node at (120,135) {\tikz[scale=80,xscale=-1]{\mouse[#1]}};
  \node at (360,135) {\tikz[scale=80]{\mouse[#1]}};
}
\begin{document}
\begin{tikzpicture}[x=1bp, y=1bp]
\fill[red!75!blue!20, use as bounding box]
  (0,0) rectangle (480,270);
\ifthenelse{\vC=1}{%
  \cDrawMice{leftstep}
}{\ifthenelse{\vC=3}{%
  \cDrawMice{rightstep}
}{%else (\vC=0, 2)
  \cDrawMice{}{}
}}
\end{tikzpicture}
\end{document}

このTeXソースをtcspingifを用いてGIFアニメ画像に変換します。

tcspingif -e pdflatex -t 4 texllm.tex

出力結果のGIF画像🐭🐭
めでたしめでたし😊

まとめ

*「このネタだとLaTeX😊をフツーに使っただけでTeX言語🤮要素がないじゃん」
ZR「しまった😲」

というわけで、引き続きお題募集中です🙃

実行中のTypstのバージョンを取得したい話

先週、Typstの新しいバージョンである0.11.0版[2024-03-15]がリリースされた。この版ではintrospection周りの機能に大きな仕様変更が行われている1

このレベルの仕様変更は久しぶり2であるが、ただしChangeLogの情報を見るとわかるように、Typstでは各回の改版において何らかの細かい非互換的変更(breaking change)が行われることが多い。Typstはまだ新しいベータ版のソフトウェアであるため、今のところは「ソフトウェアも仕様の知識も常に最新のものに更新していく」という雰囲気が強く感じられる。しかしTypstの普及がもっと進めば、パッケージ開発者の側で「今動作しているTypstのバージョンを取得してそれによってパッケージの動作を変更したい」という要望も生じてくることだろう。

そういうわけで、本記事では「実行中のTypstのバージョンを取得する方法」について解説する。

前提知識

  • Typstのプログラミングのキホン的な知識。

バージョン判定のフツーの方法

マニフェストで最小要求バージョンを指定する

プログラムコードをパッケージ3として扱う前提で、かつ「指定のバージョンに満たない場合はエラー終了する」という動作で十分である場合は、Typstのパッケージシステムの機能が使える。

パッケージのマニフェストtypst.toml)にはcompilerという項目があり、これでコンパイラ(Typst)の最小要求バージョン」を指定できる。例えば、以下のマニフェストは、当該のパッケージ(mypackage)がTypstの0.11.0版以降を要求することを宣言している。

[package]
name = "mypackage"
version = "1.0.0"
entrypoint = "lib.typ"
compiler = "0.11.0"

従って、mypackageを例えば0.10.0版のTypstで使おうとすると、パッケージ読込の時点でエラーが発生する。

error: package requires typst 0.11.0 or newer (current version is 0.10.0)
  ┌─ \\?\C:\tmp\main.typ:1:8
  │
1 │ #import "@local/mypackage:1.0.0"
  │         ^^^^^^^^^^^^^^^^^^^^^^^^

パッケージを前提とするなら、この方法が簡単であり、かつバージョン指定が“明示的”であるという点でも好ましいだろう。

sys.versionを利用する

パッケージシステムの機能が使える事例に該当しない場合はプログラム中でバージョンを取得するコードを自分で書く必要がある。例えば「バージョンが0.11.0以降か否かによって実行されるコードを変えたい」という場合を考える。つまり、以下のような使い方のできる関数v11-or-laterを実装したい。

if v11-or-later() {
  // 新しいやつ🙂(0.11.0版以降)
} else {
  // 古いやつ🙁(0.11.0版より前)
}

実は、Typstの0.9.0版[2023-10-31]以降にはまさに「実行中のコンパイラのバージョン」を表す定数sys.versionが用意されている。従って、0.9.0版以降を前提にしてよいなら話は簡単になる。sys.versionはversion型の値であり、version型の値は(フツーのsemver的な意味で)大小比較が可能なので、所望のv11-or-laterは以下のように実装できる。

// Typstのバージョンが0.11.0版以降であるか.
let v11-or-later() = {
  sys.version >= version(0, 11, 0)
}

version(0, 11, 0)はversionのコンストラクタ呼出で「引数で指定した整数値をもつversion値」を生成する。

バージョン判定のアレな方法

sys.versionを使った方法は簡単であるが、当然ながら0.9.0版以降であることが前提になる。それより古いTypstではsys.versionが定義されていない(そもそもsysというモジュールが用意されていない)ので、上記のv11-or-laterを実行するとsysを参照しようとした時点でエラーになってしまう。

error: unknown variable: sys
  ┌─ \\?\C:\tmp\main.typ:4:2
  │
4 │   sys.version >= version(0, 11, 0)
  │   ^^^

もちろん、実際にバージョン取得の処理が必要になる頃には0.9.0版は既に“大昔のバージョン”で考慮4する必要がなくなっていそうから、実用上はほぼこれで問題がない可能性が高い。

それでも、ここでは敢えて「0.9.0版より前のバージョンでも安全に(エラーになることなく)実行できるバージョン取得」というアレな機能の実装を試みることにする。

※ただし先述の事情があるので、「0.9.0版より前の個別のバージョンの判別」は不要で「0.9.0版より前のものは単にそうであると判別できること」のみを要件とする。

アレしてみた

……というわけで、作ってみた

let v11-or-later() = {
  if ("\u{2212}" in str(-1)
      or "B" not in str(numbering("\u{3042}A", 2, 1))) {
    // 上の2条件の何れかが成立なら0.9.0版以降なのでsys.versionが使用可能
    sys.version >= version(0, 11, 0)
  } else { // 0.9.0版より前なので偽を返す
    false
  }
}

もちろん上記のコードであればもっと簡単に以下のようにも書ける。

let v11-or-later-x() = {
  (("\u{2212}" in str(-1)
      or "B" not in str(numbering("\u{3042}A", 2, 1)))
      and sys.version >= version(0, 11, 0))
}

それはともかく重要なのは2・3行目に書かれている条件でこれは「コンパイラが0.9.0版以降であるか」を判定している。この2条件の何れかが成立していればほぼ間違いなく0.9.0版以降と判断してよいので、その条件下ではsys.versionを自由に使って「所望のバージョン判定」を実装できる5わけである。

以下では「この2つの条件がどこから出てきたのか」について解説する。基本的には「改版による仕様変更によって動作が変わる点を補足する」という方針に従っている。

第1条件

"\u{2212}" in str(-1)

この式は0.9.0版以降少なくとも現在最新の0.11.0版まではtrue、0.9.0版より前ではfalseになる。ChangeLog0.9.0版の節に以下の項目がある。

The U+2212 MINUS SIGN is now used when displaying a numeric value, in the repr of any numeric value and to replace a normal hyphen in text mode when before a digit. This improves, in particular, how negative integer values are displayed in math mode.

str(-1)等の「負数を文字列に変換した結果」は0.9.0版より前では(他の多くのプログラミング言語と同様に)“-1”(U+002Dの後に“1”)であったが、0.9.0版以降では“−1”(U+2212の後に“1”)となる。恐らく数式で$-1$と書いた結果と合わせるためであろう。このため「str(-1)の結果にU+2212が含まれるか」を調べることで0.9.0版以降か否かが判別できる。

このstrの仕様変更はちょうど0.9.0版で起こっているため、もしこの仕様が今後も維持されるのであればこれだけで目的の「0.9.0版以降か否かの判定」が完遂できるはずである。しかし自分の直感としてはこの仕様が将来変更される可能性6を捨てきれない。そこで“保険”をかけるために入れているのが第2条件である。

第2条件

"B" not in str(numbering("\u{3042}A", 2, 1))

この式は0.11.0版ではtrue(そして将来の版でもほぼ確実にtrue)、0.11.0版より前ではfalseになる。ChangeLog0.11.0版の節に以下の項目がある。

Added support for contemporary Japanese numbering method

0.11.0版ではnumbering関数の書式文字列のカウンタ記号(counting symbol)として“あ”(ひらがなの五十音順)が追加された。つまり0.11.0版以降では以下のようになる。

numbering("あ)", 5) //==>"お)"

※参考記事:

従って、numbering("\u{3042}A", 2, 1)という式の値は以下のようになる(なおU+3042は“あ”である)。

  • 0.11.0版以降では"あA"は2つのカウンタ記号からなる書式と解釈されるので、2に“あ”、1に“A”が適用されて結果は"いA"となる。
  • 0.11.0版より前では“あ”はカウンタ記号ではなく"あA"はカウンタ記号“A”に接頭辞が付いた書式と解釈されるので、2と1の両方に“A”が適用されて結果は"あBあA"となる7

従って「結果に“B”が含まれない」こと8により0.11.0版以降であることを判定している。numbering関数は文書テンプレート作成者が常用する機能であるため、将来に「第2条件の式が再びfalseになる」ような仕様変更が入る可能性は極めて小さいと考えられる。従ってほぼ確実にこの式は「0.11.0版以降であるか否か」の判定に使えることになる。

合わせると

  • 第1条件は0.9.0~0.11.0版でtrueになることが判っている。
  • 第2条件は0.11.0版以降でtrueになることがほぼ確実である。
  • 一方で、0.9.0版より前では第1条件も第2条件もfalseになることが判っている。

以上より、“第1条件 or 第2条件”とすることで「0.9.0版以降か否か」、すなわち「sys.versionを利用できるか否か」を判別できることになる。

バージョン判定のアレアレな方法

同様の手法、すなわち「改版による仕様変更により動作が変わる点を補足する」という方法を活用することで「Typstの(正式リリースの)全てのバージョンを判定する」ようなモジュールを作ってみた。

  • [Typst: To get the version of Typst in use](Gist/zr-tex8r)

このtcversionモジュールは以下の値を提供する。

  • version: 実行中のTypstのバージョンを表す整数の配列9。例えば、0.11.0版であれば(0, 11, 0)となる。

※もちろん0.9.0版以降である場合はsys.versionを見ているので将来のバージョンも正しく判定できる。

モジュールの使用例を示す。

#import "tcversion.typ"
This is Typst version
#tcversion.version.map(str).join(".");.

例えばこの文書を0.6.0版のTypstでコンパイルすると以下の出力が得られる。

出力結果

まとめ

というわけで、皆さんは大昔のTypstのことはサッパリ忘れてフツーに新しいTypstを使っていきましょう!💁


  1. ただし、互換性のために従来の仕様も残している(一部は非推奨の扱い)ので、これ自体は非互換的な変更ではない。
  2. 過去にあった同じレベルの変更というと、例えば0.8.0版[2023-09-13]の「type型の導入」が挙げられる。
  3. 公式レポジトリに登録するパッケージとローカルにインストールするパッケージの両方を含む。
  4. もし考慮するにしても「そんな古いバージョンではエラー終了するのが妥当で、問題は単にエラーメッセージが的確でないくらいである」となる可能性が高いだろう。
  5. Typstは“動的な言語”なので、たとえ非存在のsys.versionを参照するコードがあったとしても、それが実際に実行されない限りエラーにはならない。
  6. 少し仕様が変わっても対応できる可能性を増やすため==での完全一致判定でなくinでの部分一致判定を使っている。
  7. numberingの書式文字列の仕様はかなりヤヤコシイがこの場合は接頭辞も反復される。
  8. 第1条件のときと同様に完全一致でなく部分一致で判定している。2に“A”が適用されて“B”が発生するか否かは「“あ”がカウンタ記号か否か」によって完全に決まると考えられるからである。
  9. 「version型」は0.9.0版で導入されたものでそれより前には存在しないので代わりに配列(array)を使っている。

メモ:新しいLaTeXの文書プロパティ機能の現状

コレに関する話。

blog.wtsnjp.com

文書プロパティについて語りたい

この記事の説明(オリジナルのLaTeX Newsの内容もほぼ同じ)を読むと、文書プロパティ1は従来の相互参照(\label\ref)の機構を拡張するもので、しかも値を“展開可能”な方法で取得可能であると説明されている。これを見る限り、この新機能によって、長年TeX言語プログラマを悩ませてきた「ラベルに紐づく値を取得する確実な方法がない」という問題が解決されるように思える。

しかし実際にチョット調べてみたところ、少なくとも現状ではこの希望的観測は的外れで、実際には文書プロパティの機能は「ラベルに紐づく値の取得」には使えなかった(ざんねん🙃)、という話。

「ラベルに紐づく値の取得」が難しい

以下にLaTeXのフツーの相互参照を利用した文書を示す2

\documentclass[a4paper]{article}
\begin{document}
\setcounter{section}{41}% 節番号を42から始める
\section{Duck}\label{sec:duck}% ラベルを付けた
Quack!
\section{Conclusion}
Section~\ref{sec:duck} (p.~\pageref{sec:duck}) is dull.
\end{document}

出力

この文書ソース中で、\ref{sec:duck}\label{sec:duck}を付与した節の番号、\pageref{sec:duck}は当該の節のあるページの番号を出力するために使われている。ここで注意すべきなのは、LaTeXの仕様としては\ref\pagerefはあくまで番号を「出力」する命令であり、番号を「取得」するための手段は用意されていない、ということである。

相互参照の使い方によっては単に番号を出力する以外の使い方をしたい場合もある。ここでは(極めて人工的な例であるが)次のような命令を実装することを考えてみよう。

  • \myRefSum{‹ラベル›}‹ラベル›に紐づくカウンタ3の番号(\ref{‹ラベル›}の値)とページ番号(\pageref{‹ラベル›}の値)の合計の値を出力する。

この命令を実装しようとすると、\ref\pagerefを単純に使って番号を出力するだけでは間に合わず、\ref\pagerefの値を(トークン列なり内部整数値なりの形で)取得する必要がある。しかしLaTeXの仕様ではそもそも値を取得するための手段が用意されていないので、結局、仕様に従う限りは実装は不可能になってしまう。

どうしても\myRefSumを実装したいのであれば「LaTeXカーネル内部実装に依存するコードを書く」という強硬策に頼る4ことになるが、その場合でも現実問題として相互参照周りの内部実装は様々な要因5で変動しやすいため、「特定の仕様通りに確実に動作する」ようなコードを実装する(そして維持する)のは極めて困難なのである。

新機能で「ラベルに紐づく値の取得」ができたらよいのに

新しい文書プロパティ機能では\RefPropertyという展開可能な命令でプロパティの値を取得できる。

  • \RefProperty{‹ラベル›}{‹プロパティ›}:[展開可能]‹ラベル›に紐づくプロパティ‹プロパティ›の値。

プロパティを定義及び記録する命令6は従来の\label\refとは別に存在するのであるが、一方で仕様書(ltproperties-doc.pdf)には次のようなことが書かれている。

  • カーネルで予めlabelpageというプロパティが定義されている。
  • labelは従来の相互参照における\refの値(ラベルに紐づくカウンタ番号出力)に相当する。
  • pageは従来の相互参照における\pagerefの値(ラベルに紐づくページ番号出力)に相当する。
  • 従来の\labelの「ラベル」はそのまま文書プロパティ機能における「ラベル」にもなる。

これを読む限りは、いかにも次のような仕組みになっていそうである。

  • \RefProperty{‹ラベル›}{label}により\ref{‹ラベル›}の値が取得できる。
  • \RefProperty{‹ラベル›}{page}により\pageref{‹ラベル›}の値が取得できる。

本当にそうなっているのか確かめてみよう。

\documentclass[a4paper]{article}
\begin{document}
\setcounter{section}{41}
\section{Duck}\label{sec:duck}
Quack!
\section{Conclusion}
label=\RefProperty{sec:duck}{label};
page=\RefProperty{sec:duck}{page}.
\end{document}

出力

ありゃ、うまくいかない(ざんねん🙃)

値の部分にはプロパティの「既定値」が出力されている。どうやら、ラベルに紐づくプロパティの値が記録されていないようである。つまり、少なくとも現状の仕様においては、従来の\label命令では新機能のプロパティの値は記録されないようにみえる。

余談:値が取得できないなら警告すべきでは

先の文書のビルドの際には警告は出ないのであるが、取得するプロパティ値が記録されていないなら(従来の相互参照において指定した\labelが見つからない時と同様に)警告が出てほしい気がする。実は警告を出す命令は別にある。

  • \RefUndefinedWarn{‹ラベル›}{‹プロパティ›}‹ラベル›に紐づくプロパティ‹プロパティ›の値が記録されていなければ警告を出す。

なぜ取得する命令\RefPropertyで警告を出さないのかというと、そうすると展開可能でなくなってしまうからである。\RefPropertyを使うプログラマが“適切なタイミング”で適宜\RefUndefinedWarnを実行する必要がある。

先の文書ソースの7行目に次のコードを追記する。

\RefUndefinedWarn{sec:duck}{label}
\RefUndefinedWarn{sec:duck}{page}

すると文書のビルド時に警告が出るようになる。

LaTeX Warning: Property `label' undefined for reference `sec:duck' on page 1 on
 input line 7.

LaTeX Warning: Property `page' undefined for reference `sec:duck' on page 1 on
input line 8.

やはりプロパティ値は記録されていないことが判明した。

どうにかして「ラベルに紐づく値の取得」してみる

当然であるが、「\labelを実行する際に新機能のプロパティの値を同時に記録するようにする」と\RefPropertyで値が取得できるようになる。例えば、以下のような命令\myLabelを定義してこれを\labelの代わりに使うという方法が考えられる。

※先述の通り相互参照と文書プロパティの「ラベル」は共通(名前空間を共有する)なので、同じ名前のラベルを両方に使うことはできない(ラベル重複になってしまう)。そのため、プロパティの方のラベルの名前には接頭辞(my/)を付けている。

%% \myLabel{<ラベル>}: 相互参照と文書プロパティの両方のラベルを置く.
% ※プロパティの方のラベルには"my/"の接頭辞を付ける.
\NewDocumentCommand\myLabel{m}{%
  \label{#1}% 相互参照のラベル配置
  \RecordProperties{my/#1}{label,page}% プロパティ記録
}

これにより\RefPropertyで値が実際に取得できるようになるので、今度は先述の\myRefSumの実装が可能になる。実際に\RefPropertyでの値の取得と\myRefSumの実行をする完全なコードを以下に示した。

\intevalは整数式の計算をする命令。

\documentclass[a4paper]{article}
%% \myLabel{<ラベル>}: 相互参照と文書プロパティの両方のラベルを置く.
% ※プロパティの方のラベルには"my/"の接頭辞を付ける.
\NewDocumentCommand\myLabel{m}{%
  \label{#1}% 相互参照のラベル配置
  \RecordProperties{my/#1}{label,page}% プロパティ記録
}
%% \myRefSum{<ラベル>}: 例のアレ.
\NewDocumentCommand\myRefSum{m}{%
  % 値が記録されてなければ警告
  \RefUndefinedWarn{my/#1}{label}%
  \RefUndefinedWarn{my/#1}{page}%
  % 両方の値が記録されていれば合計値を出力する
  \IfPropertyRecordedTF{my/#1}{label}{%
    \IfPropertyRecordedTF{my/#1}{page}{%
      % 展開可能なので \inteval 中で使用可能
      \inteval{\RefProperty{my/#1}{label}%
               +\RefProperty{my/#1}{page}}%
    }{}%else
  }{}%else
}
\begin{document}
\setcounter{section}{41}
\section{Duck}\myLabel{sec:duck}
Quack!
\section{Conclusion}
% 番号を取得する例
label=\RefProperty{my/sec:duck}{label};
page=\RefProperty{my/sec:duck}{page}.\par
% \myRefSum の例
sum=\myRefSum{sec:duck}.
\end{document}

出力

なぜこんな仕様なのか

仕様書(ltproperties-doc.pdf)に次のような記述がある。

Currently the code has nearly no impact on the main \label and \ref commands as too many external packages rely on the concrete implementation. There is one exception: the label names share the same namespace. That means that if both \label{ABC} and \RecordProperties{ABC}{page} are used there is a warning Label ‘ABC’ multiply defined.

(訳)
現状では、大本の\labelおよび\ref命令はこの[新機能実装の]コードの影響をほぼ受けない。外部パッケージでその具体的な実装に依存したものがあまりに多いからである。一つだけ例外がある:ラベルの名前は名前空間を共有している。つまり、\label{ABC}\RecordProperties{ABC}{page}の両方を使うと、Label ‘ABC’ multiply definedの警告が発生する。

従来の相互参照と文書プロパティを「全く無関係な別個のもの」と位置付けるならば、両者でラベルの名前空間を共有するのは明らかに不合理なはずである。共有させているということは、恐らくLaTeXチームは「究極的には両者を統一したい」と考えているようにも思える。今は既存のパッケージの問題があって実現できていないようであるが、将来的には何か手を打つのがもしれない。

まとめ

とりあえず今のところは、ざんねん🙃(ざんねん🙃)


  1. 「文書プロパティ」はLaTeXの概念であり、PDFの「文書のプロパティ」とは無関係である。
  2. 相互参照を利用しているので、文書のビルドの際には2回タイプセットが必要である。以降の例の文書でも同様。
  3. 話を簡単にするため、当該のカウンタ(とページカウンタ)の表示書式は算用数字であることを仮定する。つまり、カウンタ番号のトークン列はカウンタ値の整数として通用する。
  4. 主に「\ref\pagerefの『実装において展開可能として動作する実行パス』を利用する」という方法と「相互参照情報を保存するマクロr@‹ラベル›を直接操作する」という2つの方法がある。
  5. hyperref等のパッケージの読込によって実装が置き換わる。また、最近のLaTeXの改修でも相互参照周りの実装が変動していて、従来の実装が動作しないという不具合が発生している。
  6. 定義する命令が\NewPropertyで、記録する命令が\RecoedProperties

100万回ハローワールドするTeX言語的な方法

とあるプログラミング言語1が超絶アレなためムシャクシャしたので、チョットTeX芸してみた。

[↓お題]

[↓結果🙃]

※上記のプログラムはplain TeX用のものである。texコマンドでコンパイルするとDVIファイル、pdftexコンパイルするとPDFファイルが得られる。以下、この記事で扱うコードはplain TeXを前提とする。

とりあえず

なるべくTeX言語特有の変態な方法を使う

という方向性にこだわってみた。

参考:フツー(?)の方法

なお、元ネタで使っている方針はもちろんTeX言語でも使える。TeX言語チョットデキル人であれば思いつくであろう2

\let\z.\def~{\z}\def\y{\edef~{~~~~~~~~~~}}\y\y\y\y\y\y
\def\z{Hello world!\par}~\bye

TeXを対話モードで使っていて「反復処理を書きたい」という場合に、このパターンを実際に使うことがたまにある。(TeXでループを書くのは面倒なので。)

変態な方法

{\catcode`\m=\active\gdefm{\hbox{Hello world!}$\par$}}
\mathcode`m="8000$\romannumeral1000000000\relax$\bye

ポイントは\romannumeral1000000000である。ローマ数字で“10億”を出力しようとしているが、(TeXの)ローマ数字で最大の数字は“m=1000”なので、結果的にTeXはこのコードをmを100万個並べた文字列に展開する。これでループ的な実行制御なしで“同じトークン100万個”を得ることに成功した🙃

ただしこのmのカテゴリコードは123なので、このままでは“m”以外のテキストを出力するのには使えない。カテゴリコードが12の文字トーク4でマクロを実行させたい……となると、math-acriveを使うことが考えられる。

文字のmath codeを"8000に設定することを“math-active”という5。文字をmath-activeにすると、数式モード中で当該の文字の(カテゴリコードが11または12の)文字トークンが実行されたときに「代わりにその文字のアクティブ(カテゴリコード13)な文字トークンが実行される」という動作になる。標準のplain TeX(やLaTeX)では数式モード中での'の入力でプライム記号(\prime)が上添字として出力されるが、この挙動は'をmath-activeにすることで実現している。

今の場合はmの入力で“Hello world!”を出力させたいので、アクティブなmにマクロを定義した上で\mathcode`\m="8000を設定する。その上で、数式モードに入って\romannumeral1000000000を実行すればよいことになる。ただし数式モードに入るのは飽くまでmath-activeのためで文字列自体は非数式で出力したいなので\hboxを使う。さらに「数式モード中では改段落ができない」のを回避するために「一旦数式モードを終結してから改段落してまた数式モードに入る」という対策をとった。

まとめ

新しいテフライブが無事にリリースできるといいですね😊(まとめろ)


  1. ただしTeX言語以外😲
  2. 先頭の\let\z.は「\zを(一時的に)展開不能にする」ために入れている。1行目を実行した時点で~の意味は「\zを100万個並べたもの」に展開されるマクロになる。
  3. \romannumeralの展開結果は\the文字列なのでこのmのカテゴリコードは11ではなく12である。
  4. e-TeXを前提するなら\scantokensを使うという手段もありそうだが、これは実際にやってみると\scantokensのバッファが100万文字に耐えられずに失敗した。
  5. もしかしたら“math-active”はオレ用語なのかもしれない🙃

Typstで“calc.abs(-8)”はメソッド呼出なのか

Typstのメソッド呼出を完全に理解する話

Typstの一部の型はメソッドをもつ。例えばarray型の値は自身の長さ(要素数)を取得するためのlen()メソッドをもっている。

#let ary = (1, 2, 3)
#ary.len() //==> 3

ここで注意すべきなのは、これはary,lenという(function型の)フィールドに関数呼出の括弧を付けたものではない、ということである。実際、array型の値aryにはary.lenというフィールドは存在しない1

#ary.len //--> error: cannot access fields on type array

Typstにおいてフィールドの参照とメソッドの参照が異なる概念であることはdictionary型をみればさらに明らかになる。以下の例をみてわかるように、フィールドとメソッドの空間は全く別になっている。

#let dict = (foo: calc.abs, len: 42)
#dict.len     //==>42
#dict.len()   //==>2
#dict.foo     //==>abs (function値の表示)
#dict.foo(-8) //-->error: type dictionary has no method `foo`
#dict.keys    //-->error: dictionary does not contain key "keys"
#dict.keys()  //==>("foo", "len")

ということは、Typstではval.name(...)という形2の式は「メソッド呼出」を表すものであり、これとval.nameの形の「フィールド参照」とは全く別のものである、といえそうである。もし「フィールドのfunction値を呼び出す式」を書きたいのなら、val.name(...)という“形式”を回避する必要があり、簡単な方法としてはval.nameの部分に括弧を付ければよい。

#(dict.foo)(-8) //==>8 ('calc.abs(-8)'の値)

“メソッド呼出の意味論”についてはTypstの公式のドキュメントに説明がある。

すなわち、val.name(...)というメソッド呼出はtype(val).name(val, ...)と等価になる。先の例でdict.len()type(dict)dictionary3であるので次の式と等価になり、これは実際に2を返す。

dictionary.len(dict)

Typstのメソッド呼出がなにもわからない話

ところで先のdictionaryの例でcalc.absという関数を使った。これは組込のcalcモジュール(module型の値4)に属している関数で、数値の絶対値を返すものである。通常はcalc.absに関数呼出の括弧を付けて使う。

#calc.abs(-8) //==>8

何の変哲もないコードであったはずだが、ここで先の考察を踏まえるとある疑問が湧いてくる。このcalc.abs(-8)というのは「メソッド呼出」なのであろうか?

この式はまさにval.name(...)という形なので形式の上ではメソッド呼出のはずである。ただし先のdictionaryやarrayの話と決定的に異なる点がある。calc.absは実際にcalcのフィールドとして存在するのである。これはcalc.absの部分に括弧を付けても呼び出せることからわかる。

#(calc.abs)(-8) //==>8

これを踏まえるとcalc.abs(-8)は「calc.absというフィールド値に関数呼出の括弧を付けた式」でありメソッド呼出でない気がしてくる🤔

やっぱりメソッド呼出でありそうな話

こういう関数を考える。

#let call-len(val) = val.len()

Typstは動的型の言語であるため、valの型は実行時にしか決まらない。もしここで、valにarrayの値とmoduleの値のどちらも受け付けるのであれば、val.len()という1つの式が成立する以上「calc.abs(-8)ary.len()とは異なる構文である」ということはありえないことになる。実際に確かめてみよう。

[mod.typ](len()という関数をもつモジュール)
#let len() = 42
[main.typ](このファイルを実行する)
#import "mod.typ"
#let call-len(val) = val.len()
#let ary = (1, 2, 3)
#let dict = (foo: calc.abs, len: 42)
#call-len(ary)   //==>3
#call-len(dict)  //==>2
#call-len(mod)   //==>42

“期待通り”の結果になった。ということは、やっぱりcalc.abs(-8)はメソッド呼出である……?🤔🤔

やっぱりメソッド呼出でなさそうな話

calc.abs(-8)がメソッド呼出であるなら、先ほど紹介した“メソッド呼出の意味論”を満たすはずである。つまり、type(calc)moduleであるからcalc.abs(-8)は以下と同値になる。

module.abs(calc, -8)

つまり、module(type値)にはmodule.absというフィールド5がありその値は「引数のモジュールのabsフィールドの関数を呼び出す」という役割をもった関数、ということになる。もちろんモジュール内の関数名には任意の識別子が使えるので、この理屈に従うと「moduleにはありとあらゆる名前のフィールドが定義されている」というオソロシイことになる。まあ論理的にありえない話ではないので、実際に確かめてみよう。

#module.abs           //-->error: type self does not contain field `abs`
#module.abs(calc, -8) //-->error: type self does not contain field `abs`

どうやらそんなオソロシイ話はなかったようである😊 でもこれだとやっぱりcalc.abs(-8)はメソッド呼出ではない……?🤔🤔🤔

Typstのメソッド呼出がチョットデキル話

なにもわからなくなったので、処理系の実装をみてみよう。

詳細の説明は(メンドクサイので🙃)省くが、やはり、val.name(...)の形式の式の実行においてはvalの型によって解釈を変えているようである。

  • valの型がsymbol、function、type、moduleの何れかである場合6は、フィールドval.nameの値に対する関数呼出と解釈する。
  • それ以外の場合は先述の“メソッド呼出の意味論”に従う。

つまり結論としては:

  • val.name(...)val.nameとは全く別の構文である。
  • しかしvalの型によって「メソッド呼出」になったり結局「val.nameの関数呼出」になったりする。
  • calc.abs(-8)は後者に該当するので「メソッド呼出」ではない

まとめ

皆さん、そんな細かいことは一切気にせずに、どんどんTypstしましょう😃


  1. そもそも、array型の値はフィールドを一切持っていない。
  2. nameは単一の識別子に限るが、valの部分は任意の式でよい。
  3. つまり、トップレベルでdictionaryとして定義されているtype型の値。
  4. 意外かもしれないがTypstではモジュールは第一級値(first-class value)である。
  5. valがtype値である場合のval.name()は(module値であるときと同様に)フィールドのval.nameの関数呼出と同じ動作になる。例えばdictionary.lenというフィールドは実際に存在する。
  6. 本当はこの場合でもtype(val).nameのフィールドが存在する場合は“メソッド呼出の意味論”が優先されるようである。ただ型の性質を考える限り、この4つの型にメソッドが設定される可能性はほぼなさそうである。

2024年のパズル年賀状

今年の年賀状。

以前に述べたとおり、年賀状にはその年の数に関連した数学パズルを載せるのが通例である1。去年も割と余裕をもってパズル問題を作ることができて、結局(例によって)チョット変わった虫食い算になった。

肝心のパズル問題の部分の文字が(例によって)小さくで読みづらいが、以下のように書かれている。

次の条件に従って掛け算の虫食い算を解きなさい。

  •  の 6 マスおよび  の 6 マスはそれぞれ 6 面ダイス(立方体のサイコロ)の展開図になっている。

※ 6 面ダイスの向かい合う面に書かれた数の和は 7 でなければならない。

(問題の図)

一番上にあるのはベトナム語(ただしチュノム😲)での新年の挨拶。


  1. Typst用の文書テンプレートの開発に時間を取られている人が多いためか、年を追うごとに年賀状で数学パズルを見ることが少なくなっているようで感じる。