マクロツイーター

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

Typstだって文献リストの直書きをしたい

LaTeXで文献リストを直書きする話

LaTeX文献リスト(“参考文献”の節に出すリスト)を出力する方法というと、BibTeX1を利用する手順が紹介されることが多い。

\bibliographystyle{jplain}% 文献スタイル(*.bst)を指定する
\bibliography{zr-bib}% 文献データベース(*.bib)を指定する

BibTeXを使う場合、文献リストの体裁(句読点の付け方や細目の表現など)は「文献スタイル」の制御下にあるため、体裁に微調整が必要な場合に時として苦行(BeaST言語)が発生することになる。

この苦行を回避するための方策として「BibTeXを使わずに文献リストを“直書き”する」というものがある。LaTeXには文献リストを“直書き”するための環境であるthebibliography環境が用意されている。

LaTeX文書1]

\documentclass[uplatex, dvipdfmx, a4paper]{jsarticle}
\begin{document}
すごい論文\cite{zr-art}および本\cite[8章]{zr-book}によると,
{\TeX}はアレ.
% thebibliography環境で文献リストを"直書き"できる.
\begin{thebibliography}{[9]}
\bibitem{zr-art} 某ZR.メッチャすごい論文,2020.
\bibitem{zr-book} 某ZR.メッチャすごい本,2022.
\end{thebibliography}
\end{document}

LaTeX文書1の出力

この例では文献の番号は自動採番に従うが2\bibitemにオプション引数を指定することで任意の文字列を“番号”として使える。

LaTeX文書2]

\documentclass[uplatex, dvipdfmx, a4paper]{jsarticle}
\begin{document}
すごい論文\cite{zr-art}および本\cite[8章]{zr-book}によると,
{\TeX}はアレ.
\begin{thebibliography}{[MMM99]}
% オプション引数に手動で"番号"を指定する.
\bibitem[ZR20]{zr-art} 某ZR.メッチャすごい論文,2020.
\bibitem[ZR22]{zr-book} 某ZR.メッチャすごい本,2022.
\end{thebibliography}
\end{document}

LaTeX文書2の出力

Typstで文献リストする話

Typstで文献リストを出力するために用意されているのはbibliography関数である。

#bibliography(
  "zr-bib.yaml", // 文献ファイル(Hayagriva形式)
  style: "ieee", // 文献スタイル(CSLファイルまたは内蔵)
)

ここで第1引数は「文献ファイル3」で、BibTeX形式またはHayagriva形式4のファイルを指定する。style引数は「文献スタイル」で、CSL形式のスタイル定義ファイルを指定するが、いくつかのスタイルについては定義が処理系に内蔵されていて、それらは名前(ieeeなど)を指定することで利用できる。

Typstで文献リストを直書きできない話

明らかにTypstのbibliography関数はLaTeX\bibliography命令に相当するもの、すなわちBibTeXを使う手順に相当する。そうすると当然「体裁に微調整が必要な場合」の対処が気になってくる。ところが、Typstの標準の機能には「文献リストを“直書き”する」ためのものがないようである。bibliography関数では明らかにできないし、それ以外の「文献リストを出力するための関数」というのも見当たらない。

一見すると「“直書き”の文献リストは単なる箇条書きである」から適当にカスタマイズしたenum関数を使えばよいようにも思える。

[Typst文書1]

#set text( // 和文出力用の最低限の設定
  lang: "ja",
  font: "Harano Aji Mincho", size: 10pt,
  top-edge: 0.88em, spacing: 0.25em,
)

// 文献リストを"直書き"した
#set heading(numbering: none)
#set enum(numbering: "[1]")
= 参考文献
+ 某ZR.メッチャすごい論文,2020.
+ 某ZR.メッチャすごい本,2022.

Typst文書1の出力

ところが実際にはこの方針はうまくいかない。文献リストは出力できるが、その中の文献項目を参照することができないのである。

[Typst文書2]

// 和文出力用の最低限の設定(先の例と同じ)は以後省略

// 参照しようとするとエラーになる😭
すごい論文 @zr-art によると…

#set heading(numbering: none)
#set enum(numbering: "[1]")
= 参考文献
// ラベルを付けてみた
+ 某ZR.メッチャすごい論文,2020.<zr-art>
+ 某ZR.メッチャすごい本,2022.<zr-book>

コンパイル時のエラー)

error: cannot reference text
  ┌─ \\?\C:\tmp\holen\jan.typ:8:6
  │
8 │ すごい論文 @zr-art によると…
  │            ^^^^^^^

この参照が失敗するのは、Typstの仕様ではそもそも箇条書きの項目は“参照可能(referenceable)ではない”からである5

Referenceable elements include headings, figures, equations, and footnotes. To create a custom referenceable element like a theorem, you can create a figure of a custom kind and write a show rule for it.

Typst Documentation: ref

この参照可能要素に関する制限があるため、単純に「マークアップとset ruleの設定」でできる範囲で「“直書き”の文献リスト」として機能するものをユーザが作り上げるのは恐らく無理であろう。

それでもTypstで文献リストを直書きしたい話

結局のところ、Typstは「文献リストを“直書き”する」機能は不要だと判断していると推測される。しかし、特に“和文対応”を前提にした場合に、その判断は妥当だろうか?

Typstの文献スタイル指定にはCitation Style Language(CSL)という言語が採用されている。これは文献項目の書式をXMLで宣言的に記述したものであり、プログラミング言語であるBeaST言語とは性質が異なる。このため(特に非プログラミング者の視点で)CSL形式の文献スタイルの改修6BibTeXよりは容易であろう。一方でプログラミング言語ではないため「想定外の複雑なスタイルは表現できない」ことが予想される。

この観点で検討した場合に真っ先に問題になりそうなのが「和欧文文献混在」のパターンである。日本における文献リストの書式の要件として「一つの文献リストの中で和文の文献7と欧文の文献では異なる書式(句読点の選択8など)を用いる」というものがある。

和文と欧文の書式が異なる例(句読点にも注意)

この要件は“日本であまり普及していない文献ツール”を利用するときに度々問題になる。例えば以下に挙げる記事はbiblatexでこの要件を扱う方法を述べたものである。

この辺りのCSLの対応状況を調べるため公式サイトを見ると、早々に白旗が上がっていることがわかる😭

But even a well-coded style still has some limitations: CSL doesn’t yet allow for per-item localization (for example, some styles require Japanese items to be cited in Japanese, and English items in US English), and doesn’t always support all grammatical peculiarities of your favorite language.

(Authors - Citation Style Language)

少なくとも現状では、CSLは日本での「和欧文文献混在」の要件には対応できないようである。従って、CSLに完全に依存しているTypstの文献リスト機能は現状では不十分であると言わざるをえない。

もちろんこれは現時点での話であり、将来にはCSLやTypstの側で解決されることが期待できる。それまでの間は「文献リストを“直書き”する」というのが簡単な“回避策”となるはずである。

Typstで文献リストを直書きする話

というわけで、Typstで「文献リストを“直書き”する」ためのパッケージ“bxbibwrite”を作ってみた😃

bxbibwriteをインストールする話

公開レポジトリにはまだ登録していなので、各自ローカルでインストールする必要がある。以下の記事が参考になる。

  • 【ここに何かTypstのローカルのパッケージのインストール方法を日本語で解説した記事のリンクを貼る🙂】
    ……ことができればよかったのだけど、そういう記事をまだ誰も書いていない😭
    仕方がないので、公式解説へのリンクを示すことにする。
    Typst Packages / Local packagesGitHub/@typst)

bxbibwriteを使う話

パッケージの読込は以下の通り。importで読み込んだ後、use-bib-item-refをshow ruleで有効化する。

※バージョン0.2.0の部分は実際のものに合わせること。

#import "@local/bxbibwrite:0.2.0": *
#show: use-bib-item-ref

基本的なインタフェースはLaTeXthebibliography環境に合わせている。文献リストを出力するのがbibliography-list関数であり、その引数の内容の中でbib-item関数で各々の文献項目を記述する。

#bibliography-list(
  title: "参考文献", // 節見出しの文言
)[
#bib-item(<zr-art>)[某ZR.メッチャすごい論文,2020.]
#bib-item(<zr-book>)[某ZR.メッチャすごい本,2022.]
]

ここでbibliography-listのオプション引数titleは文献リストの節見出しの文言である(既定値は“Bibliography”)。bib-itemの第1引数に文献項目に紐づけるラベルを指定する。

文献の参照(“[1]”等の文献番号の出力)は、Typstの本来の文献リスト(bibliography関数)の場合と同様9ref関数(またはその簡易表記の@ラベル名)で行える。補足説明を同時に出力するためにsupplement引数10を指定することもできる。

[Typst文書3]

////(ここに和文出力用の設定が入る)
// bibwriteパッケージ読込
#import "@local/bxbibwrite:0.2.0": *
#show: use-bib-item-ref

すごい論文 @zr-art や本 @zr-book[8章] によると、☃は素敵。

// 文献リストを"直書き"した
#bibliography-list(
  title: "参考文献", // 節見出しの文言
)[
#bib-item(<zr-art>)[某ZR.メッチャすごい論文,2020.]
#bib-item(<zr-book>)[某ZR.メッチャすごい本,2022.]
]

Typst文書3の出力

bxbibwriteをもっと使う話

文献番号を自動採番ではなく手動で指定したい場合は、bib-item関数のオプション引数keyを利用する。

[Typst文書4]

////(ここに和文出力用の設定が入る)
#import "@local/bxbibwrite:0.2.0": *
#show: use-bib-item-ref

すごい論文 @zr-art や本 @zr-book[8章] によると、☃は素敵。

#bibliography-list(title: "参考文献")[
// key引数で"番号"を手動で指定できる.
#bib-item(<zr-art>, key: "ZR20")[某ZR.メッチャすごい論文,2020.]
#bib-item(<zr-book>, key: "ZR22")[某ZR.メッチャすごい本,2022.]
]

Typst文書4の出力

文献番号の出力書式は既定では“[1]”のようになるが、これを“(1)”に変更したい場合は、use-bib-item-ref関数のオプション引数numberingを指定する11。具体的には、パッケージ読込時のshow ruleを以下のように書き換える。

#show: use-bib-item-ref.with(numbering: "(1)")

[Typst文書5]

////(ここに和文出力用の設定が入る)
#import "@local/bxbibwrite:0.2.0": *
// 文献番号の書式を変更する.
#show: use-bib-item-ref.with(numbering: "(1)")

すごい論文 @zr-art や本 @zr-book[8章] によると、☃は素敵。

#bibliography-list(title: "参考文献")[
#bib-item(<zr-art>)[某ZR.メッチャすごい論文,2020.]
// "番号"を手動で変更した.
#bib-item(<zr-book>, key: "1a")[某ZR.メッチャすごい本,2022.]
]

Typst文書5の出力

さらに、use-bib-item-ref関数とは別にbibliography-list関数にもオプション引数numberingが存在する。これは文献リストの中の文献番号だけ書式を変更するためのものである。

[Typst文書6]

////(ここに和文出力用の設定が入る)
#import "@local/bxbibwrite:0.2.0": *
// 文献参照での番号の書式は変えない("[1]"のまま).
#show: use-bib-item-ref

すごい論文 @zr-art や本 @zr-book[8章] によると、☃は素敵。

#bibliography-list(
  title: "参考文献",
  numbering: "☃1", // ゆきだるま😊
)[
#bib-item(<zr-art>)[某ZR.メッチャすごい論文,2020.]
#bib-item(<zr-book>, key: "1a")[某ZR.メッチャすごい本,2022.]
]

Typst文書6の出力

まとめ

Typstの標準の文献リスト機能の“和文対応”が早く完璧になってほしいですね😃


  1. 海外では新しいツールであるbiblatexも普及しているが、恐らく和文対応のノウハウが蓄積されていないせいで、日本ではあまり普及していないようである。
  2. なお、thebibliography環境の引数の[9]はリスト出力での番号部分の横幅を決めるためのものである。(番号の書式を決めるものではない。)
  3. BibTeXの「文献データベースファイル」に相当するもの。
  4. HayagrivaはTypstのために新たに開発された文献管理ライブラリである。
  5. さらに言うと、+ 某ZR…,2020.<zr-art>と書いた場合にラベル<zr-art>が付けられている要素は単なるテキストの塊の某ZR…,2020.であり、箇条書きの項目(enum.item要素)にはなっていない。
  6. GUIベースの改修ツールも公開されているようである。
  7. ここで「和文の文献」とは「リスト中で和文で表記される文献項目」のことである。もちろん、日本語で著されている文献を(英訳などをして)欧文表記の文献項目として扱うという方法もある。
  8. 当然、和文で表記されているなら句読点は和文の規則に従うべきである。
  9. ただし、本パッケージの文献リストの参照にcite関数を使うことはできない。
  10. 簡易表記@ラベル[補足]において、後ろに付けた内容ブロック(content block)がsupplement引数である。この簡易表記は#ref(<ラベル>, supplement: [補足])と同等になる。
  11. numbering引数に指定するのは書式指定用のパターン文字列である。これはTypstのnumbering関数の第1引数などで用いられるパターン文字列と類似の記法に従うが、「手動で設定した番号の文字列」や「supplement引数の内容」がカウンタ記号の位置に挿入されるなど、若干の相違点もある。詳細については説明書(READMEファイル)を参照してほしい。

非プログラミング者のためのTypst公式ドキュメント攻略ガイド

いまTypstの学習を始めるとなると、一番重要な情報源となるのは何といってもTypstの公式のドキュメントでしょう。現状ではユーザによる解説記事はまだ絶対数が少なく、そこから“正解”の情報を導き出すのは極めて困難だからです。しかしこの公式ドキュメントにも弱点があります。

一つは「Typstの機能の全てを網羅しているため単純に量が多すぎる」ということです。チュートリアルから読み始めればよいことは明らかだとしても、それだけでは(特に日本語の文書を作成するには)不十分なので、他の部分も読む必要が生じます。そのときにTypstの機能全体の概略がつかめていないと「どこを見ればいいのか」が判断できません。

もう一つ、特に非プログラミング者にとって深刻な問題は「一部のページを読むのにプログラミングの知識1が必要になる」ということです。Typstのプログラミング言語は「容易に学習できること」に重点をおいて設計されている(雰囲気としてはPythonに近いです)のですが、そうはいってもプログラミングの概念を含んだ解説を非プログラミング者が読んで理解するのはかなり辛いことになるでしょう。

※Typstは「非プログラミング者であっても簡単なレイアウトの調整ができる」ように設計されています。将来には“複雑な調整”(特に和文文書に対するサポート)については「プログラミングできる人が作ったパッケージ」を利用して対応する態勢ができるかもしれませんが、一方で「簡単なレイアウトの調整」については「文書作成者がやるべきこと」という扱いになりそうです。(少なくとも現状ではそうです。)

これらの弱点に対処するため、非プログラミング者の視点にたって「Typstをこれから学ぶときに前もって(あるいは早い段階に)読んでおくべき公式ドキュメントのページ」を厳選してみました。本記事ではその一覧を紹介します。

Reference以外

  • Overview
    表紙です。1ページの短い内容なので読みましょう。

  • Tutorial
    文書作成者向けの4ページのチュートリアルです。必ず読みましょう。

  • Guides
    特定のユーザ層およびユースケース向けの解説です。

    • Guide for LaTeX users
      LaTeX経験者の人はぜひ読んでおきましょう。

    • Page setup guide
      日本語文書を作成したい人なら、現状ではページ設定の知識は必須です。ある程度の前提知識が要るので、Typstの文書(マークアップ)を作るのにある程度慣れた段階で読んでおきましょう。

Referenceの中

  • Reference
    Referenceの表紙です。

  • Syntax
    一度目を通しておきましょう。3つのモード(マークアップ、数式、コード)の違いについては最初に理解しておいた方がよいでしょう。コードモード(code mode)の文法は非プログラミング者には不要なのでスキップしてかまいません。マークアップと数式の文法は、最初は「どんなものがあるのか」をざっと把握しておけばよいでしょう。後で必要に応じて何度も見返すことになるはずです。

  • Styling
    先述の“Page setup guide”を読む段階になったときに一緒に読むといいでしょう。

    ※ただし「show rule」については「show-set rule」だけ把握しておけば十分です。一般のshow ruleはプログラミングが関わることが多いので、単に「そういう文法もある」とだけ覚えておきましょう。

  • Scripting
    これはプログラミング言語のガチな解説であるため、非プログラミング者が読んで理解するのはかなり困難でしょう。しかしこのページの中には「非プログラミング者でも知っておくべき内容」も含まれています。「Styling」を読見終わったなら、ついでに以下の節は読んでおきましょう。

    • Expressions
      #とは結局何なのか」についての解説です。
    • Modules
      ファイル分割やパッケージ読込の際に必要になります。
    • Packages
      パッケージ読込の際に必要になります。
  • Math
    数式の文法の解説です。数式を書きたいなら必ず読みましょう。
  • Symbols
    簡易記法(テキスト中の---や数式中の<=など)の一覧があります。

“死角”コーナー

入門段階で「解説がどこにあるのか判りにくい」機能の説明のページを集めてみました。

  • par(要素)
    段落に関すパラメタです。
  • text(要素)
    テキスト属性(サイズ・フォント・色など)に関するパラメタです。
  • equation(要素)
    $~$math.equation要素の簡易構文なので、数式自体のパラメタについて知りたい場合はこれを読みましょう。
  • sym(モジュール)
    数式中の“記号の名前”(timessubset.eqなど)の一覧です。
  • styles
    variants
    数式中の書体指定(boldcalなど)の解説です。
  • page(要素) ページレイアウトに関するパラメタです。
  • length(型)
    ratio(型)
    relative(型)
    fraction(型)
    長さとして使われる値です。

まとめ

Typstは”プログラミングしない人”でもある程度使いこなせるので、非プログラミング者な人もドンドンTypstしましょう!😃


  1. 特定のプログラミング言語の知識ではなく、“いかにしてプログラミングをするのか”という考え方に関する知識です。“Typstのプログラミング言語”についてはもちろん公式ドキュメントで解説がされています。

野生の難解TeX言語クイズ

徒然に某キ~タでTeX/LaTeX関連の記事を拾い読みしていたら、トッテモ面白い🤯TeX言語のコードを発見した。ちょうど「TeX言語GW特別キャンペーン🍀」の期間中なので、クイズの形で紹介することにする。

そのコード

qiita.com

最近よくみるtcolorbox関連の記事であるが、今の話において重要な部分のコードを抜粋する。

\newcommand{\kara}{} %無を出力するコマンド
\newtcolorbox[auto counter, number within=section, crefname = {Def.}{Defs.}]{definition}[3][]
{enhanced, breakable = true, fonttitle = \bfseries,
title = Def.~\thetcbcounter~\if #2\kara \else#2\fi,
#1,
label = thm:#3}

\newtcolorboxで新しい(tcolorboxの機能を利用した)“definition環境”を定義するコードであるが、注目してほしいのはこの部分である。

\if #2\kara \else#2)␣\fi

※「空白トークンになる空白」をで表した1

ここで、#2は新しく定義されたdefinition環境の第2引数(第1引数はオプション引数なので最初の必須引数)である。すなわち、definition環境は以下のような書式をもつが、この中の«名称»の部分が#2になる。

\begin{definition}[«追加オプション»]{«名称»}{«ラベル»}
«内容»
\end{definition}

記事の後の方を見ると、次のような記述がある。

つまり「#2が空になる」という使用法も想定されていることがわかる。では #2 が空だった場合に何が起こるのか、というのがこの記事(クイズ)のトピックである。

実際に#2を空にしたコードを考えてみよう。

\if \kara \else()␣\fi

\ifの直後に\karaがあるが、これは以下のように定義されたマクロである。

\newcommand{\kara}{} %無を出力するコマンド

\ifは「展開不能2つのトークンを引数に取る」という仕様をもつため、\ifの直後に展開可能なトークンがある場合はそれは展開される。\karaを展開すると単に消えるだけなので、結局、\kara はこのif文の実行結果に何の影響も与えない。要するに、先のif文は次のコードと等価になる。話を簡単にするため以下ではこのコードを考える。

\if \else()␣\fi

この\if文は、明らかに奇怪な形をしている。普通は\ifの直後に比較対象となる2つのトークンがあるはずだが、この文ではそれがなくて\ifの後にいきなり\elseが出現している。

というわけでクイズ

\if \else()␣\fi

このトークン列を完全展開した結果はどうなるか? 次の選択肢の中から正しいものを選ぼう。

  1. ()␣ (ただしは空白文字トークン)
  2. トークン列
  3. \relax
  4. 単一の“\relaxモドキ”からなるトークン列
  5. その他

※“\relaxモドキ”とは「実行された場合の挙動は\relaxプリミティブと全く同一だが、\relaxプリミティブとは意味が異なる(\ifx-等価でない)ような特殊なトークン」のことを指す。

(正解は後日発表🙃)


  1. ただし「そこに空白トークンがあること」はこの記事の話題とは無関係である。

“TeX”で2次方程式の解の公式を出力する話

「“TeX”とは何を指すのか」というのは常に悩ましい問題1であるが、自分の感覚としては「“TeX”とはplain TeX(のみ)を指す」という習慣は少なくとも現代において2は「“TeX”とはLaTeX(のみ)を指す」という習慣と同類である(要はあまり妥当でない)と考えている。

となると、「LaTeXのコードは“TeXのコード”ではない」という前提において、何だったらTeXのコード”といえるだろうか。この前提の下では「plain TeXのコード」もその他のフォーマットのコードも“TeXのコード”ではなさそうである。唯一可能性があるのは「本当にTeX処理系本来の機能のみを前提にしたコード」、つまり「iniTeXTeXのINIモード3)で動くコード」ということになるだろう。

というわけで、本記事では、この「iniTeXで2次方程式の解の公式を組版して出力するコード」についてグダグダと雑に解説していくことにする。

※雑談なので前提知識を厳密には決めないが、TeX言語🤮のキホン的な知識はあった方が楽しめると思う。

使い方を説明してみる

TeX: To typeset the quadratic formula with iniTeX · GitHub

[ini-formula.tex]
\catcode`\{=1 \catcode`\}=2 \catcode`\$=3 \catcode`\^=7
\mathcode`\+="202B \mathcode`\-="2200 \mathcode`\=="303D
\hsize=77mm \vsize=22mm \scriptspace=0.5pt
\parfillskip=0pt plus 1fil \nulldelimiterspace=1.2pt
\delimiterfactor=901 \delimitershortfall=5pt
\thinmuskip=3mu \medmuskip=4mu \thickmuskip=5mu
\font\tt=cmr10 \font\st=cmr7 \tt\fam0
\font\tm=cmmi10 \font\ts=cmsy10 \font\tx=cmex10
\textfont0=\tt \scriptfont0=\st \scriptscriptfont0=\st
\textfont1=\tm \scriptfont1=\tm \scriptscriptfont1=\tm
\textfont2=\ts \scriptfont2=\ts \scriptscriptfont2=\ts
\textfont3=\tx \scriptfont3=\tx \scriptscriptfont3=\tx
\output{\shipout\vbox to\vsize{\vfill\unvbox255}}
$$x={-b\mathchar"2206\radical"270370{b^2-4ac}\over2a}$$
\end

※ツイッタァー(現𝕏)の投稿に画像として示すために改行を少なめにしているが、それ以外には特別(なるべく短くする、チョット読みにくくする🎄、等)な書き方はしていない。

このソースは「DVI出力を利用して128mm×72mmの用紙サイズで出力すること」を前提としている。元祖TeX(のINIモード)とdvipdfmxを利用してPDFに変換するには以下のコマンドを実行すればよい。

tex -ini ini-formula.tex
dvipdfmx -p "128mm,72mm" ini-formula.dvi

出力の全体

中身を説明してみる

そういうわけで、春🌸なのでこの「iniTeX用のTeXコード」をテキトーに説明していくことにする。

基本的に「plain TeXと同じ組み方の数式」を得るのが目的であるため、「TeXの初期状態」(iniTeX起動時の状態)から始めて「plain TeXでやっている設定のうち今回必要になるものだけを選んで踏襲する」という方針をとる。

1行目

\catcode`\{=1 \catcode`\}=2 \catcode`\$=3 \catcode`\^=7

{ } $ ^の各文字4カテゴリコードをplain(やLaTeX)と同様に設定している。逆に言うとTeXの初期状態ではその設定はされていない」ということであり、実際、初期状態ではほとんどの文字のカテゴリコードは12である。

ちなみに、\のカテゴリコードについては最初から0に設定されている。そうでないとそもそも\catcode自体が使えなくてカテゴリコードの設定が何もできなくなるからである。同様の理由で英字のカテゴリコードも初期状態で11になっている。他にも空白(10)、改行(5)、%(14)等も初期状態で設定されている。

2行目

\mathcode`\+="202B \mathcode`\-="2200 \mathcode`\=="303D

ここでは+ - =の3文字5について数式コード(math code)をplainと同様に設定している。数式コードは「その文字を数式中でフツーに出力した(つまり当該の文字のカテゴリコード11または12の文字トークンを実行した)ときにどのように出力すべきか」を決定する。

  • +のコード値 "202B は「二項演算子(2)として、数式ファミリ0のフォントの文字コード "2B のグリフを出力する」ことを意味する。後で行うフォント設定に従うと、ファミリ0のフォントはcmr10であり、その文字コード "2B には(ASCIIと同じく)“+”の記号が入っている。
  • -のコード値 "2200 は「二項演算子(2)として、数式ファミリ2のフォントの文字コード "00 のグリフを出力する」ことを意味する。ファミリ2のフォントはcmsy10で文字コード "00 には“−”(マイナス)の記号6が入っている。
  • =のコード値 "303D は「関係演算子(3)として、数式ファミリ0のフォントの文字コード "3D のグリフを出力する」ことを意味する。ファミリ0(cmr10)の文字コード "3D は“=”である。

なお、TeXの初期状態の数式コードの値は以下のようになっている(xxは当該文字のASCIIコード)。

  • 英字(A~Z、a~z)については"71xx、つまり「数式英字、ファミリ1の当該の文字」。
  • 数字(0~9)については"70xx、つまり「数式英字、ファミリ0の当該の文字」。
  • それ以外は"00xx、つまり「通常文字、ファミリ0の当該の文字」。

従って、英字や数字(x a 2 4等)については数式コードの設定は不要である。

3~5行目

\hsize=77mm \vsize=22mm \scriptspace=0.5pt
\parfillskip=0pt plus 1fil \nulldelimiterspace=1.2pt
\delimiterfactor=901 \delimitershortfall=5pt

各種のレイアウトパラメタの設定である。このうち最初の2つ(\hsize\vsize)はplainの設定値ではなく独自の値を設定している7

  • \hsize\vsizeは版面のサイズを表す。左側と上側のマージンはTeXの初期値のままの1インチ8なので、ここでは右と下のマージンも1インチと想定した上で、用紙サイズ(128mm×72mm)からマージンを除いたサイズ(77mm×22mm)を設定した。

残りのパラメタはplainの設定値に合わせている。

  • \scriptspaceは添字の直後に挿入される空きの大きさ。
  • \parfillskipは段落の末尾に自動的に追加されるグルーの大きさ。段落最終行を左揃えにするため普通は0pt plus 1filに設定する。
  • \nulldelimiterspaceは「区切り記号(大型括弧)があるべき箇所に実際に何もない場合に代わりに置かれる空き」の大きさ。今回の出力では分数の前後にこの空きが入る9
  • \delimiterfactor\delimitershortfallは区切り記号の大きさを決定するのに使われるパラメタ。根号の大きさをplainに合わせるために設定した。

6行目

\thinmuskip=3mu \medmuskip=4mu \thickmuskip=5mu

これらは記号の周りに自動的に入る空きの大きさを決めるパラメタである。今回の数式の中では、“±”と“−”の周りの空きが\medmuskip、“=”の周りの空きが\thickmuskipである。(\thinmuskipは使われていないので設定は不要だった🙃)

これらのパラメタは“mu”(math unit)という「数式用フォントのサイズに基づく相対単位」で表す(現在の数式スタイルでのファミリ2のフォントの1emが18muに等しい)。

plainでは伸縮付きの値(例えば\medmuskip=4mu plus 2mu minus 4mu)が設定されているが、今回は伸縮は不要なので外した。

7~8行目

\font\tt=cmr10 \font\st=cmr7 \tt\fam0
\font\tm=cmmi10 \font\ts=cmsy10 \font\tx=cmex10

必要なフォント(fontdefトーク)の定義をしている。今回のコード中に現れるプリミティブでない制御綴はここで定義されるもの(\tt\st\tm\ts\tx)しかない10

7行目末尾の\ttはテキストのフォントをcmr10に設定している(初期状態のフォントは\nullfont11である)。実際にはテキスト(数式以外)の文字は一切出力していないが念のため設定した。\fam0は「“現在の数式ファミリ”(つまり“数式英字フォント”として使われる数式ファミリ)を0番に設定する」という意味だが、これも不要であった🙃12

9~12行目

\textfont0=\tt \scriptfont0=\st \scriptscriptfont0=\st
\textfont1=\tm \scriptfont1=\tm \scriptscriptfont1=\tm
\textfont2=\ts \scriptfont2=\ts \scriptscriptfont2=\ts
\textfont3=\tx \scriptfont3=\tx \scriptscriptfont3=\tx

数式ファミリ(math family)にフォント(fontdefトークン)を割り当てている。\textfontで通常サイズ、\scriptfontで添字用の小さいサイズ、\scriptscriptfontで二重添字サイズのフォントを指定する。

ここではplainと同様にファミリ0にcmr、ファミリ1にcmmi、ファミリ2にcmsy、ファミリ3にcmexを使っているが、添字用(小さいサイズ)のものは実際に必要なもの(式の中に上添字の“2”があるので\scriptfont0は必要)以外は別のサイズのもので代替している。

例えば、式の先頭の“x”(数式コード "7178)は通常サイズのファミリ1、すなわち\textfont1で出力されるが、その\textfont1\tm、すなわちcmmi10である。

ちなみに、初期状態では全てのファミリのフォントが未定義(\nullfont)になっている。使用しないファミリは未定義でかまわないのだが、例外的にファミリ2と3については全てのサイズのフォントが定義済である必要がある13

13行目

\output{\shipout\vbox to\vsize{\vfill\unvbox255}}

出力ルーチン\outputトークン列レジスタ)を設定している。

\outputの初期値は空でこの場合は「既定の出力ルーチン」である

\shipout\box255

が使われることになっている。これは「TeXのページ分割の結果作られたページ(255番のボックスレジスタの中身)をそのままDVIに出力する」という処理を意味している。

今回のコードでは版面の垂直方向の中央に数式を出したいので、「\box255の中身の前に\vfillを追加した上で\shipoutする」という出力ルーチンを実装した14

ここまでのコードで全ての設定が完了したことになる。

14行目

$$x={-b\mathchar"2206\radical"270370{b^2-4ac}\over2a}$$

「解の公式」の数式を出力するコードである。比較のために、plain TeXで同じ数式を普通に書いた場合のコードを以下に示す。

$$x={-b\pm\sqrt{b^2-4ac}\over2a}$$

このplainのコード中に現れる制御綴のうち、\overはプリミティブであるが\pm\sqrtは“plainで定義されたもの”である。従って、iniTeXでは同等の機能をプリミティブだけで書く必要がある。

  • \pm\mathchardef\pm="2206で定義されるmathchardefトークである15。従って、\mathchar"2206で同じ動作になる。
  • \sqrt\def\sqrt{\radical"270370 }で定義されるマクロである。従って、単純にマクロの本体で置き換えればよい。

このように書き換えると最初に挙げたiniTeXのコードができあがる。既に“plainと同じ”になる設定が行われているので、このコードで“plainと同じ”の「解の公式」が出力される。

参考として、ここで用いたコードの意味を説明しておく。

  • \mathchar"2206は「数式コードが "2206 の文字を実行する」のと同等である。すなわち「二項演算子(2)として、数式ファミリ2のフォント(ここではcmsy10)の文字コード "06 のグリフを出力する」という動作になる。
  • \radical根号を出力するためのプリミティブであり、根号は“伸長可能なグリフ”と上線の組み合わせで構成される。引数の "270370 のうち、前3桁の "270 は通常のグリフの位置(数式ファミリ2のフォントの文字コード "70)、後3桁の "370 は大型のグリフ16の位置(数式ファミリ3(ここではcmex10)のフォントの文字コード "70)を表している。

15行目

\end

TeXの実行を終了させるプリミティブは\endである17。これにより以下の処理が行われる。

  • まだメモリに残っている内容をDVIに出力する。
    • ページビルダを実行して「解の公式」の数式を含むページの内容を\box255に格納する。
    • 出力ルーチン(先ほどの\outputトークン列)を実行する。その中の\shipoutによりボックスの内容(「解の公式」の数式)がDVIに出力される。
  • TeXの実行を終了する。

めでたしめでたし😊

まとめ

というわけで、「LaTeXなんて“本当のTeX”ではない」と主張する人は、“本当のTeX”であるiniTeXについてもっと学習しましょう!💁


  1. もちろん「TeX処理系およびその言語」というのが本来の意味であるが、でももしそれに従うのであれば、「LaTeXのコード」も「plain TeXのコード」も間違いなく「TeXのコード」といえるはずである。
  2. 「plainフォーマットのTeX」のことを「plain TeX」と呼ぶのは後代の用語である、という話を聞いたことがある。
  3. plainやLaTeX等の“フォーマット”の実装コードを何も読み込まずに、本当にTeXの「初期状態」で起動するモードのこと。初期のTeX配布物では本体のTeXとは別のソフトウェアになっていてそれを“iniTeX”と呼んでいたのだが、後にiniTeXの機能をTeXに組み込んで「TeXのINIモード」として扱うようになった。
  4. 例えば、「解の公式」の数式の中にはb^2があるので^は設定する必要がある。
  5. 実は、“+”の記号は「解の公式」の中で全く使っていないので、+の数式コードの設定は不要であった。
  6. ちなみに、cmr10の文字コード"2D(ASCIIのhyphen-minusの位置)にあるグリフはマイナスではなくハイフンである。
  7. TeXのパラメタの初期値(初期状態の値)は大抵はゼロ(0、0pt、0mu)である。
  8. パラメタ\hoffset\voffsetが初期値の0ptのままで、これに“例の1インチ”が加わる。
  9. TeXの“汎化分数”のプリミティブ(\abovewithdelims)はそれ自身に大型括弧を付ける機能があったことを思い出そう。
  10. なお、TeXの初期状態ではプリミティブ以外の制御綴は全て未定義の状態である。
  11. プリミティブとして用意されている、“全くグリフが定義されていない”ようなfontdefトークン。
  12. \famパラメタは数式の開始時に常に−1にリセットされるため数式の外で設定しても意味がない。そもそも\famの初期値は0である。
  13. 未定義のまま数式を入力するとMath formula deleted: Insufficient symbol fonts.というエラーが出る。これらのファミリの「フォントのパラメタ」が数式全体のパラメタとして参照されるからである。
  14. \vfillを前にだけ入れている理由は、\box255の末尾に既に\vfillが入っているからである。この\vfill\endの処理の中で挿入されるようである。
  15. \mathchardef\X=‹整数n›で定義されるmathchardefトーク\Xを実行すると\mathchar‹整数n›と同等の動作になる。特定のコードの代わりになるという点ではmathchardefトークンはマクロと似ているが、マクロとは異なり展開不能である。なお、mathchardefトークンと\mathcharの関係は、chardefトークンと\charの関係と同じである。
  16. もちろん、TeXの数式中の根号は2段階ではなくもっと多くの段階をもって伸長できる。この「伸長によるグリフの置換・再配置」はTFMの内部の情報を使って処理されている。括弧類についても同様である。
  17. なお、plainの\bye\par\vfill\supereject\endに展開されるマクロである。

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)を使っている。