前回のプログラミングの結果がイイカンジ🙂だったので、味を占めてもうチョット難しい問題を試してみます。
イマドキのAIのTeXプログラミングをもっと添削する
前回の問題の説明で「RubyのString#split
を作ろうとすると地獄に陥る😱」と言いましたが、今回はその「String#split
(の制限版)を作る」という問題です🤯
自作のLaTeXパッケージの実装でもこのsplitマクロを何度か使ったこと(アレとかコレとか)があります。
TeX特有のインタフェースの指示が元々長くなるので、今回はRubyからの変換ではなく「要求仕様を日本語で記述する」形にしました。
※この問題ではGemini 2.5 Proを使用しました。
入力したやつ
以下の要件を満たすTeX言語のマクロ\mySplit
を実装してください。
入力
-
\mySplit{<文字列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@aux
やmy@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言語を書く力も読む力も要らなくなる?😲 文字通りの富豪的💰プログラミングだ……😐
-
前節で出てきた
\my@split@firstofone
は結局不適切だったわけですが、それ以前の問題としてそもそも\ifx\my@split@marker\my@split@firstofone#1
というコードは、\my@split@firstofone
を予め一回展開しないと意味を成さないでしょう。不要なときに\expandafter
祭りを散々やっておいて、肝心な時に忘れてしまっています(ざんねん🙃)↩ -
区切り文字列が空の場合の例外的な処理の中では
\myPre
・\myPost
にローカルな代入を行っていて、動作が一貫していません。↩ -
処理途中の代入をローカルにする必要があるのは「当該のマクロがネストして使われる」場合ですが、今の
\mySplit
については「何らかの意味でネストする」ような使い方がそもそも存在しないわけです。↩ -
TeX on LaTeXのコードは原則的に「そもそも最初から
\makeatletter
の状態であるファイル」(*.sty
や*.cls
)に記述されるべきであると自分は考えています。↩