概要
いわゆるGoFのデザインパターン本に出てくる「テンプレートメソッドパターン」を使って、LaTeXの \section
マクロを作り直してみます。
ポイント:
-
\section
を、より小さいマクロの組み合わせとして定義する。 - カスタマイズするとき、必要なマクロだけを上書きするだけで済む。
- TeX言語では継承は使えないので、かわりにマクロの上書きを利用する。
オブジェクト指向言語じゃないのに「テンプレートメソッドパターン」とはおかしなことかもしれません。
しかし実装方法が、継承によるメソッドオーバーライドなのか、それともマクロ定義の上書きなのかという違いだけであり、考え方はテンプレートメソッドパターンそのものです。
(注:あるマクロを小さいマクロに分解するだけでは「テンプレートメソッドパターン」とは言いません。挙動を上書きできるように分解してこその「テンプレートメソッドパターン」です。)
なおTeXおよびLaTeXでは「マクロ」とは言わず、「コントロール・シーケンス」または「制御綴」と呼ぶのが正しいそうです。
しかしこの記事では、一般的な呼び名として普及している「マクロ」という用語を使います。
\section
を定義する
\section
をステップ・バイ・ステップで定義してみます。
Step 1. とても原始的な\Sectionマクロ
まずは、次のようなとても原始的な \Section
マクロを考えます。
(\section
ではなく \Section
にします。)
%% セクションタイトル用のマクロ
\newcommand\Section[1]{% % #1: タイトル
\vspace{2\Cvs}% % タイトル上部に2行分のマージン(余白)
\refstepcounter{section}% % セクション用のカウンタを増やす
{%
\setlength{\parindent}{0pt}% インデント幅をゼロに
\headfont\Large% % フォントを大きめのゴシック体に変更
\thesection% % セクション番号を表示
\hspace{1zw}% % 全角1文字分の空白
#1% % 引数(タイトル)を表示
\par\nobreak% % 改行、かつ直後での改ページを防ぐ
}%
\vspace{1\Cvs}% % タイトル下部に1行分のマージン(余白)
}
使い方は \section
と同じです(ただし*
などを除く)。
\Section{セクションタイトルのサンプル}
Step 2. 上部と下部のマージンをマクロ化
次に、タイトルの上部と下部にマージン(余白)を入れている部分を、独立したマクロにします。
%% セクションタイトル用のマクロ
\newcommand\Section[1]{% % #1: タイトル
\Section@topmargin% % !!!!!
\refstepcounter{section}%
{%
\setlength{\parindent}{0pt}
\headfont\Large%
\thesection%
\hspace{1zw}%
#1%
\par\nobreak%
}%
\Section@bottommargin% % !!!!!
}
%% セクションタイトル上部のマージン(余白)
\newcommand{\Section@topmargin}{\vspace{2\Cvs}} % 2行分
%% セクションタイトル下部のマージン(余白)
\newcommand{\Section@bottommargin}{\vspace{1\Cvs}} % 1行分
これで、マージンだけをカスタマイズできるようになりました。
たとえば次のようにマクロを上書きすれば、セクションごとに改ページできます。
%% マージンを入れるかわりに、改ページする
\renewcommand{\Section@topmargin}{%
\ifnum\c@section=0% % 最初のセクションなら
\vspace{2\Cvs}% % マージン(余白)を入れる
\else% % そうでないなら
\clearpage% % 改ページする
\fi%
}
従来の \section
定義では、これをするだけでも \section
の定義をコピーして上書きしなければいけませんでした。
\Section
マクロを分解しておけば、全体を上書きすることなく(つまり他の部分に影響を与えることなく)、必要な箇所だけを変更できます。
Step 3. セクションタイトル部をマクロ化
続いて、セクションの番号とタイトルを表示する部分をマクロ化します。
ついでにフォント指定もマクロ化します。
%% セクションタイトル用のマクロ
\newcommand\Section[1]{% % #1: タイトル
\Section@topmargin%
\refstepcounter{section}%
{%
\setlength{\parindent}{0pt}
\Section@font% % !!!!!
\Section@title{\thesection}{#1}% % !!!!!
\par\nobreak%
}%
\Section@bottommargin%
}
%% セクションタイトル上部のマージン(余白)
\newcommand{\Section@topmargin}{\vspace{2\Cvs}}
%% セクションタイトル下部のマージン(余白)
\newcommand{\Section@bottommargin}{\vspace{1\Cvs}}
%% セクションタイトルのフォント
\newcommand{\Section@font}{%
\headfont\Large% % フォントを大きめのゴシック体に変更
}
%% セクションタイトル
\newcommand{\Section@title}[2]{% #1: 番号, #2: タイトル
#1% % セクション番号を表示
\hspace{1zw}% % 全角1文字分の空白
#2% % タイトルを表示
}
ここで、\Section@title
の引数に(タイトルだけでなく)セクション番号も渡されていることに注意してください。
このおかげで、「セクション番号は \thesection
で参照する」というLaTeX知識を持っていなくても \Section@title
を上書きできます。
Step 4. タイトルデザインをカスタマイズ
ここまでできたら、タイトルデザインをカスタマイズしてみましょう。
%% 設定ファイルの値によってセクションデザインを変更
\def\@tempa{grayback}
\ifx\config@section@decoration\@tempa
\renewcommand{\Section@title}[2]{% #1: 番号, #2: タイトル
\textcolor{lightgray}{% % タイトルの背景を薄いグレーに
\rlap{\rule[-0.6zh]{\textwidth}{2zh}}% 本文幅
}%
\textcolor{black}{% % 章番号の背景色を黒に
\rlap{\rule[-0.6zh]{3.5zw}{2zh}}% 全角3.5文字分の幅
}%
\makebox[3.5zw][c]{\textcolor{white}{#1}}% 白抜き文字で番号を表示
\hspace{0.9zw}% % 空白
#2% % タイトルを表示
}
\fi
こんなデザインになります。

このようなデザインの変更をするのに、必要な箇所のマクロだけを上書きすればいいのが分かります。
繰り返しますが、従来の \section
の定義ではデザインを変更したければ \section
定義をコピペして変更、上下のマージン(余白)を変更するだけだとしても \section
定義をコピペして変更する必要がありました。
これに対し、テンプレートメソッドパターンを使えば \Section
マクロをより小さいマクロの集まりとして定義できます。
そのおかげで、必要な箇所のマクロだけを上書きすればいいので、\Section
全体を上書きする必要はありません。
説明しなかったこと
今回のサンプルコードでは、次のようなことは説明を省略しました。
-
\Section*
のように*
をつけたときは番号をつけない - 番号がつく場合とつかない場合でデザインやマージン(余白)を切り替える
-
\addcontentsline{toc}{見出し種類}{タイトル}
を使って目次を出力 -
\hyperref
の機能を使ってセクションへのアンカーを設定 -
\@hangfrom{章番号\hspace{1zw}}{タイトル}
を使って、長いタイトルが2行になるときの対策 - 各種ペナルティの設定(←今回は実装しなかった)
実際のスタイルファイル(後述するsty/starter-heading.sty
)ではこれらを考慮したので、ずっと複雑であり、マクロもより細かく分かれています。
しかし「テンプレートメソッドパターン」という考え方は変わりません。
実際のコード
ここで説明した方法は、「Re:VIEW Starter」というドキュメント生成ツールの中で使っています。
- Re:VIEW Starter
https://kauplan.org/reviewstarter/
興味ある人はここにアクセスして、プロジェクトを作成し、zipファイルを解凍してください。
その中の sty/starter-heading.sty
に実装コードが書かれてあります。
従来の \section
の問題点
従来の \section
には、カスタマイズしにくいこと以外にも問題があります。
問題点:「番号はつけないけど目次には出す」ができない
従来の \section
では、\section*
のように *
をつけると番号をつけなくなります。
それだけでなく、目次にも出さなくなります。
つまり、*
が2つの機能を持っています。
- 番号をつけない
- 目次に出さない
しかし、番号はつけないけど目次には出したいことは珍しくありません。
たとえば「まえがき」や「あとがき」では、チャプターやセクションには番号をつけません。
そこで \section*
を使うのですが、そうすると目次にも出なくなります。
(LaTeXでは \frontmatter
と \mainmatter
と \backmatter
を使うと、\chapter
だけは「まえがき」や「あとがき」のチャプターに番号がつきません。しかし \section
や \subsection
ではそのような手当はされていません。)
これは \section
の仕様が間違っています。
本当なら「番号をつける・つけない」と「目次に出す・出さない」を個別に指定できるべきでした。
解決策:第1引数が空文字列なら目次に出さない
ところで \section
には、タイトルとは別に「目次用のタイトル」を指定できます。
\section[目次用タイトル]{セクションタイトル}
通常は目次用タイトルを指定しないので、そのときは セクションタイトル
がそのまま目次用タイトルとして使われます。
この引数を利用すれば、目次用タイトルに空文字列を指定したら目次に出さないという仕様にできます。
使い方は次のようになります。
\Section{AAA} % 目次に「AAA」が出る
\Section[BBB]{AAA} % 目次に「BBB」が出る
\Section[]{AAA} % 目次に出ない!!!!!!!!!!
実装はこんな感じになるでしょう。
%% `\relax` は、他の言語でいうところのNULLみたいなもの。
%% ここでは第1引数 `#1` のデフォルト値を `\relax` (NULL) に指定している。
\newcommand{\Section}[2][\relax]{%
\def\@tempa{#1}%
\def\@tempb{\relax}%
\ifx\@tempa\@tempb% % 第1引数が指定されてなければ
#2を目次として使う
\else\ifx\@tempa\empty% % 第1引数が空文字列なら
目次に出さない
\else% % 第1引数が空文字列でなければ
#1を目次として使う
\fi\fi%
...
}
要は、こういうことです。
- 第1引数
#1
が未指定なら、第2引数#2
を目次に出す。 - 第1引数
#1
が空文字列なら、目次に出さない。 - 第1引数
#1
がそれ以外なら、第1引数を目次に出す。
これは *
の指定とは独立して行えます。
そのため、「番号をつける・つけない」と「目次に出す・出さない」を別個に指定できます。
前述の sty/starter-heading.sty
ではこの方法を採用しています。
問題点:一部を変更したいだけなのに全体のコピーが必要
\section
や \subsection
は実際にどのような定義になっているでしょうか。
クラスファイル jsbook.cls
から該当箇所を引用します。
\newcommand{\section}{%
\if@slide\clearpage\fi
\@startsection{section}{1}{\z@}%
{\Cvs \@plus.5\Cdp \@minus.2\Cdp}% 前アキ
{.5\Cvs \@plus.3\Cdp}% 後アキ
{\normalfont\Large\headfont\raggedright}}
\newcommand{\subsection}{\@startsection{subsection}{2}{\z@}%
{\Cvs \@plus.5\Cdp \@minus.2\Cdp}% 前アキ
{.5\Cvs \@plus.3\Cdp}% 後アキ
{\normalfont\large\headfont}}
\newcommand{\subsubsection}{\@startsection{subsubsection}{3}{\z@}%
{\Cvs \@plus.5\Cdp \@minus.2\Cdp}%
{\if@slide .5\Cvs \@plus.3\Cdp \else \z@ \fi}%
{\normalfont\normalsize\headfont}}
\newcommand{\paragraph}{\@startsection{paragraph}{4}{\z@}%
{0.5\Cvs \@plus.5\Cdp \@minus.2\Cdp}%
{\if@slide .5\Cvs \@plus.3\Cdp \else -1zw\fi}% 改行せず 1zw のアキ
{\normalfont\normalsize\headfont\jsParagraphMark}}
\newcommand{\subparagraph}{\@startsection{subparagraph}{5}{\z@}%
{\z@}{\if@slide .5\Cvs \@plus.3\Cdp \else -1zw\fi}%
{\normalfont\normalsize\headfont}}
こうして見ると、\section
から \subparagraph
までが \@startsection
という1つのマクロに共通化されていることが分かります(これは book.cls
でも同様です)。
おかげでそれぞれの定義はわずか数行で済んでいます。
おそらくですが、数行しかないんだから \section
を変えたいときはまるごとコピーして変更すればいいという設計思想なのでしょう。
しかしこれはプログラミング的には悪手です。
たとえばマージンの高さを変更したい場合であっても、全体をコピーして上書きする必要があるので、もしオリジナルのほうがフォントを変更した場合、その変更はコピーには反映されません。
オリジナルの定義が一切変わらないという保証がない限り、このようは方法は推奨されません。
(今のところ、book.cls
や jsbook.cls
では変更がされてないから問題になってないのでしょう。)
問題点:個別にカスタマイズしたいのにコードを共通化している
先ほど説明したように、クラスファイル book.cls
や jsbook.cls
では見出し定義の \section
から \subparagraph
までを1つのマクロ(\@startsection
)で共通化しています。
そのせいで、\@startsection
を変更するとそれが(\chapter
を除く)すべての見出しレベルに影響します。
すべての見出しレベルを同じようなデザインにしたいならこの方法でもいいのですが、実際には「\section
と \subdsection
と \subsubsection
はデザインを大きく変えたい」という要望が多いです。
その場合、1つのマクロで共通化していることはマイナス要素でしかありません。
個別に変更やカスタマイズが必要なものを下手に共通化すると、変更やカスタマイズはとてもしにくくなります。
\@startsection
はまさにその例です。
(なお \@seccntformat
というマクロをうま〜く再定義すると、番号のデザインは変更できます。うま〜く定義すればね。)
問題点:性質が大きく異る \section
と \paragraph
を同じマクロで定義している
\@startsection
は、\chapter
を除いた次の見出し定義で使われています。
\section
\subsection
\subsubsection
\paragraph
\subparagraph
しかし上3つはいいとして、下2つ(\paragraph
と \subparagraph
)は見出しとしての性質が大きく違います。
この2つには番号はつかないし、目次にも出しません。
見出しの一種とはいえ、\paragraph
や \subparagraph
を \section
などと同じように定義する必要はありません。
また \@startsection
は次のような奇妙な実装になっています。
- 第4引数がマイナスかどうかでインデントの挙動を変える。
- 第5引数がマイナスかどうかでタイトル後の改行する・しないを変える。
なぜこんなことになっているのか、マクロ定義を読んだときは意味が分からなかったのですが、どうやら \@startsection
が \paragraph
と \subparagraph
をサポートするためにこんな奇妙な仕様になっているようです(あくまで推測です)。
どう考えても、\@startsection
マクロが \paragraph
の定義までサポートするのはやめて、\paragraph
と \subparagraph
は個別に定義したほうがシンプルになったことでしょう。
性質の異なるものを共通化したせいで、共通化部分が複雑になった失敗例です。
(どちらかというと、\chapter
を \@startsection
で定義できるようにすべきでした。)
前述の sty/starter-heading.sty
では、\paragraph
と \subparagraph
は \section
とは関係なく定義しているので、定義がとても簡潔になりました。
問題点:ページ先頭でもセクションタイトルの上部にマージンが入る
jsbook.cls
を使っていると、たとえページ先頭であってもセクションタイトルの上にマージン(余白)が入ります(book.cls
なら入りません)。
jsbook.cls
の中身を見ると、次のような箇所があります。
1134: \addpenalty\@secpenalty
1135: \ifdim \@tempskipa >\z@
1136: \if@slide\else
1137: \null
1138: \vspace*{-\baselineskip}%
1139: \fi
1140: \vskip\@tempskipa
1141: \fi
1140行目では \vskip
を使っているので、一見するとページ先頭ならマージンは入らないように見えます。
しかし1137行目と1138行目のせいで、ページ先頭のはずが余計な文字が入るせいでページ先頭とみなされなくなってしまうのです。
(jsbook.cls
がなぜこんな余計な仕様になっているかは分かりませんでした。
また1136行目に \if@slide
があるので、\@slidetrue
を使えば回避できますが、\if@slide
が使われているのはここだけではないので、他の箇所に影響が出ます。)
この問題を回避するために、たとえば \section
定義を次のようにしてみましょう。
\renewcommand{\section}{%
\@startsection{section}{1}{\z@}%
{0pt}% % 上マージンをなくす
{.5\Cvs \@plus.3\Cdp}% % 下マージンは残す
{%
\vskip \Cvs \@plus.5\Cdp \@minus.2\Cdp\relax% 上マージンを入れる
\normalfont\Large\headfont\raggedright%
}%
}
一見するとこれでうまくいきそうですが、実は hyperref
パッケージを読み込むと、やはりページ先頭であっても上マージンが入ります(hyperref
を使わなければこんな余計なマージンは入りません)。
これは hyperref
が \refspepcouter
を上書きしているせいです。
\refspepcouter
はチャプターやセクションの番号を増やすマクロですが、hyperref
はこれを上書きして、番号を増やすだけでなくハイパーリンク用のアンカーもつけてしまいます。
このアンカーはもちろん目に見えませんが、LaTeXではテキスト要素とみなされてしまうので、ページ先頭であってもページ先頭だとはみなされなくなり、\vspace
によるマージンが入ってしまいます。
前述の sty/starter-heading.sty
ではこの問題に対処するために、上書きされた \refstepcounter
を使わず、上書きされていない \refstepcounter
を使いました。
またハイパーリンク用のアンカーは hyperref
パッケージのマクロを使って明示的につけました。
これでようやく、ページ先頭では余計なマージンが入らないようになりました。
(もっと簡単な方法としては、\refstepcounter
を呼び出す前に上マージンを入れる方法もあります。今回はマクロ定義の都合でできませんでした。)
問題点:いろんなパッケージが \section
の実装に依存している
先ほど説明したように、hyperref
パッケージはハイパーリンクを実現するために、\section
の内部で使われている \refstepcounter
マクロを上書きします。
つまり hyperref
パッケージは \section
の実装に強く依存しており、密結合になっています。
そのせいで、独自に \section
マクロを定義すると hyperref
パッケージの目次機能は機能しません(当然です)。
そのため、独自に \section
マクロを定義するときは自力で hyperref
の機能を呼び出して同じような機能を実現する必要があります。
これはうまい解決策はなさそうです。
ただし \section
マクロが部分的にカスタマイズしやすくなっていれば、密結合の強度を弱めることはできたはずです。
まとめ
LaTeXの \section
マクロを、テンプレートメソッドパターンを使って実現する方法を紹介しました。
この方法を使うと、必要な箇所のマクロだけを上書きすればいいので、\section
のカスタマイズ性が格段に向上します。
また \section
の仕様そのものにも問題があることを説明しました。
特に「番号をつける・つけない」と「目次に出す・出さない」を個別に指定できないのは、仕様の欠陥です。
プログラミングの観点でみると、LaTeXはTeX言語だけでなく、クラスファイルやスタイルファイルの定義にも問題が多いと感じます。
いくらTeXやLaTeXが古いからとはいえ、プログラミングの定石から外れたコード(マクロ定義)が多すぎるように見えます。
\section
の定義はそれが顕著で、コードを共通化したいという気持ちは分かるものの、やり方がよくなくて、「タイトル上下のマージンの高さを変えたい」「フォントの大きさを変更したい」という程度の変更ですら「\section
定義の全体をコピーして必要な箇所を書き換える」ことが必要です(Open-Closed Pricipleに反してますよね)。
また \section
と \paragraph
の定義は共通化する必要がないのに共通化してるし、逆に共通化できるはずの \section
と \chapter
はしていないという、とてもちぐはぐな設計です。
LaTeXのパワーユーザーで『リファクタリング』を読んだ人って、実は少ない(というかほぼいない?)のではないでしょうか。
せめて『リーダブルコード』ぐらいは読んで、もうちょっとコード品質を気にかけてほしいです1。
そうすれば「LaTeXに詳しくないと読めないコード」が減って、LaTeXに詳しくなくてもカスタマイズができるのに、と思います。
逆に言えば、クラスファイルやスタイルファイルの品質(LaTeX的な意味ではなくプログラミング的な意味での品質)を向上すれば、LaTeXはもっと使いやすくなるはずだと期待しています。
TeX言語の仕様は変えられないけど、クラスファイルやスタイルファイルは変えられるので。
おまけ
余計なお世話でしょうけど、jlreq.clsが200行を超えるようなマクロ定義を平気でしてるの、やめたほうがいいと思いますよ?(プログラミング的な意味で)
-
もし読んでいてあのコード品質なら、読んでないのと同じ。 ↩