我想让我的生活自动化一点,发现该enumitem
包提供了一种创建自定义列表的方法,可以根据前一个列表恢复计数。如果没有这个,我必须使用明确操作计数器,enumi
这\setcounter
可能会变得很痛苦,因为很容易无法正确计数。:-) 无论如何,enumitem
使用时,连续计数有效resume*
。但是,这样做会破坏 cleverref 的引用!引用该列表中的项目时,resume,,
会打印额外的文本。我该如何摆脱它?我在序言中做错了什么,或者这是一种错误?
\documentclass[a4paper,english]{article}
\usepackage{enumitem}
\usepackage{amssymb}
\usepackage{cleveref}
% defining theorem environments for their specialized versions
\makeatletter
\newenvironment{base@thm}[3]%
{$\;$\linebreak\noindent\mbox{$\triangleright\;\textsc{\textbf{\large #1} #2 (#3).}$}}
{\hfill$\;$\linebreak}
\makeatother
% lemma environments take a label and a name
\newcounter{lemma}
\crefname{lemma}{lemma}{lemmata}
\makeatletter
\newenvironment{lemma}[2]%
{\refstepcounter{lemma}\begin{base@thm}{Lemma}{\thelemma}{#2}\label[lemma]{lemma:#1}\def\@currentlabel{#2}\label{t@lemma:#1}}
{\end{base@thm}\noindent}
\makeatother
% referencing a lemma
\newcommand{\lemmaref}[1]{\Cref{lemma:#1}~(\ref{t@lemma:#1})}
\newcommand{\lemref}[1]{\Cref{lemma:#1}}
\newlist{passumptionslist}{enumerate}{1}
\setlist[passumptionslist]{label=(\alph*)}
\crefalias{asm}{passumptionslisti}
\crefalias{goal}{enumi}
\crefname{asm}{assumption}{assumptions}
\crefname{goal}{goal}{goals}
\newenvironment{assumptions}
{\begin{passumptionslist}[label=(\alph*)]
}
{
\end{passumptionslist}
}
\newenvironment{goals}
{\begin{enumerate}[label=(\roman*)]
}
{
\end{enumerate}
}
\newcommand{\IH}[1][]{I{\kern-1.5pt}H#1}
\newenvironment{passumptions}[1]
{%
\begin{passumptionslist}[resume*,label={$\left(#1_{\arabic*}\right)$}]%
}
{%
\end{passumptionslist}%
}
% individual cases
\newcommand{\asm}[2]{\item\label[asm]{asm:#1} {$#2$}}
\newcommand{\goal}[2]{\item\label[goal]{goal:#1} {$#2$}}
\newcommand{\asmref}[1]{\Cref{asm:#1}}
\newcommand{\goalref}[1]{\Cref{goal:#1}}
\newenvironment{proof}[1][\textbf{Proof}]%
{\restartlist{passumptionslist}\par\noindent\ignorespaces\mbox{\textsc{#1}.}$\;$\\\noindent}
{$\;$\hfill$\square$\\\ignorespacesafterend}
\begin{document}
\begin{lemma}{lemma}{Some Lemma Name}
If
\begin{assumptions}
\asm{lemma}{a}
\end{assumptions}
then
\begin{goals}
\goal{lemma}{b}
\end{goals}
\end{lemma}
\begin{proof}
Unfold \goalref{lemma} and introduce the assumptions.
\begin{passumptions}{H}
\asm{lemma0}{v\Downarrow b}
\end{passumptions}
What is left to show is:
\begin{goals}
\goal{lemma0}{c}
\end{goals}
From \asmref{lemma0}, get:
\begin{passumptions}{H}
\asm{lemma1}{a=v}
\end{passumptions}
Rewrite \asmref{lemma} and \asmref{lemma0} using \asmref{lemma1}:
\begin{passumptions}{H}
\asm{lemmap}{v=c}
\asm{lemma0p}{a\Downarrow b}
\end{passumptions}
Use \asmref{lemma0p}:
\begin{passumptions}{H}
\asm{lemma2}{a=b}
\end{passumptions}
Finally, rewrite \goalref{lemma0} via \asmref{lemma1}.
Then, \asmref{lemma2} solves it.
\end{proof}
\end{document}
答案1
这个答案解释了为什么resume,,
要打印。
从根本上来说,有两件事是相互冲突的。
[resume*]
如果没有先例,名单就会失效
您可以通过以下简单的 MWE 看到这种行为:
\documentclass{article}
\usepackage{enumitem}
\begin{document}
\begin{enumerate}[resume*]
\item Test
\end{enumerate}
\end{document}
为什么会这样?要启用resume*
,enumitem
代码必须将列表配置从上次发布列表传递到下次调用列表。它通过将\enit@resumekeys@<list>
(其中<list>
替换为列表名称,即enumerate
或passumptionslist
)设置为(本质上)作为列表的可选参数传递的内容来实现enumitem
。这将在列表末尾执行,因此当\end{enumerate}
在上述示例中遇到时。并且不创建了默认值;因此在第一次调用列表之前,宏\enit@resumekeys@<list>
是不明确的。
现在,当resume*
被传递给列表时,enumitem
会尝试从列表的最后一次调用中加载配置,因此它会查看\enit@resumekeys@<list>
。值得注意的是,它使用\csname ... \endcsname
构造来实现这一点,因此当\enit@resumekeys@<list>
未定义时,这不会出错(出现未定义的宏错误)。当您使用\csname ... \endcsname
访问未定义的宏时,它总是返回\relax
。
为了允许当前调用中的键覆盖先前指定的键,enumitem
添加将此转换为可选参数。因此,resume*
在 MWE 中处理之后,有效的可选参数变为\relax,resume
。
但是现在,要处理选项列表,enumitem
需要一次处理一个逗号分隔的列表。它使用标准技巧来实现这一点。它使用以下宏定义
\def\enitkv@do#1,{%
\ifx\relax#1\empty\else
\enitkv@split#1==\relax %% This processes the key, ignore it for now
\expandafter\enitkv@do\fi}
宏以逗号分隔。因此,当输入一个以逗号分隔的列表时,它将吃掉下一串字符,直到下一个,
;它将处理该字符串,丢弃它(和逗号),然后对下一个参数再次执行相同的操作。
这个宏怎么知道该怎么停止呢?它遇到 时就停止\relax
。实际上,这个宏是通过
\enitkv@do#2,\relax,
其中#2
是可选参数列表。现在你看到问题了,当你使用resume*
没有先行词时上述宏在调用时会处理
\enitkv@do\relax,resume,\relax,
当第一个\relax
命中时,宏终止。所以我们剩下的是一个悬空的
resume,\relax,
由于这些不是任何宏的参数,因此它们将作为文本处理。并且作为\relax
无操作,打印时会变成
resume,,
你所看到的。
群组
但是你说,等一下,你还assumptions
根据 定义了环境passumptionslist
,并且你称它为前在您的代码中调用第一个passumptions
。这是否意味着实际上应该有一个先行词?
您应该将以下两个 MWE 与之进行比较。
MWE 1(无团体)
\documentclass{article}
\usepackage{enumitem}
\begin{document}
\begin{enumerate}
\item Test
\end{enumerate}
\begin{enumerate}[resume*]
\item Test
\end{enumerate}
\end{document}
MWE 2 (与团体)
\documentclass{article}
\usepackage{enumitem}
\begin{document}
\bgroup
\begin{enumerate}
\item Test
\end{enumerate}
\egroup
\begin{enumerate}[resume*]
\item Test
\end{enumerate}
\end{document}
MWE 1 可以正常运行,但 MWE 2 则会出现异常。那么这里到底发生了什么?
以下是 中的一项功能enumitem
,尽管我不确定这是否是个好主意。在列表中保存当前选项时,enumitem
在四种不同情况下的行为有所不同。
- 当您有一个
series
选项(不是resume
或 )时resume*
,代码将使用 保存当前计数器值和当前可选参数\global\def
。 - 当您有一个
series
选项也是resume
或 时resume*
,代码仅通过 保存计数器\global\def
,但不会保存当前可选参数。 - 当您没有
series
但有 时resume*
,代码仅通过 保存计数器\global\def
,但不会保存可选参数。 series
当您有一个既不是也不是的普通列表时resume*
(但奇怪的是,resume
这里没问题),代码会使用 保存计数器和可选参数\def
。
这会导致几个有趣的副作用。
- 如果您将 包裹
\begin{enumerate}[resume]...\end{enumerate}
起来\newenvironment
,则会触发第四种情况。但由于环境是分组的,因此\def
不会传播出环境。这解释了为什么编号是错误的,正如您在此评论。 - 使用您的原始代码作为参考,当您包装在通过定义的环境
\begin{passumptionslist}...\end{passumptionslist}
中时,当您调用时,包会尝试保存当前的计数器和列表选项,但正如我们处于情况 4 中一样,这些都是通过本地定义制作的,所以一旦我们击中这些就会丢失。assumptions
\newenvironment
\begin{assumptions}...\end{assumptions}
\end{passumptionslist}
\end{assumptions}
上面发现的第一个问题有点奇怪,即 的正常使用enumitem
不应该调用[resume*]
之前未使用过的列表。也就是说,只需使用不同的标记来划分 中的列表末尾,即可修复此边缘情况\def\enitkv@do#1,
。
第二个问题实际上有记录(尽管记录得有些差)。在当前版本的 enumitem 文档中,resume
第 3.3 节中顺便提到了 local。尽管这种奇怪的交互resume*
没有明确记录(但绝对应该记录)。
答案2
我认为没有必要使用passumptionslist
包装在各种环境中的底层。最好将所有内容都定义为\newlist
s。
%%%% UNTIL ANNOUNCED, SAME AS YOUR CODE %%%%
\documentclass[a4paper,english]{article}
\usepackage{enumitem}
\usepackage{amssymb}
\usepackage{cleveref}
% defining theorem environments for their specialized versions
\makeatletter
\newenvironment{base@thm}[3]%
{$\;$\linebreak\noindent\mbox{$\triangleright\;\textsc{\textbf{\large #1} #2 (#3).}$}}
{\hfill$\;$\linebreak}
\makeatother
% lemma environments take a label and a name
\newcounter{lemma}
\crefname{lemma}{lemma}{lemmata}
\makeatletter
\newenvironment{lemma}[2]%
{\refstepcounter{lemma}\begin{base@thm}{Lemma}{\thelemma}{#2}\label[lemma]{lemma:#1}\def\@currentlabel{#2}\label{t@lemma:#1}}
{\end{base@thm}\noindent}
\makeatother
% referencing a lemma
\newcommand{\lemmaref}[1]{\Cref{lemma:#1}~(\ref{t@lemma:#1})}
\newcommand{\lemref}[1]{\Cref{lemma:#1}}
%%%% CHANGES BELOW %%%%
% 1. Removed passumptslist
% 2. Removed your crefaliases
% The crefname are kept
\crefname{asm}{assumption}{assumptions}
\crefname{goal}{goal}{goals}
% 3. Defined assumptions, passumptions, and goals directly as new lists
\newlist{assumptions}{enumerate}{1}
\newlist{passumptions}{enumerate}{1}
\newlist{goals}{enumerate}{1}
% 3. (cont'd) and set their formatting directly
\setlist[assumptions]{label=(\alph*)}
\setlist[goals]{label=(\roman*)}
% 4. Set the format for passumptions; define macro to change the label.
\makeatletter
\newcommand*\setPassLabel[1]{\gdef\@passLabel{#1}}
\setlist[passumptions]{resume,label={$(\@passLabel_{\arabic*})$}}
\makeatother
%%%%% BELOW AGAIN UNCHANGED, EXCEPT MARKED LINES %%%%%%%
\newcommand{\IH}[1][]{I{\kern-1.5pt}H#1}
% individual cases
\newcommand{\asm}[2]{\item\label[asm]{asm:#1} {$#2$}}
\newcommand{\goal}[2]{\item\label[goal]{goal:#1} {$#2$}}
\newcommand{\asmref}[1]{\Cref{asm:#1}}
\newcommand{\goalref}[1]{\Cref{goal:#1}}
%%%% Make the list that is restarted the passumptions list, and not passumptionslist.
\newenvironment{proof}[1][\textbf{Proof}]%
{\restartlist{passumptions}\par\noindent\ignorespaces\mbox{\textsc{#1}.}$\;$\\\noindent}
{$\;$\hfill$\square$\\\ignorespacesafterend}
\begin{document}
\begin{lemma}{lemma}{Some Lemma Name}
If
\begin{assumptions}
\asm{lemma}{a}
\end{assumptions}
then
\begin{goals}
\goal{lemma}{b}
\end{goals}
\end{lemma}
\begin{proof}
\setPassLabel{H} %%% Set the label for the passumptions.
Unfold \goalref{lemma} and introduce the assumptions.
\begin{passumptions} %%% No more mandatory argument (Ditto below)
\asm{lemma0}{v\Downarrow b}
\end{passumptions}
What is left to show is:
\begin{goals}
\goal{lemma0}{c}
\end{goals}
From \asmref{lemma0}, get:
\begin{passumptions}
\asm{lemma1}{a=v}
\end{passumptions}
Rewrite \asmref{lemma} and \asmref{lemma0} using \asmref{lemma1}:
\begin{passumptions}
\asm{lemmap}{v=c}
\asm{lemma0p}{a\Downarrow b}
\end{passumptions}
Use \Cref{asm:lemma0p}; \asmref{lemma0p}:
\begin{passumptions}
\asm{lemma2}{a=b}
\end{passumptions}
Finally, rewrite \goalref{lemma0} via \asmref{lemma1}.
Then, \asmref{lemma2} solves it.
\end{proof}
\begin{proof}
\setPassLabel{K}
\begin{passumptions}
\asm{lemma3}{c}
\end{passumptions}
\end{proof}
\end{document}
解释
我认为最好将所有列表环境直接定义为
\newlists
,而不是将它们\newenvironments
包装为列表passumptionslist
。原因是这样您就可以一致地使用接口enumitem
通过可选参数动态修改列表,也可以全局使用\setlist
,从而增加了使用它们的灵活性。据我所知,这
\crefalias
对您的代码没有影响;所以我删除了它们,特别是因为已知它可能存在问题。这样做的好处是,无论何时使用和,hyperref
您都会使用可选参数,因此能够从可选参数中获取类型信息。\label
\asm
\goal
\Cref
我碰巧认为使用
\newlists
和\setlist
使得格式的配置更加透明。这是我们必须完成的唯一部分。在您的原始代码中,您定义了
passumptions
一个强制参数,用于设置label
底层列表的。我不同意这种设置:首先,从您的代码中可以看出passumptions
“每个证明”都使用,因此理想情况下,passumptions
同一证明中的连续项应具有相同的标签类型。(我认为如果项目的编号为 H1、H2,然后是 C3、D4、H5,看起来会很奇怪。)考虑到这一点,最好有一种全局更改标签的方法(对于所有后续passumptions
列表)。顺便说一句,这样做还可以将passumptions
其编码为\newlist
,因为以这种方式创建的列表不能采用强制参数。基于这种设计理念,我们创建了一个命令来设置 的标签符号/字符
passumptions
,并使用该符号来配置此列表的格式。我还添加了resume
键,因为此列表旨在恢复,直到每次证明开始时重置。