以下の内容はhttps://zrbabbler.hatenablog.com/entry/2025/07/09/221725より取得しました。


イマドキのエ~アイの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)に記述されるべきであると自分は考えています。



以上の内容はhttps://zrbabbler.hatenablog.com/entry/2025/07/09/221725より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14