\mathrm{sin} 和 \sin 印在纸上看起来完全是同一个 “sin”。可前者依然是错的,差别是 3.33325pt。在 LaTeX 中测量 $a\operatorname{op}b$ 得到 23.46638pt,$a\mathrm{op}b$ 则是 20.13313pt——直立的字形一模一样,但 \mathrm 前后不带任何空白。这 3.33325pt 不是装饰,而恰好是两个细空白,也就是 TeX 在 Op 原子(atom) 左右各放的一个。本页从这个原子讲起,接着定位内置的 32 个函数名写在 LaTeX 源码的哪里,解释为何恰好有 10 个把下标放在正下方,说明 \DeclareMathOperator 的星号改变了什么,并用实测宽度区分 \bmod、\pmod、\mod、\pod 这四种模运算写法。
为什么 \mathrm{sin} 是错的:原子类别决定间距
在数学模式中,每个英文字母都会被当作变量名并以数学斜体排版,所以输入 sin x 得到的不是“正弦”,而是 s、i、n、x 四个量的乘积。这一点广为人知,很多人想到的补救办法是 \mathrm{sin}——因为字形变直立后,问题看起来就解决了。但 \mathrm 改变的 只有字体。在 TeX 看来,原子类别仍然是 Ord,也就是普通符号。
TeX 把公式的各个成分分成 原子类别——Ord(普通)、Op(算子)、Bin(二元运算符)、Rel(关系符)等——并且完全根据相邻的是哪两类来决定间距。Ord 与 Ord 之间没有空白;Op 与 Ord 之间有一个 细空白(\thinmuskip,3mu)。\sin 是 Op,\mathrm{sin} 是 Ord,差别由此而来。在 10pt 正文中测量下面四项,这套机制会直接化为数字。
\DeclareMathOperator{\myop}{op}
% \sbox0{$...$}\message{\the\wd0}
$a\operatorname{op}b$ 23.46638pt
$a\myop b$ 23.46638pt % identical to \operatorname
$a\mathop{\mathrm{op}}b$ 23.46638pt % same font as \mathrm, but Op class
$a\mathrm{op}b$ 20.13313pt % 3.33325pt narrower
$\mkern3mu$ 1.66663pt % one thin space; two of them = 3.33326pt第三行是决定性的。\mathop{\mathrm{op}} 使用的字体与 \mathrm 完全相同,宽度却与 \operatorname 精确到十万分之一磅一致。可见造成差别的不是字体,而是 \mathop 赋予的 Op 类别。而 3.33325pt 的差恰好是两个 1.66663pt 的细空白——a 与 “op” 之间一个,“op” 与 b 之间一个。“\mathrm 会让间距出错”这句流传已久的说法,至此精确到小数点后每一位都得到了解释。实用结论很简单:函数名一律用专用命令或 \operatorname 书写,绝不用 \mathrm。
内置的 32 个函数名,以及它们写在哪里
\log 和 \sin 的定义并不在某个宏包里,而在 LaTeX 本身的格式文件 latex.ltx 中。在 TeX Live 2024 里,它们占据第 12487 行到第 12518 行——正好 32 行——每行形如 \DeclareRobustCommand\log{\mathop{\operator@font log}\nolimits}。这份清单还要更古老:几乎同样的一组就在高德纳的 plain.tex 第 1058–1085 行。所以能写 \sin 并不是 amsmath 的功劳,它从一开始就可用,无需任何宏包(只有自定义算子时才需要 amsmath,见下文)。
- 三角函数:
\sin\cos\tan\cot\sec\csc,以及反函数\arcsin\arccos\arctan。 - 双曲函数:
\sinh\cosh\tanh\coth——注意\sech与\csch并不包含在内,需要时请自行声明。 - 对数与指数:
\log(一般)、\ln(自然对数)、\lg(以 2 为底,信息论常用)、\exp。 - 代数、几何等:
\deg(次数)、\dim(维数)、\ker(核)、\hom、\arg(幅角)、\det(行列式)。 - 把标记放在正下方的 10 个:
\lim\limsup\liminf\max\min\sup\inf\det\Pr\gcd。
底数和指数直接使用普通的上下标机制:\log_2 x 把 2 放在 “log” 右下,\sin^2\theta 把 2 放在右上,得到惯用的 sin²θ。有一个不读源码就不会注意的小细节:\limsup 的定义是 \mathop{\operator@font lim\,sup}——“lim” 与 “sup” 之间手工放了一个 \, 细空白。它不是一个连写的词,而是两个被刻意稍稍分开的词。
\[
\sin^2\theta + \cos^2\theta = 1, \qquad
\log_2 8 = 3, \qquad \ln e = 1.
\]把标记放在正下方的 10 个,以及为何 \limits 无效
32 个当中有 22 个在定义末尾带 \nolimits,因此标记出现在右下方。其余 10 个——\lim \limsup \liminf \max \min \sup \inf \det \Pr \gcd——没有写 \nolimits。由于 \mathop 在陈列样式下的默认行为是“标记放在正下方”,所以正是这 10 个会在陈列公式中把 x→0 放在 \lim 之下。极限和最大值应当醒目地显示“在什么范围上”,这条数学惯例被直接写进了定义。在文本样式(行内)下,这 10 个也退回到右下方。
\[
\lim_{x \to 0} \frac{\sin x}{x} = 1, \qquad
\max_{1 \le i \le n} a_i .
\]这时很自然会想:既然 \limits 对 \sum、\int 有效,把它用在函数名上不就行了。可是 在加载 amsmath 的情况下,\log\limits_{k} 什么也不会发生——没有报错,没有警告,被静静忽略。原因在 amsopn.sty 第 27 行:\def\nolimits@{\@ifnextchar\limits{\nolimits\@gobble}{\nolimits}}。函数名的定义在末尾调用这个 \nolimits@,而 如果下一个记号是 \limits,它就先输出 \nolimits,再把那个 \limits 吃掉。实测也可佐证。
% depth of the box tells us where the script went (bigger = below the operator)
% WITHOUT amsmath — \log is \mathop{...}\nolimits, and \limits overrides it
$\displaystyle\log_{k}x$ d = 2.44443pt
$\displaystyle\log\limits_{k}x$ d = 9.47220pt % moved below
% WITH amsmath — the \limits is swallowed by \nolimits@
$\displaystyle\log_{k}x$ d = 2.44443pt
$\displaystyle\log\limits_{k}x$ d = 2.44443pt % unchanged: nothing happened也就是说,\limits 的一般规则(可用于 \sum 等大型算子,详见“求和、积分与大型算子”)并不适用于 amsmath 下的函数名。反方向的 \nolimits 则能顺利通过,所以 \lim\nolimits_{k} 确实可以把 \lim 的标记赶到右下方。若想把标记放到正下方,正确答案不是 \limits,而是 下一节的带星声明。
\DeclareMathOperator 与 \operatorname:星号决定什么
当你要排一个不在清单里的名字——符号函数 sgn、迹 tr、秩 rank、ess sup、argmax——就轮到 amsmath 出场了。临时使用写 \operatorname{sgn} x;反复使用则在导言区 声明:\DeclareMathOperator{\sgn}{sgn}。正如本页开头的实测所示,声明得到的 \sgn 与 \operatorname{sgn} 宽度完全一致——它们调用同一个 \qopname,字面上就是同一样东西。声明的好处不在质量,而在于 集中于一处:以后想改记号,只需改导言区的一行。
\usepackage{amsmath}
\DeclareMathOperator{\sgn}{sgn} % scripts to the right, like \log
\DeclareMathOperator*{\argmax}{arg\,max} % scripts underneath, like \lim
% in the body:
% \[ \sgn x, \qquad \argmax_{x \in S} f(x) \]星号决定的只有标记的位置。 不带星号得到的是 \log 一族(右下);带星号得到的是 \lim 一族(在陈列公式中位于正下方)。临时使用时,\operatorname*{…} 效果相同。这一点同样可以量出来:把同一个 op 排在陈列公式中并加上 _{n\to\infty},盒子的深度在无星号时为 2.44443pt,有星号时为 8.94444pt。多出来的深度,正是垂在符号下方的那个标记。名字里夹的 \,(arg\,max)与 \limsup 的定义出于同一理由:在两个词之间留出恰当的空白。
amsmath 还补充了上下极限一带的词汇。\varlimsup 与 \varliminf 是在 “lim” 上方(下方)加一条横线 的变体,比 \limsup 的 “lim sup” 拼法更具符号性。\injlim 与 \projlim 把正极限与逆极限排成 “inj lim”“proj lim”;\varinjlim 与 \varprojlim 则给出范畴论中常见的形态,在 “lim” 下方铺一支 →(或 ←)。它们和带星声明一样,标记都放在正下方。选哪一个应遵循所在领域的惯例;拿不准时,先看看投稿目标的样式文件是否已经定义过。
\bmod \pmod \mod \pod:用实测区分四种模写法
四种模写法之所以让人犹豫,是因为它们都打印出 “mod”,区别只在空白与括号。\bmod 和 \pmod 属于标准 LaTeX,\mod 和 \pod 来自 amsmath。设计上的差异从定义一眼可见:只有 \bmod 是用 \mathbin{…} 定义的——也就是作为 Bin 原子,与 +、- 同一类别——其余三个只是先插入一段空白再排出内容,相当于一条后置的注记。
\DeclareRobustCommand{\bmod}{\nonscript\mskip-\medmuskip\mkern5mu\mathbin
{\operator@font mod}\penalty900
\mkern5mu\nonscript\mskip-\medmuskip}
\DeclareRobustCommand{\pod}[1]{\allowbreak
\if@display\mkern18mu\else\mkern8mu\fi(#1)}
\DeclareRobustCommand{\pmod}[1]{\pod{{\operator@font mod}\mkern6mu#1}}
\DeclareRobustCommand{\mod}[1]{\allowbreak\if@display\mkern18mu
\else\mkern12mu\fi{\operator@font mod}\,\,#1}请注意 \if@display。\pod 与 \pmod 在 陈列公式中放 18mu,行内放 8mu;\mod 则是 18mu 与 12mu。\bmod 没有这个分支——作为二元运算符,它只是左右对称地各放一个 \mkern5mu——因此它是四者中 唯一在陈列与行内宽度相同的一个。用 10pt 测量同一个式子 a … n,设计就直接化为数字。
| 命令 | 输出 | 宽度 行内 → 陈列 | 使用场合 |
|---|---|---|---|
a \bmod n | a mod n | 36.01039pt → 36.01039pt(不变) | 当你要写作为 值 的余数时。属 Bin 原子,故左右对称。标准 LaTeX |
a \pmod{n} | a (mod n) | 46.01036pt → 51.56578pt | 同余式 x \equiv y \pmod{n} 末尾的常规写法。标准 LaTeX |
a \mod{n} | a mod n | 40.45473pt → 43.78798pt | 用于不喜欢括号的同余写法;前置空白比 \bmod 更宽。需要 amsmath |
a \pod{n} | a (n) | 23.51038pt → 29.06580pt | 省去 “mod” 只保留括号,适用于模数在上下文中显而易见时。需要 amsmath |
把行内宽度从陈列宽度中减去,定义就精确地重现出来。\pmod 与 \pod 的差是 5.55542pt——即 18mu − 8mu = 10mu,在 10pt 下为 9.99976 − 4.44434 = 5.55542pt。\mod 的差是 3.33325pt,对应 18mu − 12mu = 6mu。\bmod 的差为 0。二者的取舍取决于含义。若你说的是 作为值的余数,用 \bmod(5 \bmod 3 = 2);若你断言 两数在模 n 下同余,用 \pmod(17 \equiv 5 \pmod{12})。把这两者弄混,读者就无法分辨你说的是“值”还是“关系”。
\[
5 \bmod 3 = 2, \qquad
17 \equiv 5 \pmod{12}.
\]