マクロツイーター

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

それでも TeX でプログラミングしたい人のための何か (4)

あるいは 〜私の TeX プログラム変換環境〜

ステップ 2 : 変数関連の処理を行う

ここからは、Lua 言語の範疇において、元のプログラムを「TeX に近い」形に変換していく。元のプログラムを再掲しておく。

[eltaso0.lua]

alpha_name = {
  [0] = "ぜっと",
  "えー", "びー", "しー", "でー", "いー", "えふ", "じー",
  "えいち", "あい", "じぇー", "けー", "える", "えむ", "えぬ",
  "おー", "ぴー", "きゅー", "あーる", "えす", "てぃー", "ゆー",
  "ぶい", "だぶりゅー", "えっくす", "わい"
}
digit = {
  [0] = "",
  "一", "二", "三", "四", "五", "六", "七", "八", "九"
}

function knumeral(n)
  local dm, dc, dx, di = 
    ("%04d"):format(n):match("^(.)(.)(.)(.)$")
  local km = knum_pos(dm, "千")
  local kc = knum_pos(dc, "百")
  local kx = knum_pos(dx, "十")
  return km .. kc .. kx .. digit[tonumber(di)]
end
function knum_pos(d, u)
  d = tonumber(d)
  if d == 0 then return ""
  elseif d == 1 then return u
  else return digit[d] .. u
  end
end
function eltaso_name(n)
  return knumeral(n) .. "反田" .. alpha_name[n % 26]
end
function eltaso(n)
  if n > 9999 then n = 9999 end
  for j = 1, n do
    print(eltaso_name(j))
  end
end

eltaso(1000)

ローカル変数をグローバル変数に置き換える

上記のプログラムで、local というキーワードと共に宣言された変数はローカル変数(局所変数;可視な範囲がプログラムの一部(特定の関数の中など)に限られる変数)である。変数をローカルだと指定する方法は言語によって異なる*1が、ともかく現在普及しているほとんどの言語には「ローカル変数」の概念が存在する。ところが、TeX は違う。

TeX には(上述の意味での)「ローカル変数」は存在しない

従って、プログラムをローカル変数を使わない(グローバル変数だけを用いる)形に変換しておく。ただし、引数の変数はローカル変数の一種であるがこれは今はそのままで構わない。(次の段階で考慮する。)

eltaso0.lua の場合、単純に local キーワードを全部取り去って全ての変数をグローバルに変えればそれでよいが、多くの場合、そのままでは異なる箇所で使われる同名の変数が衝突するので変数名を変えるなりして対処する必要がある。((なお、Lua の文法では for 文のループ変数(eltaso0.lua では j が該当する)は常に for のブロック内でローカルと見做される。だから j は実際にはローカルなままであるが、(衝突がないことを確認した上で)グローバルと思い込むことにしよう。))

なお、今の作業および以降の変換作業において、変数の衝突がないことの確認を容易にするため、プログラムで使われている変数の一覧を書き出しておくことにしよう。

補足: TeX で「ローカル変数」のように使われるもの

TeX には Lua と同じ意味での(静的スコープでの)ローカル変数はない。しかし(既に存在する)変数について「一時的に別の値を持たせる」という機能(「ローカル(局所的)な代入」という)がある。具体的に言うと、\begingroup というプリミティブを実行すると、以降に行われた代入の効果は \endgroup が実行されると失われる(つまり \begingroup 実行の直前の値に戻る)というものである。*2従って、「ローカル変数」に相当するものが欲しい場合は、「まずローカルな代入のための変数*3を(\tclt@temp のような名で)用意して、それを複数の箇所の \begingroup\endgroup で使い回す」という手法が使われる。この際に、LaTeXカーネルが既にそのような変数を幾つか用意している(\@tempcnta\@tempdima 等)ので、それを借用することもよく行われる。ただし、この「ローカルな代入」の動作は「(Lua 等の)ローカル変数」とは相違するところがあり、それをよく理解しない者が無闇に使うと、訳の分からない誤動作を引き起こす原因になる。((特に \@tempcnta 等の「借用」の使用法を間違えると、LaTeX の動作に不具合をもたらすことになり危険である。))そういう訳で、初級者は「ローカルな代入」を使わずにグローバル変数だけで何とか頑張るという方法を採った方がよいと、私は考えている。

引数の名前を変えて、その使用を検証する

関数の引数は後で TeX のマクロパラメタ(#1#2、……〉に置き換えられるが、この段階で _1_2 等の「他の変数と一目で区別がつく名前」に変換しておく。その上で、後で TeX に直した時に上手くいかなくなる箇所をこの時点で修正しておく。名前を変えるのは、「失敗する箇所」を見つけ易くするためである。(だから慣れてきたら名前の変更は飛ばしても構わない。*4

TeX のマクロの展開処理は、他の言語の関数の実行(評価)よりも、寧ろ C言語におけるマクロの展開のような単純な文字列の置換に類似している(だって「マクロ」だから!)。

#define     TWICE(a)    (a *= 2)

例えば上のような(C言語の)引数付きマクロを考える。*5ここでもし「TWICE(42);」のように定数を引数として渡すと、「(42 *= 2);」のような無意味な文に変換されてしまう。当該の eltaso0.lua でいうと関数 eltaso() の中の次の箇所で同様の問題が生じる。

-- 引数 n → _1 と変換
  if _1 > 9999 then _1 = 9999 end

引数には「1000」などの定数になりうるので、「_1 = 9999」という代入文は不適切である。((そのまま TeX に直すと「#1=9999 」となるだろうが、それではマクロ展開後に「1000=9999 」になってしまう。))そこで、新たに max という(グローバル)変数を導入して、引数 _1 の値を最初にコピーしておくことにする。

-- 引数 n → _1 と変換
  max = _1
  if max > 9999 then max = 9999 end

補足: 引数の挙動についての詳細

ただし、このように「左辺」が引数になることが常に間違いである訳ではない。引数に与えられるものが常に変数であると判っているならばそれも可能である。ただしその場合、引数は元の変数の別名となることに注意する必要がある。例えば、上掲の TWICE() マクロで

  int foo = 42;
  TWICE(foo);
  printf("%d", foo); /*==> 84 */

のように変数 foo を渡すとその変数自身が更新されることになる。(これは C言語の関数では絶対に起こらないことである。)TeX の場合でも同様なコードを意図的に用いることができる。*6ここで述べた

引数に渡された変数が元の変数の別名になる

という性質については、意図的な参照渡しを行う場合以外でも注意が必要である。例えば、TeX(にされる途中の Lua)のコードが次のようになったとする。

function some_func(_1)
  -- この中で変数 x を使用
  x = 100; y = _1 * 2
  -- ......
end
-- 別の箇所で
  some_func(x)
  -- ......

この時、some_func の中の x と外の x は実際には衝突することになるので解消する必要がある。

TeX での実現が難しい処理を選り出して別の関数にしておく

Lua から TeX に「機械的に」変換するのが難しい処理をピックアップしておく。そしてそれが式や関数本体の一部として埋まっている場合は、別個の関数として分離しておく。(なお、配列変数の処置は後で行うのでここでは手をつけない。)

eltaso0.lua の場合、まず次の箇所が該当する。((この代入文は多重代入であり、string.match() 関数は多値の戻り値を持つ。))

  dm, dc, dx, di =   -- local を消去
    ("%04d"):format(n):match("^(.)(.)(.)(.)$")

Luastring.format()C言語sprintf() に相当)および string.match() 関数(正規表現を用いた抽出)に直接対応する機能は TeX にはないので、この部分は別の方法で実装する必要がある。なので次のような関数を新設してそれでこの部分を置き換えることにしよう。

function split_digit(_1) -- TeXでは再実装が必要
  return ("%04d"):format(_1):match("^(.)(.)(.)(.)$")
end

あと、「整数除算の余り」も TeX では直接求めることができない。

function eltaso_name(n)
  return knumeral(n) .. "反田" .. alpha_name[n % 26]
end

これも一旦別の関数にしておこう。

function remainder(_1, _2) -- TeXでは再実装が必要
  return _1 % _2
end

変換後のプログラム
[eltaso1.lua]

-- [変数一覧]
-- 整数: max, j
-- 文字列: dm, dc, dx, di, km, kc, kx
-- 配列: alpha_name, digit

alpha_name = {
  [0] = "ぜっと",
  "えー", "びー", "しー", "でー", "いー", "えふ", "じー",
  "えいち", "あい", "じぇー", "けー", "える", "えむ", "えぬ",
  "おー", "ぴー", "きゅー", "あーる", "えす", "てぃー", "ゆー",
  "ぶい", "だぶりゅー", "えっくす", "わい"
}
digit = {
  [0] = "",
  "一", "二", "三", "四", "五", "六", "七", "八", "九"
}

function knumeral(_1)
  dm, dc, dx, di = split_digit(_1)
  km = knum_pos(dm, "千")
  kc = knum_pos(dc, "百")
  kx = knum_pos(dx, "十")
  return km .. kc .. kx .. digit[tonumber(di)]
end
function split_digit(_1) -- TeXでは再実装が必要
  return ("%04d"):format(_1):match("^(.)(.)(.)(.)$")
end
function knum_pos(_1, _2)
  -- tonumber の扱いは保留
  if tonumber(_1) == 0 then return ""
  elseif tonumber(_1) == 1 then return _2
  else return digit[tonumber(_1)] .. _2
  end
end

function eltaso_name(_1)
  return knumeral(_1) .. "反田" .. alpha_name[remainder(_1, 26)]
end
function remainder(_1, _2) -- TeXでは再実装が必要
  return _1 % _2
end

function eltaso(_1)
  max = _1
  if max > 9999 then max = 9999 end
  for j = 1, max do
    print(eltaso_name(j))
  end
end

eltaso(1000)

ところで、このステップの作業では、全て Lua の枠内で行っている。従って、上記の変換結果のプログラムは正当な Lua のプログラムであり、しかも元のプログラムと同じ出力を行うはずなので確認してみよう。この性質を利用して、変換を誤りなく行ったかを検証することが(ある程度)できるであろう。

*1:Perl では my でローカル変数を宣言する。Ruby ではブロック内で初めて参照された変数は自動的にローカルになる。C言語Java では、関数(メソッド)の内部で宣言された変数はローカルである。

*2:昔の Perl で使われた local 宣言がこの動作に近い。ただし TeX の局所化では自動的に全ての変数(代入)が対象になるところが Perl の local と異なる。一番近いのは Windows のバッチの setlocal 〜 endlocal であろう ;-)

*3:このような変数が実際に「ローカル変数」と呼ばれることがある。

*4:私自身はやっていない。

*5:あまり良い例ではないが……。

*6:「元の言語」が Lua のような参照渡しの機構のない言語だとそういうコードを用いる機会がないとは思うが。