5

I'm writing a class and thus storing a bunch of data into variables. Those variable needs to be access, and rely on a counter value.

However, when accessing the values of previously-defined variable, I'm surprised to see that it has been overwritten.

My MWE features a counter, dataID. I want data to be stored in dynamic variables declared with this pattern : data@<counter_value>@prio. I'm using custom macro (getValue and storeValue that resectively use \@nameuse and \@namedef).

However as you can see in the MWE, when the counter dataID is at 2, the variable data@1@prio gets overwritten with the value stored in data@2@prio. How can I prevent this ?

\documentclass[a4paper]{article}
\usepackage{hyperref}
\usepackage{xcolor}

\makeatletter
\newcommand{\getValue}[1]{%
    \@ifundefined{#1}{%  
        \PackageError{lookup}{No #1 key defined}{}%
    }{%
        \@nameuse{\expandafter#1}%
    }%
}
\newcommand{\storeValue}[2]{%
    \@namedef{#1}{#2}%
}

\newcommand{\labelText}[1]{%
    \@bsphack
    \csname phantomsection\endcsname % for hyperrredf usage
    \def\@currentlabel{#1}{\label{#1}}%
    \@esphack
}

\newcounter{dataID}
\newcommand{\data}{%
    \refstepcounter{dataID}%
    \labelText{data\thedataID}
    data\thedataID

    \quad dataID value = \thedataID
    \expandafter\storeValue{data@\thedataID @prio}{This is \ref{data\thedataID}}
}
\makeatother


\begin{document}
    \data   % data1

    \texttt{data@1@prio} $\rightarrow$ \getValue{data@1@prio} \quad\textcolor{blue}{1st Definition of \texttt{data@1@prio} !}

    ---

    \data   % data2

    \texttt{data@thedataID@prio} $\rightarrow$ \getValue{data@\thedataID @prio} (because dataID=2)

    ---

    \texttt{data@1@prio} $\rightarrow$ \getValue{data@1@prio} \quad\textcolor{red}{Redefinition of \texttt{data@1@prio} !}

    \texttt{data@2@prio} $\rightarrow$ \getValue{data@2@prio}

\end{document}

Here is the output : MWE output

Depending on the place where it's called, the value of \getValue{data@1@prio} is not the same.

| improve this question | | | | |
6

As @frougon says in their nice breakdown of your code, for which he deserves the green tick, the main problem in terms of the output is that you are storing the unexpanded \ref{data\thedataID} so what \getValue returns depends on the current the value of \thedataID.

Rather than using \@nameuse and friends I recommend using the corresponding commands from the etoolbox package as, at least for me, this results in more readable code:

\documentclass[a4paper]{article}
\usepackage{hyperref}
\usepackage{xcolor}
\usepackage{etoolbox}

\newcommand{\getValue}[1]{
    \ifcsdef{#1}{\csuse{#1}}%
                {\PackageError{lookup}{No #1 key defined}{}}%
}
\newcommand{\storeValue}[2]{\csxdef{#1}{#2}}

\makeatletter
\newcommand{\labelText}[1]{\typeout{label=#1.}%
    \@bsphack
    \phantomsection % for hyperrredf usage
    \def\@currentlabel{#1}{\label{#1}}%
    \@esphack
}
\makeatother

\newcounter{dataID}
\newcommand{\data}{%
    \refstepcounter{dataID}%
    \labelText{data\thedataID}
    data\thedataID

    \quad dataID value = \thedataID
    \storeValue{data@\thedataID @prio}{This is \noexpand\ref{data\thedataID}}
}

\begin{document}
    \data   % data1

    \texttt{data@1@prio} $\rightarrow$ \getValue{data@1@prio} \quad\textcolor{blue}{1st Definition of \texttt{data@1@prio} !}

    ---

    \data   % data2

    \texttt{data@thedataID@prio} $\rightarrow$ \getValue{data@\thedataID @prio} (because dataID=2)

    ---

    \texttt{data@1@prio} $\rightarrow$ \getValue{data@1@prio} \quad\textcolor{red}{Redefinition of \texttt{data@1@prio} !}

    \texttt{data@2@prio} $\rightarrow$ \getValue{data@2@prio}

\end{document}

The output is the same as above:

enter image description here

| improve this answer | | | | |
10

You picked the question tags right: your main problem here concerns expansion. :-)

  • When you do \@nameuse{\expandafter#1}, this is... not very useful, because:

    1. You are passing \expandafter#1 to \@nameuse (with the argument substituted for #1), whereas you probably wanted to pass the result of its expansion. In this particular case, since \@nameuse is simply macro:#1->\csname #1\endcsname, its argument will be expanded anyway, due to the way \csname works.
    2. The \expandafter is not put to good use here. If you call for instance \getValue{foo}, when the \getValue control sequence token is expanded, \getValue{foo} will be replaced by the replacement text of \getValue, in which the argument foo will have been substituted for #1. In this case, this will yield:
      \@ifundefined{foo}{%  
          \PackageError{lookup}{No foo key defined}{}%
      }{%
          \@nameuse{\expandafter foo}%
      }%
      
      When TeX expands this \@nameuse, this will result in:
      \csname \expandafter foo\endcsname
      
      thus, when TeX gets to the expand the \expandafter, it will try to expand the token following f, i.e. the first character token o, which is unexpandable. So, this \expandafter doesn't serve any purpose.
  • The other \expandafter:

    \expandafter\storeValue{data@\thedataID @prio}{This is \ref{data\thedataID}}
    

    has a similar problem. It will try to expand the opening brace { following \storeValue, which again is a non-active character token and therefore is unexpandable. In this case, you want to expand the two \thedataID tokens but not \ref before \storeValue is itself expanded. Therefore, I would propose something like:

    \begingroup
    \edef\tmp{%
      \endgroup
      \noexpand\storeValue{data@\thedataID @prio}
                          {This is \noexpand\ref{data\thedataID}}%
    }%
    \tmp
    

    The \noexpand tokens prevent expansion of the next token inside the \edef. Then they disappear, so that after \tmp is expanded in the last line of this excerpt, TeX sees:

    \endgroup
    \storeValue{data@1@prio}{This is \ref{data1}}%
    

    assuming for instance that \thedataID expanded to 1. The \endgroup closes the group where \tmp was defined, and what is left in the input stream is exactly what you wanted.

  • Similarly, in \labelText, you want the argument to be recursively expanded in order to get the counter value in the replacement text of \@currentlabel, as opposed to the five tokens data\thedataID (four character tokens followed by one control sequence token). Since \label recursively expands its argument anyway using \protected@write, we can do \protected@edef\@currentlabel{#1} for consistency to expand the argument (this implies that \protect will work when used inside this argument in case you need it). Actually, \def also works here, because \label does \protected@write\@auxout{}{ ... \@currentlabel ...}, so it recursively expands both its argument and the \@currentlabel.

Other things:

  • There is no need to wrap the \label call within braces (and make it look like an argument to \def!).

  • hyperref should be loaded last, except in very special cases such as when cleveref is used (cleveref must be loaded after hyperref).

  • Minor nit: you don't need \csname ... \endcsname to form the \phantomsection control sequence token (all letters p, h, a, ..., i, o, n have category code 11 in normal circumstances, i.e. are letters for TeX).

Note: I left your @ as is because it works, but there is a trap, so beware. Indeed, the @ in the replacement text of your \data macro has category code 11 (letter), whereas the ones in your document body have category code 12 (other). These are not the same character tokens. This works because in the end, they are only used inside \csname ... \endcsname, and there it doesn't matter whether they have catcode 11 or 12 (they become part of a control sequence name, which is just a sequence of characters in the ordinary sense, without any catcode attached). If unsure, you can always use another character, such as . or -.

All in all, I propose the following:

\documentclass{article}
\usepackage{hyperref}

\makeatletter
\newcommand{\getValue}[1]{%
  \@ifundefined{#1}{%
      \PackageError{lookup}{No '#1' key defined}{}%
  }{%
      \@nameuse{#1}%
  }%
}

\newcommand{\storeValue}[2]{%
  \@namedef{#1}{#2}%
}

\newcommand{\labelText}[1]{%
  \@bsphack
  \phantomsection % for hyperrred usage
  % \def also works here (see above)
  \protected@edef\@currentlabel{#1}%
  \label{#1}%
  \@esphack
}

\newcounter{dataID}
\newcommand{\data}{%
    \refstepcounter{dataID}%
    \labelText{data\thedataID}
    data\thedataID

    \quad dataID value = \thedataID
    \begingroup
    \edef\tmp{%
      \endgroup
      \noexpand\storeValue{data@\thedataID @prio}
                          {This is \noexpand\ref{data\thedataID}}%
    }%
    \tmp
}
\makeatother

\begin{document}

\data   % data1

\verb|data@1@prio|
$\rightarrow$ \getValue{data@1@prio}

---

\data   % data2

\verb|data@\thedataID@prio|
$\rightarrow$ \getValue{data@\thedataID @prio} (because dataID=2)

---

\verb|data@1@prio| $\rightarrow$ \getValue{data@1@prio}

\verb|data@2@prio| $\rightarrow$ \getValue{data@2@prio}

\end{document}

screenshot

| improve this answer | | | | |
  • +1 for your nice breakdown of the OP – Andrew Jan 15 at 12:15
  • Thanks @Andrew, and +1 for your idea of using etoolbox. You can use the pronoun he for me. :-) – frougon Jan 15 at 12:35
  • Thanks for the complete explanation and the solution. I'll go for the etoolbox solution since I find it more readable indeed. – 3isenHeim Jan 15 at 12:49
  • @3isenHeim Please give the tick to frougon as his solution is more comprehensive than mine and he was first to solve the actual problem that you had. – Andrew Jan 15 at 15:48
4

I propose a different implementation using property lists of expl3:

\documentclass[a4paper]{article}
\usepackage{xcolor,xparse}
\usepackage{hyperref}

\ExplSyntaxOn

\prop_new:N \g_eisenheim_values_prop
\cs_generate_variant:Nn \prop_item:Nn { Ne }
\cs_generate_variant:Nn \prop_gput:Nnn { Nxx }
\prg_generate_conditional_variant:Nnn \prop_if_in:Nn { Ne } { T,F,TF,p }
\cs_generate_variant:Nn \msg_expandable_error:nnn { nne }

\NewExpandableDocumentCommand{\getValue}{m}
 {
  \prop_if_in:NeTF \g_eisenheim_values_prop { #1 }
   {
    \prop_item:Ne \g_eisenheim_values_prop { #1 }
   }
   {
    \msg_expandable_error:nne { lookup } { missing } { #1 }
   }
 }

\NewDocumentCommand{\storeValue}{mm}
 {
  \prop_gput:Nxx \g_eisenheim_values_prop { #1 } { #2 }
 }

\msg_new:nnn { lookup } { missing } { The~value~#1~is~missing }

\ExplSyntaxOff

\makeatletter
\newcommand{\labelText}[1]{%
    \@bsphack
    \csname phantomsection\endcsname % for hyperref usage
    \def\@currentlabel{#1}{\label{#1}}%
    \@esphack
}
\makeatother

\newcounter{dataID}

\newcommand{\data}{%
    \refstepcounter{dataID}%
    \labelText{data\thedataID}%
    data\thedataID
    \quad dataID value = \thedataID
    \storeValue{data@\thedataID @prio}{This is \noexpand\ref{data\thedataID}}
}


\begin{document}

\data   % data1

\texttt{data@1@prio} $\rightarrow$ \getValue{data@1@prio}

---

\data   % data2

\texttt{data@thedataID@prio} $\rightarrow$ \getValue{data@\thedataID @prio} (because dataID=2)

---

\texttt{data@1@prio} $\rightarrow$ \getValue{data@1@prio} 

\texttt{data@2@prio} $\rightarrow$ \getValue{data@2@prio}

\getValue{x}

\end{document}

enter image description here

The advantage is that \getValue is fully expandable.

| improve this answer | | | | |
  • +1 for showing me \mes_new:nnn but do you really want to advocate the use of \csname phantomsection\endcsname? Also, is there a more elegant eplc3 replacement for \@bsphack...\@esphack? – Andrew Jan 15 at 15:52
  • @Andrew That's a known trick. Possibly, in a not so far future, the LaTeX kernel can provide a way to avoid it. As far as \@bsphack and \@esphack is concerned, there's nothing better ATM. – egreg Jan 15 at 15:54

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.