4

We all know how \newtheorem works. It takes a third optional argument and it matters where you put it:

\newtheorem{lem}[thm]{Lemma}

is different to

\newtheorem{lem}{Lemma}[thm]

I'm getting into macros and would like to replicate it in my own way. I understand optional arguments for LaTeX and have read this TeX article https://www.tug.org/TUGboat/tb22-1-2/tb70eijk.pdf but I found it overwhelming and wondered:

Exactly how was \newtheorem defined?

As I understand \newcommand I don't know how this position dependence could be done from LaTeX. I know I can solve this problem with xparse etc. I am just curious exactly how the maker(s) did this without xparse.

5

The default approach for \newcommand is to have the optional argument as the first element

\mycmd[<opt>]{<arg1>}...{<argn>}

However, one can chain \newcommands in a way that makes placement of the optional argument in any way you want:

\newcommand{\mycmd}{\firstcmd}
\newcommand{\firstcmd}[1]{first #1\space \secondcmd}
\newcommand{\secondcmd}[2][opt]{second (opt: #1) #2}

%...

\mycmd{1st}[OPT]{2nd}

enter image description here

In the above, \mycmd just provides the "user interface" to \firstcmd which takes a single mandatory argument. At the end of \firstcmd, we chain \secondcmd which requires 2 arguments, the first of which is optional. Since the two are chained (\secondcmd is called at the end of \firstcmd), their arguments can be chained. To that end, it seems from a user perspective that \mycmd takes 3 arguments, with the middle one being optional.

You'll note this string of commands defined and chained within ltthm.dtx or latex.ltx (the LaTeX kernel) when searching for \newtheorem:

%%% From File: ltthm.dtx
\def\newtheorem#1{%
  \@ifnextchar[{\@othm{#1}}{\@nthm{#1}}}
\def\@nthm#1#2{%
  \@ifnextchar[{\@xnthm{#1}{#2}}{\@ynthm{#1}{#2}}}
\def\@xnthm#1#2[#3]{%
  \expandafter\@ifdefinable\csname #1\endcsname
    {\@definecounter{#1}\@newctr{#1}[#3]%
     \expandafter\xdef\csname the#1\endcsname{%
       \expandafter\noexpand\csname the#3\endcsname \@thmcountersep
          \@thmcounter{#1}}%
     \global\@namedef{#1}{\@thm{#1}{#2}}%
     \global\@namedef{end#1}{\@endtheorem}}}
\def\@ynthm#1#2{%
  \expandafter\@ifdefinable\csname #1\endcsname
    {\@definecounter{#1}%
     \expandafter\xdef\csname the#1\endcsname{\@thmcounter{#1}}%
     \global\@namedef{#1}{\@thm{#1}{#2}}%
     \global\@namedef{end#1}{\@endtheorem}}}
\def\@othm#1[#2]#3{%
  \@ifundefined{c@#2}{\@nocounterr{#2}}%
    {\expandafter\@ifdefinable\csname #1\endcsname
    {\global\@namedef{the#1}{\@nameuse{the#2}}%
  \global\@namedef{#1}{\@thm{#2}{#3}}%
  \global\@namedef{end#1}{\@endtheorem}}}}
\def\@thm#1#2{%
  \refstepcounter{#1}%
  \@ifnextchar[{\@ythm{#1}{#2}}{\@xthm{#1}{#2}}}
\def\@xthm#1#2{%
  \@begintheorem{#2}{\csname the#1\endcsname}\ignorespaces}
\def\@ythm#1#2[#3]{%
  \@opargbegintheorem{#2}{\csname the#1\endcsname}{#3}\ignorespaces}
\def\@thmcounter#1{\noexpand\arabic{#1}}
\def\@thmcountersep{.}
\def\@begintheorem#1#2{\trivlist
   \item[\hskip \labelsep{\bfseries #1\ #2}]\itshape}
\def\@opargbegintheorem#1#2#3{\trivlist
      \item[\hskip \labelsep{\bfseries #1\ #2\ (#3)}]\itshape}
\def\@endtheorem{\endtrivlist}

Each definition is called using conditions as part of \newtheorem and \@nthm using \@ifnextchar[. These conditions are implicit when defining a macro using \newcommand; \newtheorem uses \def and therefore explicitly checks to see if the following argument starts with a [ (which should be optional).

These tricks are simplified by xparse where you can mix optional arguments within a definition without chaining. For example:

\usepackage{xparse}
\NewDocumentCommand{\mycmd}{m O{opt} m}{first #1\space second (opt: #2) #3}

displays the same output as defined above using chaining.

4

How are commands with an optional argument defined in the LaTeX kernel way?

The key function is \@ifnextchar, which tests the following token (gobbling spaces) and can perform different actions. See Understanding \@ifnextchar for more information.

In your case we want a command \foo that does different things when called as \foo{m1}{m2}, \foo{m1}[o1]{m2} or \foo{m1}{m2}[o2]. The two optional argument are mutually exclusive.

Let's start: the macro \foo will collect the first mandatory argument and test for [:

\def\foo#1{\@ifnextchar[{\foo@firstopt{#1}}{\foo@nofirstopt{#1}}}

It's very important that \@ifnextchar<token>{<true>}{<false>} is at the end.

We now have to define \foo@firstopt, which has to absorb another mandatory argument; the [ is not removed from the main input list, so we can do

\def\foo@firstopt#1[#2]#3{%
   <the code for the "o1" case>%
   \@ifnextchar[{\foo@badsecondopt}{}%
}

At the end we add code for testing the presence of the second optional argument, to raise an error and remove the offending part:

\def\foo@badsecondopt[#1]{<raise an error>}

Now let's tackle \foo@nofirstopt; we need to check for a trailing optional argument:

\def\foo@nofirstopt#1#2{%
  \@ifnextchar[{\foo@secondopt{#1}{#2}}{\foo@nosecondopt{#1}{#2}}%
}

Now it's easy:

\def\foo@secondopt#1#2[#3]{%
  <the code for the "o2" case>%
}
\def\foo@nosecondopt#1#2{%
  <the code for the "no optional arguments" case>%
}

The description of \newtheorem is a bit more complicated, because the kernel tries to avoid code duplication.

Note how already absorbed arguments can be carried on to the next stage.

How can we do the same with xparse?

\NewDocumentCommand{\foo}{m o m o}{%
  \IfNoValueTF{#1}%
    {% no o1
     \IfNoValueTF{#2}%
       {% no o2
        <code for the "no optional arguments" case>%
       }%
       {% o2
        <code for the "o2" case>%
       }%
    }%
    {% o1
     <code for the "o1" case>%
     \IfNoValueF{#2}{<error message>}%
    }%
  }

The two mandatory arguments are referred to as #1 and #3, the two optional arguments as #2 and #4.

2

Werner in his answer already exhibited and explained the code sequences of the LaTeX 2ε-kernel where \newtheorem is defined.

As you can see in his answer, this is not done by means of \newcommand.

To be honest, I cannot get all too excited about macros with many optional arguments in square brackets.

Often it is worth considering to use a package like keyval or xkeyval or processkv or pdfkeys or whatever for key=value-processing, and to have only one optional argument where you can pass a key=value-list where the keys and values denote which values you wish to differ in which ways from their default-values.

Be that as it may.

In any case I strongly recommend to define macros in a way where optional arguments are never preceded and/or trailed by other optional arguments. In other words: Optional arguments should never be directly adjacent to each other.
An exception to the rule could be a scenario where providing adjacent optional arguments at the further right only makes sense when all adjacent optional arguments at the further left are provided also.

In case you are interested, here is my own small personal toolkit for defining macros which process several optional arguments.

The gist is:

With my toolkit mechanisms for processing several optional arguments consist of two things:

  • A "Wrapper"-macro which first collects optional and non-optional-arguments into a list of non-optional arguments maintained via macro \UD@CollectedArguments and then passes the expansion of \UD@CollectedArguments to the internal macro which does the actual work. The "wrapper-macro" must be robust/non-expandable as by LaTeX 2ε-kernel-design macros that process optional arguments cannot be carried out in pure-expansion-contexts.
  • An internal macro which processes all arguments as non-optional arguments and does the actual work.

The single macros:

  • The macro \UD@ClearCollectedArguments defines the list \UD@CollectedArguments to be empty.
  • The macro \UD@PassAndClearCollectedArguments{⟨internal macro⟩} passes the list of arguments held in \UD@CollectedArguments to ⟨internal macro⟩ and defines \UD@CollectedArguments to be empty.
  • The macro \UD@AddOptArgToCollectedArguments{⟨default value⟩}{⟨continue⟩} collects an optional argument whose default value is ⟨default value⟩ and attaches the tokens which form that optional argument, as another non-optional argument, to the list of non-optional-arguments that is held in \UD@CollectedArguments and then delivers ⟨continue⟩.
  • The macro \UD@AddNonOptArgToCollectedArguments{⟨continue⟩} collects a non-optional argument and attaches the tokens which form that non-optional argument, as another non-optional argument, to the list of non-optional-arguments that is held in \UD@CollectedArguments, and then delivers ⟨continue⟩.

Thus you can gather up arguments by nesting calls to \UD@AddOptArgToCollectedArguments/\UD@AddNonOptArgToCollectedArguments within the ⟨continue⟩-argument. In the innermost nesting-level the ⟨continue⟩-argument holds the \UD@PassAndClearCollectedArguments-directive for passing the collected arguments to the ⟨internal macro⟩ which does the actual work.

\documentclass[landscape, a4paper]{article}

%-------------------[adjust margins/layout for the example]--------------------
\csname @ifundefined\endcsname{pagewidth}{}{\pagewidth=\paperwidth}%
\csname @ifundefined\endcsname{pdfpagewidth}{}{\pdfpagewidth=\paperwidth}%
\csname @ifundefined\endcsname{pageheight}{}{\pageheight=\paperheight}%
\csname @ifundefined\endcsname{pdfpageheight}{}{\pdfpageheight=\paperheight}%
\textwidth=\paperwidth
\oddsidemargin=2cm
\marginparsep=.125\oddsidemargin
\marginparwidth=\oddsidemargin
\advance\marginparwidth-2\marginparsep
\advance\textwidth-2\oddsidemargin
\advance\oddsidemargin-1in
\evensidemargin=\oddsidemargin
\textheight=\paperheight
\topmargin=2cm
\footskip=.5\topmargin
{\normalfont\global\advance\footskip.5\ht\strutbox}%
\advance\textheight-2\topmargin
\advance\topmargin-1in
\headheight=0ex
\headsep=0ex
\pagestyle{plain}
\parindent=0ex
\parskip=\bigskipamount
%------------------[eof margin-adjustments]------------------------------------

\makeatletter
%========[This is my personal toolkit for gathering optional arguments]========
% (As macros with optional arguments by LaTeX2e-kernel-design cannot be used in
%  full-expansion-contexts, let's gather arguments within a macro
%  \UD@CollectedArguments and in a last step pass the arguments gathered in
%  that macro to an internal macro which does process non-optional
%  arguments only.)
\newcommand\UD@exchange[2]{#2#1}%
\newcommand\UD@CollectedArguments{}%
\newcommand\UD@ClearCollectedArguments{\long\gdef\UD@CollectedArguments{}}%
\newcommand\UD@PassAndClearCollectedArguments[1]{%
  \expandafter\UD@exchange\expandafter{\UD@CollectedArguments}{\UD@ClearCollectedArguments#1}%
}%
%------------------------------------------------------------------------------
% \UD@AddOptArgToCollectedArguments{<default value>}{<continue>}
%
% Grabs an optional argument whose default value is <default value>
% , wraps it in curly braces and adds it to the macro \UD@CollectedArguments
% and delivers <continue>.
%..............................................................................
\newcommand\UD@AddOptArgToCollectedArguments[2]{%
  \@testopt{\UD@@AddOptArgToCollectedArguments{#2}}{#1}%
}%
\@ifdefinable\UD@@AddOptArgToCollectedArguments{%
  \begingroup
  % Check the availability of the \unexpanded-primitive:
  \edef\@tempa{\meaning\unexpanded}%
  \edef\@tempb{\string\unexpanded}%
  \expandafter\endgroup
  \ifx\@tempa\@tempb\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
  {% <- \unexpanded is available:
    \long\def\UD@@AddOptArgToCollectedArguments#1[#2]{%
      \xdef\UD@CollectedArguments{\unexpanded\expandafter{\UD@CollectedArguments{#2}}}%
      #1%
    }%
  }{% <- \unexpanded is not available:
    \long\def\UD@@AddOptArgToCollectedArguments#1[#2]{%
      \expandafter\UD@exchange\expandafter{\expandafter
      \toks@\expandafter{\the\toks@}}{%
        \toks@\expandafter{\UD@CollectedArguments{#2}}%
        \xdef\UD@CollectedArguments{\the\toks@}%
      }%
      #1%
    }%
  }%
}%
%------------------------------------------------------------------------------
% \UD@AddNonOptArgToCollectedArguments{<continue>}
%
% Grabs a non-optional argument, wraps it in curly braces and adds it to the
% macro \UD@CollectedArguments and delivers <continue>.
%..............................................................................
\newcommand\UD@AddNonOptArgToCollectedArguments[2]{%
   \UD@@AddOptArgToCollectedArguments{#1}[{#2}]%
}%
%------------------------------------------------------------------------------
% Explanation:
%
% You can gather up arguments within the macro \UD@CollectedArguments by nesting
% calls to \UD@AddOptArgToCollectedArguments / \UD@AddNonOptArgToCollectedArguments
% within the <continue>-arguments.
%========[eof personal toolkit]================================================

% Now let's use the personal toolkit for creating a mechanism which
% gathers 9 arguments, whereof the 1st, 3rd, 5th, 7th and 9th are optional
% while the 2nd, 4th, 6th and 8th are non-optional:
%
\newcommand\ProcessNineArgumentsInternal[9]{%
  \textbf{Arguments are:}\\
  \texttt{[#1]\{#2\}[#3]\{#4\}[#5]\{#6\}[#7]\{#8\}[#9]}
}%
% Now the argument-gathering wrapper for \ProcessNineArgumentsInternal -
% !!!!! this must be robust !!!!
\@ifdefinable\ProcessNineArguments{%
  \DeclareRobustCommand\ProcessNineArguments{%
    \UD@ClearCollectedArguments
    \UD@AddOptArgToCollectedArguments{DEFAULT 1}{%
      \UD@AddNonOptArgToCollectedArguments{%
        \UD@AddOptArgToCollectedArguments{DEFAULT 2}{%
          \UD@AddNonOptArgToCollectedArguments{%
            \UD@AddOptArgToCollectedArguments{DEFAULT 3}{%
              \UD@AddNonOptArgToCollectedArguments{%
                \UD@AddOptArgToCollectedArguments{DEFAULT 4}{%
                  \UD@AddNonOptArgToCollectedArguments{%
                    \UD@AddOptArgToCollectedArguments{DEFAULT 5}{%
                      \UD@PassAndClearCollectedArguments{\ProcessNineArgumentsInternal}%
                    }%
                  }%
                }%
              }%
            }%
          }%
        }%
      }%
    }%
  }%
}%
\makeatother

\begin{document}

\verb|\ProcessNineArguments{1st non-opt}{2nd non-opt}{3rd non-opt}{4th non-opt}| yields:\\
\ProcessNineArguments{1st non-opt}{2nd non-opt}{3rd non-opt}{4th non-opt}

\verb|\ProcessNineArguments[1st opt]{1st non-opt}{2nd non-opt}[3rd opt]{3rd non-opt}{4th non-opt}| yields:\\
\ProcessNineArguments[1st opt]{1st non-opt}{2nd non-opt}[3rd opt]{3rd non-opt}{4th non-opt}

\verb|\ProcessNineArguments[1st opt]{1st non-opt}{2nd non-opt}[3rd opt]{3rd non-opt}{4th non-opt}[5th opt]| yields:\\
\ProcessNineArguments[1st opt]{1st non-opt}{2nd non-opt}[3rd opt]{3rd non-opt}{4th non-opt}[5th opt]

\verb|\ProcessNineArguments[1st opt]{1st non-opt}[2nd opt]{2nd non-opt}[3rd opt]{3rd non-opt}[4th opt]{4th non-opt}[5th opt]| yields:\\
\ProcessNineArguments[1st opt]{1st non-opt}[2nd opt]{2nd non-opt}[3rd opt]{3rd non-opt}[4th opt]{4th non-opt}[5th opt]


\end{document}

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.