LoginSignup
2
1

functional でもっと便利に tabularray

Last updated at Posted at 2023-12-05

この記事は TeX & LaTeX Advent Calendar 2023 の 5 日目の記事です。4 日目は doraTeX さんでした。6 日目は CareleSmith9 さんです。

今年の TeX & LaTeX Advent Caleandar 2023 の重点テーマは「(La)TeXで幸せになる方法」であり、アドベントカレンダーが始まったことを伝える はてな記事 では以下のように書かれていました。

徹底的に tabularray を使いこなして幸せ :blush: になるのもよし、

と言うことで、tabularray パッケージにおける functional ライブラリを利用して表作成の一部を自動化することで幸せになる方法を紹介します。

実は、tabularray パッケージを用いて増減表を書く方法の記事の最後で “functional ライブラリを利用すれば、セルの内容を評価して矢印を自動的に判定させることも出来る気がします。” と書いていました。

これらを受けて本記事では、functional ライブラリを利用して増減表の矢印を自動判定させることを最終目標とします。
また、tabularray については既にある程度よく知っているものとします。

増減表とは、高校数学で扱うグラフの概形をつかむための表です。

例:
zougen-image-4.png

functional ライブラリとは

functional ライブラリは functional パッケージを tabularray に導入するためのライブラリです。

functional パッケージは expl3 や LuaTeX の代替となる LaTeX2e 用の関数型プログラミングインターフェースです。
Lua のように外部言語に依存せず 1、かつ expl3 と異なり関数の評価が内側から外側に行います。(ちょっとこの辺りは僕には詳しく説明できないため、誰かに譲ります)

簡単に言えば、外部言語なしに Lua みたいな書き方でプログラミングできるパッケージです。

functional の基本

functional は expl3 によって実装されていることもあり、いくつかの類似点があります。
まず、これらの類似点を表にしておきます。

functional expl3
関数の開始 \IgnoreSpacesOn \ExplSyntaxOn
関数の終了 \IgnoreSpacesOff \ExplSyntaxOff
型の種数 10 19
関数の命名規則 lowerCamelCase snake_case

\IgnoreSpacesOn 2 ~\IgnoreSpacesOff の間では、expl3 と同じように行末の処理や半角スペースを気にする必要はない一方で、半角スペースを明示的に ~ を用いて表現する必要があります。3

また、基本的な関数は expl3 から導入されています。そのため、expl3 に慣れている人であればそれなりに使いやすいかも知れません。

新たな関数を定義するには \prgNewFunction を使い、戻り値に \prgReturn を使います。

functional には以下の 10 の型とそれぞれの接辞があります。

接辞 型名
bool 真偽値
str 文字列
tl トークンリスト
int 整数
fp 浮動点小数
dim ディメンション
clist コンマ区切りリスト
seq シーケンス
prop プロパティリスト
regex 正規表現

例えば、整数型における整数値をセットするための関数は \intSet となっています。
以降では、型の接辞を ‹type› と表現します。

変数

変数は \‹type›New によって宣言されます。
宣言される変数の変数名は自由に決めることが出来ますが、ローカル変数は l から、グローバル変数は g から始まる必要があります。
これは expl3 と類似しています。

また、\lTmpaStr のような規定変数があり、ローカル変数は \lTmpn‹type› で表現され、グローバル変数は \gTmpn‹type› で表現されます。n には abcijk のいずれかが入ります。

したがって、\lTmpaStr は文字列型 a 番目のローカル変数です。

tabularray における functional ライブラリ

functional ライブラリは \usepackage{tabularray} の後で \UseTblrLibrary を使って、導入します。

\usepackage{tabularray}
\UseTblrLibrary{functional}

セルの評価

tabularray パッケージで作成された表に process キーを用いることで、セル内を評価する関数を表に適用することが出来ます。

例えば、適当に定義された関数 \function を表に適用するには次のようにします。

\begin{tblr}{
    process = \function,
    % some style settings
  }
  % table contents
\end{tblr}

特定のセル情報の取得・反映

tabularray パッケージには、特定のセルの内容を取得する \cellGetText や特定のセルの内容やスタイルを決定する \cellSetText\cellSetStyle があります。

  • セルの内容の取得:
    • \cellGetText{<rownum>}{<colnum>}
  • セルの内容の書き込み:
    • \cellSetText{<rownum>}{<colnum>}{<text>}
  • セル・行・列のスタイルの指定:
    • \cellSetStyle{<rownum>}{<colnum>}{<style>}
    • \rowSetStyle{<rownum>}{<style>}
    • \columnSetStyle{<colnum>}{<style>}

例えば、2 行 4 列目のセルの内容を取得する場合は、次のように指定します。

\cellGetText{2}{4}

また、表の行数・列数は rowcountcolcount によって取得できます。ただし、実際には \arabic{rowcount} などとする必要があります。

そのため、3 行最終列目のセルに内容を書き込む場合は、次のように指定します。

\cellSetText{3}{\arabic{colcount}}{New~cell~content}

増減表の矢印を自動的に判定する

tabularray で作成された増減表の矢印を自動的に判定するコードを書きます。

今回作成したい関数は、増減表の矢印を増減表の微分行の正負符号から自動的に判別することです。
すなわち、次のような表を書いているときに、表のコンテンツとして矢印用のコマンドを明示的に書かないことを目標としています。

\begin{tblr}{
    process = \judgeArrow,
    % some style settings
  }
  x    & \cdots & -2  & \cdots & 1           & \cdots & 4  & \cdots \\
  f'   & +      & 0   & -      & -           & -      & 0  & +      \\
  f''  & -      & -   & -      & 0           & +      & +  & +      \\
  %%   ここのセルに自動的に矢印が出てくるようにしたい
  %%     ↓              ↓                      ↓             ↓
  f(x) &        & 5   &        & \frac{3}{4} &        & -2 &        \\
\end{tblr}

判定方法の概要

増減表は一次微分のみのものと二次微分までのものの 2 種類あります。そのため、今回はそれぞれに対応する関数を作成します。
加えて、表の行数からどちらの関数が適用されるかを条件分岐させます。

反復処理で各列の微分行のセル内容を取得して、正負によって条件分岐させ矢印を決定します。

今回、評価方法は +- をそのまま文字列として取り扱いました。
また、+- 以外の文字列の場合には、セル内にあるままのトークン列が出力されるようにします。

完成したコード

完成したコードは次のようになりました。

\judgeDiagonalArrow が一次微分のみの増減表用、
\judgeRoundedArrow が二次微分までの増減表用の判定関数です。

\judgeArrow は表の行数を評価して \judgeDiagonalArrow\judgeRoundedArrow に条件分岐させます。
そのため、実際に利用する関数は \judgeArrow です。

判定結果で出力される \diagonalArrow**\roundedArrow** は、前回の記事 で TikZ を用いて描いた矢印を利用しています。

\IgnoreSpacesOn
\prgNewFunction \judgeDiagonalArrow { } {
  % 2 列目から最終列まで反復試行
  \intStepOneInline {2} {\arabic{colcount}} {
    % 2 行 ##1 列目のセルの内容を lTmpaStr に格納
    \strSet \lTmpbStr { \cellGetText {2} {##1} }
    % lTmpaStr の内容が + か - かそれ以外かを条件分岐
    \strCaseF {\strUse \lTmpaStr} {
        % + または - に応じた矢印を lTmpiTl に格納
        {+} { \tlSet \lTmpiTl {\evalWhole{\diagonalArrowNE}} }
        {-} { \tlSet \lTmpiTl {\evalWhole{\diagonalArrowSE}} }
      }
      {
        % + でも - でもない場合は 3 行 ##1 列目のセルの内容を lTmpiTl に格納
        \tlSet \lTmpiTl { \evalWhole{\cellGetText {3} {##1}} }
      }
    % 3 行 ##1 列目のセルに lTmpiTl を書き込む
    \cellSetText {3} {##1} { \tlUse \lTmpiTl }
  }
}
\prgNewFunction \judgeRoundedArrow { } {
  % 2 列目から最終列まで反復試行
  \intStepOneInline {2} {\arabic{colcount}} {
    % 2 行 ##1 列目のセルの内容を lTmpbStr に格納
    \strSet \lTmpbStr { \cellGetText {2} {##1} }
    % 3 行 ##1 列目のセルの内容を lTmpcStr に格納
    \strSet \lTmpcStr { \cellGetText {3} {##1} }
    % lTmpbStr と lTmpcStr を連結して lTmpaStr とする
    \strConcat \lTmpaStr \lTmpbStr \lTmpcStr
    % lTmpaStr の内容が ++、+-、-+、-- かそれ以外かを条件分岐
    \strCaseF {\strUse \lTmpaStr} {
        % ++、+-、-+、-- に応じた矢印を lTmpiTl に格納
        {++} { \tlSet \lTmpiTl {\evalWhole{\roundedArrowRU}} }
        {+-} { \tlSet \lTmpiTl {\evalWhole{\roundedArrowUR}} }
        {-+} { \tlSet \lTmpiTl {\evalWhole{\roundedArrowDR}} }
        {--} { \tlSet \lTmpiTl {\evalWhole{\roundedArrowRD}} }
      }
      {
        % ++、+-、-+、-- のいずれでもない場合は 3 行 ##1 列目のセルの内容を lTmpiTl に格納
        \tlSet \lTmpiTl { \evalWhole{\cellGetText {4} {##1}} }
      }
    % 4 行 ##1 列目のセルに lTmpiTl を書き込む
    \cellSetText {4} {##1} { \tlUse \lTmpiTl }
  }
}
\prgNewFunction \judgeArrow { } {
  % 行数が 3 か 4 か条件分岐
  \intCase {\arabic{rowcount}} {
    % 3 であれば judgeDiagonalArrow を実行
    {3} {\judgeDiagonalArrow}
    % 4 であれば judgeRoundedArrow を実行
    {4} {\judgeRoundedArrow}
  }
}
\IgnoreSpacesOff

実際に、次のような増減表に \judgeArrow 関数を適用させてみると、ちゃんと矢印の含まれた増減表を取得できます。

\begin{tblr}{
    width = {0.7\linewidth},
    hline{1,Z} = {0.08em},
    hline{2,Y}, vlines,
    colspec = {*{8}{X[1,c]}},
    columns = { mode = dmath },
    process = \judgeArrow,
  }
  x    & \cdots & -2  & \cdots & 1           & \cdots & 4  & \cdots \\
  f'   & +      & 0   & -      & -           & -      & 0  & +      \\
  f''  & -      & -   & -      & 0           & +      & +  & +      \\
  f(x) &        & 5   &        & \frac{3}{4} &        & -2 &        \\
\end{tblr}

zougen-image-3.png

余談

tabularray パッケージの functional ライブラリを利用する方法を紹介しました。
表作成はセルの内容に応じてスタイルを変更することもあるので、自動化することでより幸せになることでしょう。

tabularray パッケージは、第 9 版 LaTeX 美文書作成入門 でも取り扱われている使いやすい表作成パッケージです。
表にたくさん触れる機会がある人や、表のコンテンツとスタイルを分離したいと考えている人、セル結合の必要がある人等、ぜひ一度使ってみることをオススメします。

tabularray パッケージに関するざっくりとした使い方に関して、以下の記事もあります。こちらも是非参照してください。

functional パッケージは、このパッケージ自体が「誰が使ってるねん」と思うくらいインターネッツ検索してもヒットしません。(特に日本語!)4
LaTeX パッケージについて多数投稿されているあの 天地有情 さんでさえ functional パッケージは紹介されていませんでした :cry:

どちらのパッケージも、日本語ネット情報はあまり多くありません。興味がある方はぜひ使って記事として公開してください。(特に functional……)

ちなみに、functionaltabularray の作者は同じ人です。

functional の tips

増減表の矢印を判定させる関数を書く際に個人的に詰まったところを tips として紹介します。

変数を関数内で利用する

\strCase のような条件分岐でテスト文字列にローカル変数 (\lTmpaStr) を用いる場合、そのままでは変数として利用できません。

例えば、次のようなコードを書いてみます。

\IgnoreSpacesOn
\strSet \lTmpaStr {abc}
\strCaseF {\lTmpaStr} {
  {123} {Numeral}
  {abc} {Alphabet}
  {αβγ} {Greek}
}{No~match}
\IgnoreSpacesOff

3 行目の \strCaseF {\lTmpaStr} { のテスト文字列は “\lTmpaStr” となっています。これは変数 \lTmpaStr に格納された内容をテスト文字列として利用しているのではなく、文字列 “\lTmpaStr” そのものです。
そのため、“No match” が出力されます。

これを回避するには、テスト文字列内でも \strUse する必要があります。

\IgnoreSpacesOn
\strSet \lTmpaStr {abc}
\strCaseF {\strUse \lTmpaStr} {
  {123} {Numeral}
  {abc} {Alphabet}
  {αβγ} {Greek}
}{No~match!}
\IgnoreSpacesOff

これによって、\lTmpaStr がテスト文字列に利用されます。

変数に格納された LaTeX コマンドを出力する

functional を扱っていて少し困ったことがありました。それは変数に格納された LaTeX コマンドが出力されないことです。

例えば、ローカル変数 \lTmpaStr\LaTeX を格納して出力しようと次のように書いてみます。
LaTeX ロゴが出力されることが期待されますが、これによって出力される文字列は “\LaTeX” です。

\IgnoreSpacesOn
\strSet \lTmpaStr { \LaTeX }
\strUse \lTmpaStr
\IgnoreSpacesOff

上の例での間違いは 2 点です。

  • LaTeX コマンドはトークンリスト型である
  • \evalWhole を用いて展開した状態で格納する必要がある

これらを踏まえると、次のように書くことで LaTeX ロゴが出力されるようになります。

\IgnoreSpacesOn
\tlSet \lTmpaTl { \evalWhole{\LaTeX} }
\tlUse \lTmpaTl
\IgnoreSpacesOff

反復処理のカウンタ

functional では、反復処理を行う \‹type›StepInline\intStepOneInline があります。

ifthen パッケージ等ではカウンタを適当に定義してから反復処理を書きますが、functional では #1 がカウンタを担います。(expl3 でもそう)

そのため、反復処理内で反復処理を行う場合カウンタが重複してしまうため、# が連続するようになります。
すなわち、外側の反復処理のカウンタでは #1 を使うのに対して、内側の反復処理のカウンタでは ##1 を使います。
さらに、3 重の反復処理を行う場合、最内の反復処理のカウンタは ####1# が 4 つ)です。5

これを踏まえると、N 重の反復処理の最内のカウンタは 2N 個の # が連続します。

これに加えて、functional では \prgNewFunction を用いて関数を定義することが出来ます。これももちろん引数を #1 として表現します。
そのため、\prgNewFunction 内で反復処理する場合も # を連続させるため、ふつうに ##1####1 が登場します。

もう少し増減表を簡単に

\judgeArrow 関数を利用した際の表のコンテンツ部分を見ると、\cdots は省略できそうです。

\cdots そのものを明示的に入力しなくても、1 行目が空白または ... があるようなセルには \cdots が自動的に挿入されるようなコードを \judgeArrow に追加しておいても良いかもしれません。

例えば、次のような \varDots 関数を書きます。

\prgNewFunction \varDots { m } {
  % 1 行 #1 列目のセルの内容を lTmpaStr に格納
  \strSet \lTmpaStr { \cellGetText {1} {#1} }
  % lTmpaStr が null または .{3}、\cdots の場合は true コードを実行
  \strCaseT {\strUse \lTmpaStr} {
    {} {}
    {.} {}
    {..} {}
    {...} {}
    {\cdots} {}
  } {
    % lTmpiTl に \cdots を格納
    \tlSet \lTmpiTl {\evalWhole{\cdots}}
    % 1 行 #1 列目に lTmpiTl を書き込む
    \cellSetText {1} {#1} { \tlUse \lTmpiTl }
  }
}

これを \judgeDiagonalArrow\judgeRoundedArrow 関数内に挿入しておけば、\cdots も省略できます。

完全に空白がイヤな場合は、.... を入れておくだけでも \cdots になるようにしてあります。

\cdots を省略できる \judgeArrow 関数(折りたたみ)
\IgnoreSpacesOn
\prgNewFunction \varDots { m } {
  \strSet \lTmpaStr { \cellGetText {1} {#1} }
  \strCaseT {\strUse \lTmpaStr} {
    {} {}
    {.} {}
    {..} {}
    {...} {}
    {\cdots} {}
  } {
    \tlSet \lTmpiTl {\evalWhole{\cdots}}
    \cellSetText {1} {#1} { \tlUse \lTmpiTl }
  }
}
\prgNewFunction \judgeDiagonalArrow { } {
  \intStepOneInline {2} {\arabic{colcount}} {
    \varDots{##1}
    \strSet \lTmpaStr { \cellGetText {2} {##1} }
    \strCaseF {\strUse \lTmpaStr} {
        {+} { \tlSet \lTmpiTl {\evalWhole{\diagonalArrowNE}} }
        {-} { \tlSet \lTmpiTl {\evalWhole{\diagonalArrowSE}} }
      }
      {
        \tlSet \lTmpiTl { \evalWhole{\cellGetText {3} {##1}} }
      }
    \cellSetText {3} {##1} { \tlUse \lTmpiTl }
  }
}
\prgNewFunction \judgeRoundedArrow { } {
  \intStepOneInline {2} {\arabic{colcount}} {
    \varDots{##1}
    \strSet \lTmpbStr { \cellGetText {2} {##1} }
    \strSet \lTmpcStr { \cellGetText {3} {##1} }
    \strConcat \lTmpaStr \lTmpbStr \lTmpcStr
    \strCaseF {\strUse \lTmpaStr} {
        {++} { \tlSet \lTmpiTl {\evalWhole{\roundedArrowRU}} }
        {+-} { \tlSet \lTmpiTl {\evalWhole{\roundedArrowUR}} }
        {-+} { \tlSet \lTmpiTl {\evalWhole{\roundedArrowDR}} }
        {--} { \tlSet \lTmpiTl {\evalWhole{\roundedArrowRD}} }
      }
      {
        \tlSet \lTmpiTl { \evalWhole{\cellGetText {4} {##1}} }
      }
    \cellSetText {4} {##1} { \tlUse \lTmpiTl }
  }
}
\prgNewFunction \judgeArrow { } {
  \intCase {\arabic{rowcount}} {
    {3} {\judgeDiagonalArrow}
    {4} {\judgeRoundedArrow}
  }
}
\IgnoreSpacesOff

\cdots を省略できる \judgeArrow 関数を利用すると、次のように書くことで済むようになります。

\begin{tblr}{
    width = {0.7\linewidth},
    hline{1,Z} = {0.08em},
    hline{2,Y}, vlines,
    colspec = {*{8}{X[1,c]}},
    columns = { mode = dmath },
    process = \judgeArrow,
  }
  x    &   & -2  &   & 1           &   & 4  &   \\
  f'   & + & 0   & - & -           & - & 0  & + \\
  f''  & - & -   & - & 0           & + & +  & + \\
  f(x) &   & 5   &   & \frac{3}{4} &   & -2 &   \\
\end{tblr}

ここまで簡略化できればかなり楽でしょう。ただ、どれくらい増減表を書きたい人がいるのかは不明ですが……

  1. functional は expl3 で実装されているので、expl3 が利用できるエンジンであれば使えます。

  2. \IgnoreSpacesOn は LaTeX 標準の \ignorespaces に似ていますが、まったく意味が異なります。

  3. 実は、functional では関数を必ずしも \IgnoreSpacesOn~\IgnoreSpacesOff で利用する必要はありません。とは言っても、行末の処理を考えることは面倒になることがあるため、\IgnoreSpacesOn~\IgnoreSpacesOff の間で関数を定義するべきです。

  4. Google で "functional" と検索しても絶対に上手く出てきません。(そもそも、ネーミング的に厳しいです)"functional tabularray" や変数の "ltmpaint" などと検索する必要があります。

  5. 実は \newcommand でも入れ子にすることで # が連続することが知られています。(参照

    参照しているコメントでは 2 重の \newcommand についてのみしか言及されていませんが、3 重の \newcommand を実際に行ってみると最内の \newcommand の引数は ####1 となっています。

    \newcommand{\test}[1]{
      \newcommand{\Test}[1]{
        \newcommand{\TEST}[1]{
          #1, ##1, ####1
        }
      }
    }
    
    \test{a} \Test{b} \TEST{c}
    
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1