マクロツイーター

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

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

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

ステップ 3 : 関数に関連する処理を行う

元のプログラムは前回の記事の最後に掲載した eltaso1.lua

関数を追放する

ここで「関数」というのは「戻り値を返す手続き」(だから式の中で使うことができる)のことである。かなり多くの言語で、「戻り値を返さない手続き」(以降単に「手続き」と呼ぶ)も「関数」として扱われているが、そのような「手続き」には手をつけない。何故関数を無くす必要があるかというと、

TeX には関数が存在しない
(マクロは関数じゃない!)

からである。といっても、関数の呼び出しを全部中身に書き換えてプログラム全体を 1 つの手続きにしてしまう、なんて間抜けなことをやってはいけない(絶対!)。ここで行うのは「関数を手続きに変換する」ことである。

関数を手続きに変換するには、「戻り値」の扱う方法を考える必要があり、種々の方法が考えられるが、ここでは最も愚直な方法である「特定の変数に戻り値を代入する」という方式を採用する。すなわち:

  • 戻り値を受け取る専用の変数として、整数用に ires、文字列用に sres を用意する。
  • 値を return で返している箇所を全て ires または sres への代入に変える。
  • (式の中での)関数の呼出を、「手続きの呼出 + ires/sres の参照」に変える。
function remainder(_1, _2)
  return _1 % _2
end

この関数は単純に式の値(整数)を返している。なので、returnires = に変更する。

function remainder(_1, _2)
  ires = _1 % _2
end

もう少し複雑な例。

function eltaso_name(_1)
  return knumeral(_1) .. "反田" .. alpha_name[remainder(_1, 26)]
end

値を「返す」側の対処としては、returnsres = に換えればよい。「返される」側については、これまで「knumeral(_1)」自体が(式として)戻り値を表していたのが、knumeral() を手続きに変換した後は、「knumeral(_1)」を実行すれば sres に求める値が入る、という規約に変更される。remainder() の方も同様である(こちらは上で実際に変更を行った)。結果的に、以下のようになる。

function eltaso_name(_1)
  knumeral(_1) -- sres に返る
  remainder(_1, 26) -- ires に返る
  sres = sres .. "反田" .. alpha_name[ires]
end

このように、「手続きへの変換」は各々の関数ごとに独立して行うことができることに注意。((ただし、eltaso_name() のコードの場合、実は「remainder()sres を破壊しない」ということに依存しているのだが。))この作業で特に問題になるのが、split_digit() 関数である。

function split_digit(_1)
  return ("%04d"):format(_1):match("^(.)(.)(.)(.)$")
end
function knumeral(_1)
  dm, dc, dx, di = split_digit(_1) -- ここで呼び出している
  km = knum_pos(dm, "千")
  -- 以下略
end

この関数は 4 つの整数値を戻り値として返す。一般的な対処法としては、こういう場合は単純に「戻り値専用変数」の個数を増やせばよい、つまり iresa、iresb、……を新たに設ければよい。ただこの 1 つの関数のためだけに 3 つ変数を増やすのはあまり嬉しくない。この split_digit()knumeral() の完全な下請けである(knumeral() の中に 1 回呼出があるだけ)であるので、ここでは、呼出側が受け取っている変数(dm 等)に直接代入する形に変更することで済ませよう。

function split_digit(_1)
  dm, dc, dx, di = ("%04d"):format(_1):match("^(.)(.)(.)(.)$")
end
function knumeral(_1)
  split_digit(_1) -- dm, dc, dx, di に戻り値が入る
  knum_pos(dm, "千"); km = sres
  -- 以下略
end

knum_pos() 関数には本体の途中で return で値を返して抜ける嘉穂sがある。*1

function knum_pos(_1, _2)
  if tonumber(_1) == 0 then return ""
  elseif tonumber(_1) == 1 then return _2
  else return digit[tonumber(_1)] .. _2
  end
end

このようなパターンは、「iressres)への代入 + return(脱出)」に置き換える。そして、return の処理を次の段階で考える。

function knum_pos(_1, _2)
  if tonumber(_1) == 0 then sres = ""; return
  elseif tonumber(_1) == 1 then sres = _2; return
  else sres = digit[tonumber(_1)] .. _2; return
  end
end
手続きの途中で return している箇所を無くす

TeX のマクロの途中で「return で脱出」することはできない*2ので、手続きの途中で return で抜けている箇所は、何とかして return のない形に書き直す。例えば、次のコードの場合:

function example(_1)
  foo(_1)
  if ires == 0 then ires = -1; return end
  bar(_1, ires)
  if ires == 0 then ires = -1; return end
  gee(_1, ires)
end

以下のようにして、return のない等価なコードに書き直せる。

function example(_1)
  foo(_1)
  if ires == 0 then ires = -1
  else
    bar(_1, ires)
    if ires == 0 then ires = -1
    else gee(_1, ires)
    end
  end
end

eltaso1.lua において該当するのは、先に示した knum_pos() 関数であるが、この場合、全ての条件分岐において、return は「その手続きで最後に実行された文」になっていることが判る。だから、この場合は単純に return を取ればよい。

変換後のプログラム
[eltaso2.lua]
-- [変数一覧]
-- 整数: max, j, ires(戻り値)
-- 文字列: dm, dc, dx, di, km, kc, kx, sres(戻り値)
-- 配列: alpha_name, digit

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

function knumeral(_1)
  split_digit(_1)
  knum_pos(dm, "千"); km = sres
  knum_pos(dc, "百"); kc = sres
  knum_pos(dx, "十"); kx = sres
  sres = km .. kc .. kx .. digit[tonumber(di)]
end
function split_digit(_1) -- TeXでは再実装が必要
  dm, dc, dx, di =  -- 直接に目的の変数に値を返す
    ("%04d"):format(_1):match("^(.)(.)(.)(.)$")
end
function knum_pos(_1, _2)
  if tonumber(_1) == 0 then sres = ""
  elseif tonumber(_1) == 1 then sres = _2
  else sres = digit[tonumber(_1)] .. _2
  end
end

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

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

eltaso(1000)

*1:よく見れば「途中ではない」のだが、そういう考察は後の段階に回す。

*2:マクロは実行コード列に「展開される」ものだからこれは当然である。