マクロツイーター

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

イマドキのエ~アイのTeX知識を添削してみた(3)

前回のプログラミングの結果がイイカンジ🙂だったので、味を占めてもうチョット難しい問題を試してみます。

イマドキのAIのTeXプログラミングをもっと添削する

前回の問題の説明で「RubyString#splitを作ろうとすると地獄に陥る😱」と言いましたが、今回はその「String#split(の制限版)を作る」という問題です🤯

自作のLaTeXパッケージの実装でもこのsplitマクロを何度か使ったこと(アレとかコレとか)があります。

TeX特有のインタフェースの指示が元々長くなるので、今回はRubyからの変換ではなく「要求仕様を日本語で記述する」形にしました。

※この問題ではGemini 2.5 Proを使用しました。

入力したやつ

以下の内容をMarkdown形式で表して入力しました。

以下の要件を満たすTeX言語のマクロ\mySplitを実装してください。

  • 入力

      - \mySplit{<文字列1>}{<文字列2>}のように呼び出す。

      - <文字列1><文字列2>は文字トークンからなるトークン列と仮定してよい。

  • 出力

      - <文字列2>を最初の<文字列1>の出現の位置で前後に分割して、
        前のトークン列を\myPre、後のトークン列を\myPostに代入する。

      - <文字列2>中に<文字列1>が出現しない場合は、
        \myPre\myPostをともに\relaxにletする。

  •   - \mySplit{ra}{abracadabra!}
        \def\myPre{ab}\def\myPost{cadabra!}と同じ動作をする。

      - \mySplit{abra}{abracadabra!}
        \def\myPre{}\def\myPost{cadabra!}と同じ動作をする。

      - \mySplit{bad}{abracadabra!}
        \let\myPre\relax \let\myPost\relaxと同じ動作をする。

出力されたやつ

能書きは一切無視して、最終的なプログラムコードだけを載せます。
\makeatletter
% \mySplit で内部的に使用するユニークなマーカーを定義します。
% これらがユーザーの入力と衝突する可能性は極めて低いです。
\long\def\my@split@marker{\my@split@marker}
\long\def\my@split@end{\my@split@end}

% 分割後の後半部分からマーカー(<区切り文字>\my@split@marker)を除去し、
% \myPostに結果を格納する補助マクロです。
% #1: 処理対象のトークン列 (<post>#2\my@split@marker の形)
% #2: 区切り文字
\long\def\my@split@getpost#1#2{%
  \begingroup
  \expandafter\long\expandafter\def\expandafter\my@split@getpost@aux\expandafter##\expandafter1#2\my@split@marker{%
    \global\def\myPost{##1}%
  }%
  \expandafter\my@split@getpost@aux#1%
  \endgroup
}

% 与えられたトークン列が \my@split@marker で始まるかをテストする補助マクロです。
% #1: テスト対象のトークン列
% #2: マーカーで始まる場合に実行するコード
% #3: マーカーで始まらない場合に実行するコード
\long\def\my@split@test#1#2#3{%
  \long\def\my@split@firstofone##1{##1}%
  \ifx\my@split@marker\my@split@firstofone#1%
    #2%
  \else
    #3%
  \fi
}

% --- メインマクロ \mySplit の定義 ---
% #1: 区切り文字列
% #2: 分割対象の文字列
\long\def\mySplit#1#2{%
  % 区切り文字列が空でないかチェックします。
  \def\my@split@delim{#1}%
  \ifx\my@split@delim\empty
    % 空文字列では分割できないため「見つからない」場合と同じ扱いにします。
    \let\myPre\relax
    \let\myPost\relax
  \else
    % 補助マクロの定義をローカル化するためにグループを開始します。
    \begingroup
      % 補助マクロ \my@split@aux を動的に定義します。
      % このマクロは #1 を区切り文字として引数を取ります。
      % \my@split@aux <pre>#1<post_with_markers>\my@split@end
      \expandafter\long\expandafter\def\expandafter\my@split@aux\expandafter##\expandafter1#1##2\my@split@end{%
        % 分割後の後半(##2)がマーカーで始まるかテストします。
        \my@split@test{##2}
          {% YES: マーカーで始まる -> 見つからなかった場合の処理
            \global\let\myPre\relax
            \global\let\myPost\relax
          }
          {% NO: マーカーで始まらない -> 見つかった場合の処理
            % 前半部分を \myPre にグローバル定義します。
            \gdef\myPre{##1}%
            % 後半部分から余分なマーカーを除去して \myPost にグローバル定義します。
            \my@split@getpost{##2}{#1}%
          }%
      }%
      % \my@split@aux を呼び出して分割を実行します。
      % 対象文字列(#2)の後ろに、(1)マッチング用の区切り文字、
      % (2)見つからない場合判定用のマーカー、(3)終端記号を追加します。
      \expandafter\my@split@aux#2#1\my@split@marker\my@split@end
    \endgroup
  \fi
}
\makeatother

添削してみるやつ

今回は出力されたままのコードを使おうとしても意味不明なエラーが出て全く動作しません😢

Runaway argument?
\my@split@marker \endgroup \fi \endgroup \fi \par \noindent {\ttfamily \ETC.
! File ended while scanning use of \my@split@getpost@aux.
<inserted text>
                \par
<*> 02test

?

マジメにデバッグする必要があるようです💪

「先頭がマーカーであるか」の判定

現状のコードが動作しない原因はコレです。

% 与えられたトークン列が \my@split@marker で始まるかをテストする補助マクロです。
% #1: テスト対象のトークン列
% #2: マーカーで始まる場合に実行するコード
% #3: マーカーで始まらない場合に実行するコード
\long\def\my@split@test#1#2#3{%
  \long\def\my@split@firstofone##1{##1}%
  \ifx\my@split@marker\my@split@firstofone#1%

コメントから判断すると、ここで\ifxで比較すべきなのは「#1先頭トークン」と\my@split@markerのはずで、現状の\my@split@firstofoneは明らかに目的に適っていません。「トークン列の先頭を取得する」ようなマクロ(もはや“firstofone”ではないので名前は“first”にしましょう)に置き換える必要があります。

  % 対象トークン列は \my@split@end は含まないはずなので,
  % 終端として \my@split@end を利用する.
  \long\def\my@split@first##1##2\my@split@end{##1}%
  \ifx\my@split@marker\my@split@first#1\my@split@end

これでロジックは合いましたが、さらに展開制御が必要です。\ifxの実行前に\my@split@firstを一回展開する必要があるため、\expandafter鎖を挿入します。

  \long\def\my@split@first##1##2\my@split@end{##1}%
  \expandafter\ifx\expandafter\my@split@marker\my@split@first#1\my@split@end
無駄な \expandafter 祭り

出力コードにはあちこちで\expandafter鎖が使われています。

%(13行目)
  \expandafter\long\expandafter\def\expandafter\my@split@getpost@aux\expandafter##\expandafter1#2\my@split@marker{%
%(49行目)
      \expandafter\long\expandafter\def\expandafter\my@split@aux\expandafter##\expandafter1#1##2\my@split@end{%
%(66行目:これは単発だけど)
      \expandafter\my@split@aux#2#1\my@split@marker\my@split@end

しかもこれらの\expandafter鎖での一回展開の対象はどれも引数(#1等)になっています。ところが\mySplitの実行において引数に入るのは「文字とマーカーからなるトークン列」に限られます。ここで一回展開を適用する理由は何もないので1、これらの\expandafterは削除しました。

%(13行目)
  \long\def\my@split@getpost@aux##1#2\my@split@marker{%
%(49行目)
      \long\def\my@split@aux##1#1##2\my@split@end{%
%(66行目)
      \my@split@aux#2#1\my@split@marker\my@split@end

前回の問題でも「余計な\expandafter」がありましたが、どうも「処理対象のトークン列がマクロに格納されている場合」との混同があるように思えます。もし「引数を展開する仕様の方がよい」と思うのなら、これも前回の話と同様で、完全展開すべきでしょう。(pxchfonのコードでは最初に引数を完全展開しています。)

代入がグローバルになっている

出力コードでは、\myPre\myPostへの代入がグローバルになっています2

            \gdef\myPre{##1}%

要求仕様の「出力」の箇所では「代入がローカルかグローバルか」を特に指示していないわけですが、少なくとも「例」の箇所では「ローカルな代入と等価であること」としていて、この内容とは整合していません。

何故代入をグローバルにしているかというと、理由はコレです。

    % 補助マクロの定義をローカル化するためにグループを開始します。
    \begingroup

ところが、\mySplitの実装において途中で動的に定義する補助マクロ(\my@split@auxmy@split@getpost@aux)をローカルにしておく必要3はそもそもありません。

従って、コード中の\begingroup\endgroupは全て不要で、代入は単純にローカルで行うことにしました。

            \def\myPre{##1}% ローカル代入でよい
その他諸々
  • 元のコードに問題があるという話ではないのですが、自分はこの\mySplitが書かれている場所はパッケージファイル(*.sty)であると想定している4ので、\makeatletterは削除しました。
添削結果

修正後のコード:

実行例
\documentclass{article}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\usepackage{ai-coding-2}
\newcommand*\myCS[1]{\symbol{`\\}#1}
\newcommand\doTest[2]{%
  \mySplit{#1}{#2}%
  \par\noindent{\ttfamily
    \myCS{mySplit}\{#1\}\{#2\} $\to$\\
    \myCS{myPre} $=$ \meaning\myPre\\
    \myCS{myPost} $=$ \meaning\myPost
  }\par\medskip}
\begin{document}
\doTest{ra}{abracadabra!}
\doTest{abra}{abracadabra!}
\doTest{bad}{abracadabra!}
\end{document}

出力結果

カンペキ😍

まとめ

これまでは、実用でTeX言語プログラミングをするという場合、どちらかというと「TeX言語を書く力」の方が重視されていたと思います。対して、TeX言語プログラミングにエ~アイを活用する場合には「他者が書いたTeXプログラムをデバッグする」作業が必要となり、そこでは「TeX言語を読む力」が今までにも増して重要になってきます。エ~アイがどんなコードを書いてきても対処できるようにTeX言語のキホンを漏らさず把握しておきましょう!💁

……えっ、もしかして、高性能エ~アイにじゃんじゃん課金してバイブコ~ディングとかすればTeX言語を書く力も読む力も要らなくなる?😲 文字通りの富豪的💰プログラミングだ……😐


  1. 前節で出てきた\my@split@firstofoneは結局不適切だったわけですが、それ以前の問題としてそもそも
    \ifx\my@split@marker\my@split@firstofone#1
    というコードは、\my@split@firstofoneを予め一回展開しないと意味を成さないでしょう。不要なときに\expandafter祭りを散々やっておいて、肝心な時に忘れてしまっています(ざんねん🙃)
  2. 区切り文字列が空の場合の例外的な処理の中では\myPre\myPostにローカルな代入を行っていて、動作が一貫していません。
  3. 処理途中の代入をローカルにする必要があるのは「当該のマクロがネストして使われる」場合ですが、今の\mySplitについては「何らかの意味でネストする」ような使い方がそもそも存在しないわけです。
  4. TeX on LaTeXのコードは原則的に「そもそも最初から\makeatletterの状態であるファイル」(*.sty*.cls)に記述されるべきであると自分は考えています。

イマドキのエ~アイのTeX知識を添削してみた(2)

前回の続き。前回の記事では“純粋な知識問題”を扱いましたが、今回は「実用的なTeX言語プログラミング」を扱います。

例の2022年の記事にある問題でプログラミングを扱っているのは6番目のFizzBuzzですが、FizzBuzzはあまり実用的とはいえないので、ここでは別の問題を扱います。

このシリーズではプログラミングの問題についてはGemini 2.5 Proを使用します。Googleにログインしている状態であれば、GeminiのフツーのWebサイトで無料枠の2.5 Proが使えます。

イマドキのAIのTeXプログラミングを添削する

「“HH:MM”の形式の時刻の文字列」を「0時からの経過分数」に変換する関数を実装します。TeXで“関数”を表現する方法はいくつかありますが、ここでは「値を返す先の変数を引数に渡す」という方式1にします。

「フツーのプログラミング言語から変換させると性能が良くなる2」という話を仕入れたので、ここではRubyの関数から変換させてみました。

RubyのコードではString#splitを使っていますが、TeXsplitを作ろうとすると地獄に陥ります😱 文字列の分割をマクロ引数のパターンマッチに任せるのが“TeX言語らしいやり方”ですね🤯

※この問題ではGemini 2.5 Proを使用しました。

入力したやつ

以下の内容をMarkdown形式で表して入力しました。
def toMinutes(time_str)
time_str.split(':').map{|x| Integer(x)}.then{|(a,b)| a*60+b}
end

このRuby関数と同じことをするTeX言語マクロ\toMinutesを作ってください。インタフェースについては

value = toMinutes("12:30")

の代わりに

\toMinutes\value{12:30}

のようにします。
※パッケージは使わないでください。

出力されたやつ

能書きは一切無視して、最終的なプログラムコードだけを載せます。
% --- マクロ定義ここから ---

% 計算に使用するカウンタ(整数型変数)を宣言
\newcount\hours
\newcount\minutes
\newcount\totalminutes

% \toMinutes の内部で呼び出されるヘルパーマクロ
% #1:#2\relax の形式で引数を取ることで、":"を区切り文字として
% 時間と分を分割します。
% #1: 時間 (HH)
% #2: 分 (MM)
% #3: 結果を格納するマクロ(例: \value)
\def\parseTime#1:#2\relax#3{
    \hours=#1
    \minutes=#2
    \totalminutes=\hours
    \multiply\totalminutes by 60
    \advance\totalminutes by \minutes
    % 計算結果を #3 で指定されたマクロに定義する
    % \edef は右辺を完全に展開してから定義する
    \edef#3{\the\totalminutes}
}

% ユーザーが呼び出すマクロ本体
% #1: 結果を格納するマクロ(例: \value)
% #2: 時刻文字列(例: {12:30})
\def\toMinutes#1#2{
    % \expandafter を用いて、#2の波括弧{}を取り除き、
    % 中身の "HH:MM" を \parseTime マクロに渡します。
    \expandafter\parseTime#2\relax#1
}

% --- マクロ定義ここまで ---

添削してみるやつ

…………ほぼカンペキ(ツマラナイ😐)

インタフェースについて、自分は\valueの部分は整数レジスタを意図していたのですが、AIは“整数値マクロ”と解釈しました。もちろん(明確に指示してないので)それでも正解で、むしろ「LaTeXの命令の実装」としてはその方が適切3かもしれませんね。

マクロの引継の処理
\def\toMinutes#1#2{
    % \expandafter を用いて、#2の波括弧{}を取り除き、
    % 中身の "HH:MM" を \parseTime マクロに渡します。
    \expandafter\parseTime#2\relax#1
}

「ユーザからもらった引数を区切り付き引数のマクロに引き継いで、パターンマッチに持ち込む」という典型的なパターンは卒なくこなせています。ただし\expandafter を用いて、#2の波括弧{}を取り除き、という説明は不適切で、実際にはここでは \expandafter は不要です。なぜなら、マクロの実引数は「マクロ呼出時の波括弧の中身」であり、従って引数#2にはそもそも波括弧は付いていないからです。

\def\toMinutes#1#2{%
    % #2の中身の "HH:MM" を \parseTime マクロに渡します。
    \parseTime#2\relax#1% \expandafter は不要!
}

「引数に含まれるマクロを展開させたい」という意図ならばこの\expandafterも意味をもちますが、時刻文字列の正当な引数は明らかに「単なる文字列」であるため、展開を考えるなら4中途半端な一回展開ではなく完全展開を施すべきです。

\def\toMinutes#1#2{%
    \edef\timestring{#2}% 完全展開する
    \expandafter\parseTime\timestring\relax#1%
}
その他諸々
  • ソースの行末による「余計な空白」が処理できていません。数箇所の行末に%を補う必要がありました。
添削結果

修正後のコード:

実行例
\documentclass{article}
\usepackage{ai-coding-1}
\begin{document}
16:40
\toMinutes\myResult{16:40}
$\to$ {\myResult} minutes
\end{document}

出力結果

カンペキ😍

まとめ

やっぱり、イマドキのエ~アイはマトモなのでツマラナイ😑(ざんねん🙃)

つづく


  1. C言語の関数で「値を返す先の変数のポインタを引数に渡す」のと同じ発想です。
  2. 性能云々以外に「所望のプログラムの仕様を自然言語で正確に記述する手間を省ける」という点も大きいかもしれません。
  3. LaTeXでは「TeXの整数型のレジスタ・パラメタ」を直接には扱わないため。
  4. もちろん、今回の仕様では完全展開を明示的に要求していないので、そうする必要はありません。

イマドキのエ~アイのTeX知識を添削してみた(1)

去る2022年の某アドベントカレンダーに「ChatGPTのTeX知識を添削してみた」という記事がありました。

イマドキの生成AIを知る人から見ると2年半前の生成AIはカス同然です。だからこそ「AIにTeXについて質問する」ことがネタとして成立していたわけです。イマドキの生成AIに質問してもここまで愉快な話にはならないことは容易に予想できます。実際、この記事以降に「AIにTeXについて質問してみた」こと自体をテーマにする記事は誰も書いていないようです。

しかし、マジメな話として「イマドキの生成AIがTeX言語1をどこまで知っているか」について興味がある人もいるでしょう。そういう人が自分で試そうとしても、当人に十分なTeX言語の知識がないと生成AIの回答を評価できません。そう考えると「イマドキの生成AIのTeX知識を添削してみた」という記事にも一定の価値はあるでしょう。

イマドキのAIのカテゴリコードの知識を添削する

例の記事では8つの質問を試していますが、この記事では、その中で“純粋な知識問題”といえる2番目(と3番目2)の「カテゴリコードの意味」を問う質問を試してみます。

※GeminiのフツーのWebサイトで、2.5 Flashを使用しました。

入力したやつ

TeXのカテゴリーコードって何? 0〜15までの各カテゴリーコードの意味も教えて

出力されたやつ

TeXにおけるカテゴリーコードとは、TeXが入力ファイルを読み込む際に、各文字に与える「役割」を規定する重要な値です。TeXは文字を読み込む際、その文字コードだけでなく、対応するカテゴリーコードも見て、その文字をどのように処理するかを決定します。

TeXには0から15までの16種類のカテゴリーコードがあります。これらのコードは、\catcodeコマンドを使って変更することができます。デフォルトの割り当てはTeXのフォーマット(plain TeXLaTeXなど)によって異なりますが、一般的なTeX処理系での各カテゴリーコードの意味は以下の通りです。

0から15までの各カテゴリーコードの意味

  • カテゴリーコード0 (Escape character / エスケープ文字):

    • 通常は \ (バックスラッシュ) に割り当てられています。

    • TeXコマンドの始まりを示します。この文字の後ろに続く文字列は、特別な意味を持つ制御綴(control sequence)として扱われます。

    • 例: \documentclass, \section

  • カテゴリーコード1 (Begin group / グループ開始):

    • 通常は { (左中括弧) に割り当てられています。

    • TeXにおけるグループの始まりを示します。グループ内で設定された変更(フォントサイズ、インデントなど)は、グループの外には影響しません。

  • カテゴリーコード2 (End group / グループ終了):

    • 通常は } (右中括弧) に割り当てられています。

    • グループの終わりを示します。

  • カテゴリーコード3 (Math shift / 数式モード切り替え):

    • 通常は $ (ドル記号) に割り当てられています。

    • 数式モードの開始と終了を切り替えます。インライン数式やディスプレイ数式に使われます。

    • 例: $x^2+y^2$

  • カテゴリーコード4 (Alignment tab / 配置タブ):

    • 通常は & (アンパサンド) に割り当てられています。

    • tabular環境やalign環境など、表組みや数式の配置においてカラムの区切りとして使われます。

  • カテゴリーコード5 (End of line / 行末文字):

    • 通常は改行文字(LF, CR)に割り当てられています。

    • TeXがこの文字を読み込むと、通常は\par(段落の終わり)を挿入するか、または空白として扱われます。

  • カテゴリーコード6 (Parameter / マクロ引数):

    • 通常は # (シャープ) に割り当てられています。

    • マクロ定義において、引数を示すために使われます(例: #1, #2)。

  • カテゴリーコード7 (Superscript / 上付き文字):

    • 通常は ^ (ハット) に割り当てられています。

    • 上付き文字(冪指数など)を示します。

    • 例: x^2

  • カテゴリーコード8 (Subscript / 下付き文字):

    • 通常は _ (アンダースコア) に割り当てられています。

    • 下付き文字(添字など)を示します。

    • 例: x_i

  • カテゴリーコード9 (Ignored character / 無視される文字):

    • 通常はNull文字(文字コード0)に割り当てられています。

    • TeXによって完全に無視され、何も生成しません。

  • カテゴリーコード10 (Space / スペース):

    • 通常はスペース文字に割り当てられています。

    • 空白として扱われます。連続する複数のスペースは通常1つのスペースとして扱われます。

  • カテゴリーコード11 (Letter / 字母):

    • 通常はアルファベット (a-z, A-Z) に割り当てられています。

    • TeXコマンド名の一部を構成したり、通常のテキスト文字として扱われたりします。

  • カテゴリーコード12 (Other character / その他の文字):

    • 上記以外のほとんどの記号や数字に割り当てられています。

    • 通常はTeXによってそのまま出力される文字として扱われます。

    • 例: 1, 2, ., ,, !, ?

  • カテゴリーコード13 (Active character / アクティブ文字):

    • このカテゴリーに割り当てられた文字は、マクロとして定義することができます。つまり、その文字自体がコマンドのように振る舞います。

    • LaTeXでは、例えば~チルダ)が改行しないスペース(non-breaking space)としてアクティブ文字に設定されています。

  • カテゴリーコード14 (Comment character / コメント文字):

    • 通常は % (パーセント) に割り当てられています。

    • この文字から行末までは、TeXによってコメントとして扱われ、無視されます。

  • カテゴリーコード15 (Invalid character / 無効な文字):

    • 通常は DEL (Delete文字、文字コード127) に割り当てられています。

    • このカテゴリーに割り当てられた文字は、TeXの処理にとって無効であり、通常はエラーを引き起こします。

カテゴリーコードはTeXの柔軟性と強力さを支える重要なメカニズムの一つであり、これらを理解することで、TeXの動作をより深く理解し、必要に応じてカスタマイズすることができます。

添削してみるやつ

…………フツー😐

昔のChatGPTの回答は「合っているのを探す方が難しい」という有様でしたが、この回答は「間違っている点を探すのが難しい」感じです。人によって「この言い方はあまりよくない」と感じる点もあると思いますが、「客観的に誤りである」記述を探すのは至難の業だと思います。自分のTeX力に自信のある人は一度探してみてください。

自分が見つけたのは以下の点です。

まとめ

イマドキのエ~アイはマトモなのでツマラナイ😑(えっ😲)

つづく


  1. 「イマドキの生成AIがLaTeXについてよく知っている」ことは実際に活用している人ならば把握しているでしょう。
  2. 当時のChatGPTの応答の長さ制限のため、質問を2回に分割する必要がありました。

TeXの長さ単位に関するアレな話

先日の某キャンペーン☘️向けの某記事​「TeXのキーワード」​について解説した。その中で「TeX絶対単位の一覧」を紹介した。

単位 意味 TeXでの定義
pt ポイント(point)
in インチ(inch) 1in=(7227/100)pt
pc イカ(pica) 1pc=12pt
cm センチメートル 1cm=(7227/254)pt
mm ミリメートル 1mm=(7227/2540)pt
bp ビッグポイント
(big point)
1bp=(7227/7200)pt
dd ディドーポイント
(Didot point)
1dd=(1238/1157)pt
cc シセロ(Cicero) 1cc=(14856/1157)pt
sp スケールドポイント
(scaled point)
1sp=(1/65536)pt

この中の「TeXでの定義」をよく見ると、「1 mm=(7227/2540) pt」のように各単位はptの分数倍として定義されている。TeXの内部において実数は有理数ではなく小数(整数部16ビット・小数部16ビットの二進固定小数点数)で表されていることを考慮すると、これは一見奇妙に思える。

TeX処理系の単位の実装の話

実際のTeXの実装コードを見てみよう。

@ The necessary conversion factors can all be specified exactly as
fractions whose numerator and denominator sum to 32768 or less.
(……中略……)

@<Scan for \(a)all other units and adjust |cur_val| and |f|...@>=
if scan_keyword("in") then set_conversion(7227)(100)
@.in@>
else if scan_keyword("pc") then set_conversion(12)(1)
@.pc@>
else if scan_keyword("cm") then set_conversion(7227)(254)
@.cm@>
else if scan_keyword("mm") then set_conversion(7227)(2540)
@.mm@>
else if scan_keyword("bp") then set_conversion(7227)(7200)
@.bp@>
else if scan_keyword("dd") then set_conversion(1238)(1157)
@.dd@>
else if scan_keyword("cc") then set_conversion(14856)(1157)
@.cc@>

説明文に「conversion factors can all be specified exactly as fractions」とある通りで、実際にpt以外の絶対単位1はptの分数倍として実装されていることがわかる。

もちろん「実数の内部表現が固定小数点数である」ことも確かなので、最終的な計算結果には丸め2が入る。例えば10.5cmが表す寸法値(sp単位)は次のように求められる。

  • ① 十進小数10.5固定小数点数で表す3
  • ② 1 cm=(7227/254) pt なので、(①の値)×7227÷254 を計算してその値を固定小数点数で表す。(これがpt単位の値になる。)
  • ③ (②の値)に65536を乗じてsp単位に直す。

これにより、10.5cmが表す値は 19 579 138 sp(pt単位で298.753 936 767 578pt)と求められる。(この値を\message等で表示させると298.75394ptとなる4。)

TeXコードの単位の実装の話

TeXの文法では寸法値の単位としてpt・mm・emなどの“本来の単位”の他に​「寸法型の内部値」​も使用できる。例えばパラメタ\baselineskipは寸法型の内部値の一種なので、2\baselineskipは有効な寸法値の表記となる。

この性質を利用したTeX言語のコーディング上の技法として「単位の代わりに寸法レジスタを使う」というものがある。

\newdimen\mm \mm=1mm
% 以後 \mm は定数として使う

このように寸法レジスタ\mmを用意しておくと、0.6\mmは「\mm(=1 mm)の寸法を単位にしてその0.6倍」だから結局 0.6 mm になるはずである。実際、この場合0.6\mm0.6mmは同じ寸法値になる5

\dimen0=0.6mm \message{\the\dimen0}
%==>"1.70717pt"
\dimen0=0.6\mm \message{\the\dimen0}
%==>"1.70717pt"

この技法は、高速化のために行われることもあるし、また「マクロの仕様において寸法の単位を可変にしたい(例えばLaTeXのpicture環境における\unitlengthの使用など)」という理由で行われることもある。

mm と \mm が同じにならない話

しかし先述の「TeXの単位は分数として定義されている」ということを勘案するとこの技法には注意すべき点があることが推測できる。寸法レジスタ(などの内部値)がもつ寸法値は普通の固定小数点数で表されていて分数ではない。だから“本物の単位”と“単位レジスタ”では内部の処理が異なるはずである。

実際、先の例で数値を0.6から1.2に変えてみると、mm単位と\mm単位の表す寸法値は異なるものになる6

\dimen0=1.2mm \message{\the\dimen0}
%==>"3.41432pt"
\dimen0=1.2\mm \message{\the\dimen0}
%==>"3.41429pt"

両者の計算を追跡すると以下のようになる。

  • 1.2mmは【((7227/2540) ×【1.2の丸め】)の丸め】という計算になる。
  • 1.2\mmは【(【(7227/2540) の丸め】×【1.2の丸め】)の丸め】という計算になる。

つまり、“レジスタ単位”を利用した後者の場合は、単位の換算係数の箇所に丸めが新たに発生し、その分だけ結果の誤差が増えるわけである7

もちろん、実用のTeX言語プログラミングでは「寸法値は計算誤差を含む」ことを前提にすることが常識であるため、特に「単位は分数である」という性質が問題になることはないだろう。しかし何か特別な用途があって敢えて「寸法値の等値比較を行う」処理を実装する場合、この性質を知っておく必要がある。

pt と \p@ が同じになる話

レジスタ単位”という技法の最も有名な使用例はplain TeX\p@であろう。(LaTeXカーネルにも継承されている。)

\newdimen\p@ \p@=1pt % this saves macro space and time

ではこれまでの話と同様にpt単位と\p@単位は異なる値になりえるのかというと、この場合はそういうことはない。なぜかというと、“レジスタ単位”を使う場合に新たに丸めが発生する箇所は「単位換算の係数」だからである。先のplainのコードでは単位がptであるため係数は1であり、これは整数であるから固定小数点数に丸めた結果も正確な1になる。従って、pt単位と\p@単位の表す寸法値は常に一致するのである。

まとめ

😲「アッ、また某ツイーターのサッパリ実用的でない記事が増えてるぞ!」
(ざんねん🙃)


  1. ただしspは扱いが別になっている。この記事ではspについては扱わないことにする。
  2. ①での丸めは四捨五入(例えば0.99999999ptは1ptになる)、②での丸めは切り捨てである。③は「整数16ビット+小数16ビット」の固定小数点数の小数点を右端にずらして32ビット整数と見なす処理なので誤差は発生しない。
  3. TeXの小数表現は二進であるため、十進法の有限小数を正確に表せずに丸めが発生する場合があることに注意。10.5(二進法で 1010.1)は正確に表せるが、1.2(二進法では無限小数)は表せない。
  4. TeXが寸法値を表示するときは「その表記を読んだときに元の寸法値となるような、最も小数部が短くなるpt単位の表記」が使われる。例えば298.75394ptという表記をTeXが読むとそれは 19 579 138 sp と解釈される。
  5. 先述の「寸法値の表示の原理」からわかるように、表示が同じ寸法値は内部でも同じ寸法値であり、逆もまた然りである。
  6. 数値が1.02あたりを超えると、mm単位と\mm単位は必ず異なる寸法値になる。
  7. 1.2mmが表す値は 223761sp (=3.414 321 899 414 06pt)、1.2\mmが表す値は 223761sp (=3.414 291 381 835 94pt)、正確な値は (21681/6350)pt (=3.414 330 7…pt)である。

Typstについて“深く学習”できる日本語の記事を一覧する件

Typstに関する日本語の記事もかなり増えてきたが、その多くは「具体的な使い方の解説」「便利な機能の紹介」を扱ったものである。そこで「Typstのもっと深いところを学習できる」ような記事、具体例としては

  • Typstの基礎(キホンじゃなくてFundamental)
  • 特に言語仕様やレイアウト仕様の詳細の話

といったテーマを扱った記事を(独断と偏見で)集めてみることにした。この一覧は随時更新される予定である。

某キ~タ

某ゼン

某ツイーター

ツイッタァー(現𝕏)ではなくて某ブログ🙃のこと。

その他諸々

これで完璧! TeX言語の演算子と区切り記号を網羅するまとめ

「TeX言語🤮の全キーワードまとめ」というネタを思いついてしまったという深刻な問題を無事解決して安心して某キ~タを眺めていたら、また新たな問題が発生してしまいました😟

しょうがないので、また記事を書くことにしました。

TeX言語において演算子・区切り記号とは何か

TeX言語における「演算子・区切り記号」と聞くと、{ } $ ^のようないわゆる特殊文字を思い浮かべる人もいるかもしれません。しかしよく知られているように、TeX言語の“特殊文字”は文字のカテゴリコードによって決定され、これはユーザが自由に設定できます。つまり“特殊文字”の機能は「特定の記号と不可分に結びついている」わけではありません。元ネタの記事の趣旨に合わせて、本記事では「プリミティブの構文の中で固定して現れる記号」に的を絞って解説します。

演算子・区切り記号に関する構文規則

キーワードの時と同様に、TeX言語の演算子・区切り記号は「プリミティブの構文の中でそれが出現する(可能性のある)位置」で出現した場合にのみ当該の働きをします。(それ以外の位置では恐らく版面にそのまま出力される。)

その他に、演算子・区切り記号には以下のような規則があります。

  • 暗黙文字トークンは使用不可。
  • 文字トークンのカテゴリコードは12である必要がある1
% カテゴリコード12の"<"は演算子になる
\message{\ifnum 1<2 YES\else NO\fi} %==>"YES"
% カテゴリコード11の"<"は演算子にならない
\catcode`\<=11
\message{\ifnum 1<2 YES\else NO\fi} %==> エラー " Missing = inserted for \ifnum"

一覧表

元祖TeXの範囲での演算子・区切り記号の一覧は以下の通りです。

単語説明使用例
"整数表記:16進法表記 "2A
'整数表記:8進法表記 '052
+符号:正数 +10000
,小数表記:小数点 29,7cm
-符号:負数 -1000pt
.小数表記:小数点 11.5in
<演算子:小なり \ifnum \month<8 …
=演算子:等しい
②代入
\ifnum \month=8 …
\catcode64=11
>演算子:大なり \ifnum \month>8 …
`整数表記:文字コード `*

詳細

以下では、演算子・区切り符号のそれぞれについてその用途と構文を解説していきます。

構文説明の中では以下の記法を用います。

  • [A]は「Aが省略可能である」ことを表します。[ ]自体は書きません。
  • (A | B)は「AまたはB」を表します。
  • A…は「Aを1個以上並べる」ことを表します。

また、使用例のコードの動作は「plain TeX@のカテゴリコードが11の状態」を仮定します。

"(quotation mark)

説明

整数表記において整数が16進法表記であることを示します。

% 以下のものは整数表記となる
"«16進数字»…

使用例

% これは64206になる
\count@="FACE
% -10が代入され, "a"が出力される
\count@=-"0Aa
% これは "DD dd と解釈されるので,
% 221dd(=236.47191pt)が代入され, "D"が1個出力される.
\dimen@ = "DDdDD
% 数字がないとエラー
\count@="\relax %==>エラー "Missing number, treated as zero"

'(apostrophe)

説明

整数表記において整数が8進法表記であることを示します。

% 以下のものは整数表記となる
'«8進数字»…

使用例

% これは52になる
\count@='064
% -10が代入され, "8"が出力される
\count@=-'128
% 数字がないとエラー
\dimen@='pt %==>エラー "Missing number, treated as zero"

+(plus sign

説明

整数や寸法の前に置いて、正符号を表します。

% 以下のものは符号となる
( + | - )

使用例

\count@ = +1
% これは1になる
\count@ = + ++ + 1
% "1cm plus 1fil"と書いたのと等価
\skip@ = +1cm plus +1fil

,(comma)

説明

小数表記の中で、小数点を表します。一部の地域では小数点として「.」ではなく「,」を用いていて、TeX言語ではこれに対応するため小数点として.,の両方をサポートしています2

% 以下のものは小数表記となる
% ※空白は実際には入れない
[ «数字»… ] , [ «数字»… ]

使用例

% "1.5em"と書いたのと等価
\parindent = 1,5em

-(hyphen-minus)

説明

整数や寸法の前に置いて、負符号を表します。

% 以下のものは符号となる
( + | - )

使用例

% これは-1になる
\count@ = -1
% 符号が3回反転するので-1になる
\count@ = - +- - 1
% 伸縮値を負にできる
\skip@ = -12pt minus -6pt

※負の伸縮値をもつグルーは奇妙な動作を引き起こすので普通は使われません。ただし負の伸縮値のグルー値は「他のグルー値と加算することで伸縮値を変化させる」などの使い方があります3

.(full stop)

説明

小数表記の中で、小数点を表します。

% 以下のものは小数表記となる
% ※空白は実際には入れない
[ «数字»… ] . [ «数字»… ]

,の項も参照してください。

\parindent = 2.0em
% これは5ptになる
\dimen@ = 5.pt
% これは0.5ptになる
\dimen@ = .5pt
% これは0ptになる
\dimen@ = .pt

<(less-than sign

説明

\ifnum\ifdimの後に続く値比較の記述において、「左辺が右辺より小さい」という条件を表します。

% 以下のものは条件記述となる
\ifnum «整数» < «整数»
\ifdim «寸法» < «寸法»

使用例

% \topskip は丁度10ptなので偽
\message{\ifdim \topskip < 10pt YES\else NO\fi} %==>"NO"

=(equal sign

説明(比較演算子

\ifnum\ifdimの後に続く値比較の記述において、「左辺が右辺と等しい」という条件を表します。

% 以下のものは条件記述となる
\ifnum «整数» = «整数»
\ifdim «寸法» = «寸法»

説明(代入)

ある種の代入文において、代入先のものと代入される値の間に置かれます。

«変数» [=] «値»
«toks変数» [=] { «トークン»… }
\let \制御綴 [=] «トークン»
\setbox «整数» [=] «ボックス記述»
\font \制御綴 [=] «TFM名» [ at «寸法» ] [ scaled «整数» ]
( \chardef | \mathchardef ) \制御綴 [=] «整数»
( \countdef | \dimendef | \skipdef | \muskipdef | \toksdef ) \制御綴 [=] «整数»
( \textfont | \scriptfont | \scriptscriptfont ) «整数» [=] «フォント»
\parshape [=] «整数» «寸法»…

«変数»とは整数・寸法・グルー値・数式グルー値・トークン列を値にもつパラメタ(\parindent等)やレジスタ\skip2等)のことを指し、引数をとるパラメタ(\catcode64\wd255等)も含みます。

何れの場合も、代入文の=は省略可能です。

使用例

% どちらも7なので真
\message{\ifnum ++007 = --'007 YES\else NO\fi} %==>"YES"
% どちらも0ptなので真
\message{\ifdim .in = ,cm YES\else NO\fi} %==>"YES"
% \parfillskip は0pt plus 1filであるが, \ifdim で比較するために
% 寸法値にキャストされて自然長の0ptが残る.
\message{\ifdim \parfillskip = 0pt YES\else NO\fi} %==>"YES"

% いろいろな代入文
\month=8
\lccode`\* = `\~
\toks4 = \toks2
\everypar={\P}
\let \ifTeX = \iffalse
\setbox2 = \hbox{\TeX}
\setbox4 = \copy2
\font\fA=cmr10
\dimendef\dB=255
\textfont4 = \fA
\parshape = 2 0pt 10em 4em 10em

>(greater-than sign

説明

\ifnum\ifdimの後に続く値比較の記述において、「左辺が右辺より大きい」という条件を表します。

% 以下のものは条件記述となる
\ifnum «整数» > «整数»
\ifdim «寸法» > «寸法»

使用例

% 1977年にはTeXは存在しない🙃
\message{\ifnum \year > 1977 YES\else NO\fi} %==>"YES"

`(grave accent)

説明

整数表記において整数が文字コード値表記であることを示します。

% 以下のものは整数表記となる
(`«文字トークン» | `\制御綴)

制御綴は「名前が1文字である」ものに限ります。文字トークンの場合はその文字、制御綴の場合はその名前の文字の、文字コードの整数値を表します。

使用例

% "Z"の文字コードの90になる
\count@=`Z
% "%"の文字コードの37になる.
% もし"\"がないと"%"は通常通りコメントを開始してしまう.
\count@=`\%
% "~"の文字コードの126. "~"の展開は抑止される.
\count@=`~
% "c"の文字コードの99. "\c"の展開は抑止される.
\count@=`\c

まとめ

というわけで、TeX言語GW特別キャンペーン☘️も今日でおしまいです。皆さんのTeX言語活動の進捗が満足のいくものであったことを願います😃


  1. 演算子・区切り記号の位置に展開可能なアクティブ文字トークンがある場合はそれは展開されます。
  2. ただしLaTeXのパッケージでは小数点として,が使われることを想定していない場合が多いと思われます。
  3. 負の伸縮値のグルー値を利用する有名な例としてLaTeXカーネルの内部マクロの\@startsectionがあります。このマクロの第4引数にはグルー値を指定しますが、この値は実際には2つの設定を担っていて「値(自然長)が正負のどちらか」のフラグと「値の絶対値」が別の意味をもっています。前者のフラグを「負」にしようとすると、後者のグルー値の符号を反転した値を指定する必要があり、この場合は負の伸縮値をもつグルー値を指定することになります。しかし実際に使われるグルー値は絶対値なので負の伸縮値をもつグルーは使われません。なお、\@startsectionマクロの第5引数も同じ性質をもちます。

MiTeXの使い方を徹底解説してみる件

先日の某言語1キャンペーン🌸用のネタ記事においてMiTeXを紹介した。折角なのでMiTeX自体についてもう少し解説したいと思う。

MiTeXの概要

端的にいうと、MiTeXは「LaTeXのコードをTypstのコードに変換するソフトウェア」である。メインの部分はRustで実装されていて「コマンドラインツール」「Typstパッケージ」「Webアプリ」の3つの形態で利用できる2

同様の目的のソフトウェアは既に存在する(例えば tex2typst 等)がMiTeXの大きな特徴は「Typstでで組版するのが目的であり、マークアップの形式変換自体は目的ではない」ということである。つまり「変換結果のTypstコードの(人間にとっての)可読性」は重視していない。例えば、次のようなLaTeXの数式コードを考える。

\left(\frac{1}{2}x + 30\right)^4

この数式はTypstでは次のように簡潔に書けて、またそのように書くのが普通である。

(1/2 + 30)^4

従って、形式変換を目的とするならこれを出力すべきだろう3。しかしMiTeXは次のような、LaTeXよりも長くて複雑なコードを出力する。

lr(\( frac(1 ,2 )x + 3 0 \) )^(4 )

MiTeXは出力のコードについて「人間が関与せずに単にTypstの入力に与えるだけ」という状況を想定しているため、変換のロジックがより簡潔で高速になることを優先しているわけである。

MiTeXのキホンの使い方

「Typst文書中でLaTeXのコードを組版する」という目的で必要な範囲の機能について説明する。

  • mi(«LaTeX記述»)[content]: LaTeXの数式モードのコードをTypstのインライン数式に変換する。
  • mimath(«LaTeX記述»)[content]: LaTeXの数式モードのコードをTypstのブロック数式に変換する。
  • mitext(«LaTeX記述»)[content]: LaTeXのテキスト(非数式)モードのコードをTypstのエレメントに変換する4
  • これらの関数の引数の«LaTeX記述»は文字列(str値)またはrawエレメントで指定する。LaTeXのコードには\が大量に出現するので\エスケープが不要なrawエレメント(`…`)の方が便利であろう。

これらの機能を用いた簡単な例を以下に示す。

#import "@preview/mitex:0.2.5": *
#set page(paper: "a5")
// mimathの出力は通常のTypstのブロック数式となるので,
// Typst側で数式番号を制御できる.
#set math.equation(numbering: "(1)")

Now we're using *Typst*, and we can write
- #mi(`\sqrt{3+2\sqrt{2}} = 1+\sqrt{2}`),
- #mi(`(x^3)'=3x^2`) and
- #mi(`\lim_{x\to\infty}(1+1/n)^n = e`).
We also have
#mitex(`
  \left( \int_0^\infty \frac{\sin x}{\sqrt x}\,\mathrm{d}x \right)^2
  = \prod_{k=1}^\infty \frac{4k^2}{4k^2-1}.
  = \frac{\pi}{2}.
`)
and thus it follows that
// mimath自体は数式番号を出力できない
#mimath(`\begin{align}
    \sum_{k=0}^n \binom{n}{k}
    &= \sum_{k=0}^n \binom{n}{k}1^k1^{n-k} \\
    &= (1 + 1)^n = 2^n.
  \end{align}`)
Note again that
#mitext(`we're using \textbf{Typst} and \emph{not} LaTeX.`)

出力結果

MiTeXのホンキの使い方

以下では、MiTeXパッケージの使い方(インタフェース)の詳細について説明する。

引数

複数の関数に共通して表される引数について、その意味を先に解説しておく5

  • «LaTeX記述»[str|content]: 変換元のLaTeXのコードの文字列。str値ならその値そのもの、content値ならtextフィールドの値が使われる。
  • オプション引数mode[str]: モード。有効な値はtextまたはmathの何れか。概略としては、textは「テキスト(非数式)を処理対象とする」、mathは「数式を処理対象とする」ことを表す。

機能一覧

  • mitext(«LaTeX記述»)[content]: LaTeXのテキストモードのコードを変換して組版した結果のコンテンツ。
  • mimath(«LaTeX記述»)[content]: LaTeXの数式モードのコードを変換して組版した結果のmath.equationエレメント。
    オプション引数:
    • block[bool;既定値=true]: ブロック数式であるか。
    • 残りの引数: math.equation()に移譲される。
  • mi(«LaTeX記述»)[content]: mimath関数のblockの既定値をfalseに変えた版。オプション引数はmimathと同じ。
  • mitex(«LaTeX記述»)[content]: modeの値に応じてmitextmimathの何れか一方に移譲する。
    オプション引数:
    • mode[str;既定値="math"]: モード。textの場合はmitextmathの場合はmimathを移譲先にする。
    • 残りの引数: mitextまたはmimathに移譲される。
  • mitex-convert(«LaTeX記述»)[str]: LaTeXのコードを変換した結果のTypstのコード。
    オプション引数:
    • mode[str;既定値="math"]: モード。textの場合「LaTeXのテキストモードのコード→Typstのマークアップモードのコード」、mathの場合「LaTeXの数式モードのコード→Typstの数式モードのコード」の変換を行う。
    • spec[bytes;既定値=bytes(())]: 命令スペック(command spec)。プラグインが内部で使用する、LaTeX命令の仕様に関するデータ。空の場合は、パッケージが用意する標準のスペックが代わりに用いられる。
  • mitex-scope[dictionary]: パッケージが用意する標準の「eval用スコープ」。つまり実際に組版をする場合はmitex-convertで得たTypstコードをこのスコープの下でevalする。
  • mitex-wasm[module6]: WASM実装のプラグイン
    プラグインの関数7
    • convert_math(«LaTeX記述», «命令スペック»)LaTeXの数式モードのコードを変換した結果のTypstのコード。
    • convert_text(«LaTeX記述», «命令スペック»)LaTeXのテキストモードのコードを変換した結果のTypstのコード。

命令スペック(mitex-convertspec引数)と「evalのスコープ」を自分で用意することでLaTeXコード中で使用可能なLaTeXの命令のセットをカスタマイズできるようになっている。

まとめ

というわけで、「Typstは気に入っている😍がTypstの数式表記はトッテモ複雑で気に食わない😩」という人はMiTeXパッケージを利用してLaTeXの数式をドンドン書きましょう!💁


  1. Typst以外🤮😱🤯
  2. TypstパッケージとWebアプリはRustコードをWASMにコンパイルすることで実現している。
  3. 実際に、tex2typstはこのコードを出力する。
  4. テキストモードの変換は開発中でありまだ不完全である。
  5. つまり、«LaTeX記述»および「modeというオプション引数」については、この節にある説明が(も)適用される。
  6. Typstの0.13.0版以降ではインポートしたプラグインを指す値はmodule型として扱われるようになった。旧来の版で使われていたplugin型は廃止された。
  7. Typstの仕様で決まっている通り、WASMプラグインの関数の引数と戻り値は全てbytes型である。