4

I build a macro named \fromlist to get value from a list. The value can be typeset, but can not be invoked directly, why and how to deal with it?

Code:

\documentclass[a4paper]{article}
\usepackage{pgffor}
\usepackage{geometry}
\geometry{showframe}
\geometry{left=1cm,right=1cm,top=1cm,bottom=1cm}
\parindent0pt


\begin{document}

\newcommand\fromlist[2]{%
  \foreach \x [count=\i] in #1{%
    \ifnum\i=#2 \x\breakforeach\fi%
  }%
}

% Test:
\def\bbb{2in}\rule{\bbb}{5pt} % This ok.
\def\bbb{\fromlist{{1in,2in,3in}}{2}}\bbb %This is ok.
\rule{\fromlist{{1in,2in,3in}}{2}}{5pt} % This causes error, why?
1
  • \fromlist is not expandable (for example, try \edef\tmp{\fromlist{{1in,2in,3in}}{2}}). Thus, \rule doesn't see the final result for use as its input, and coughs an error. Mar 22, 2022 at 1:34

4 Answers 4

4

enter image description here

You need to extract the item by expansion, you can not make assignments within the argument to \rule. Fortunately LaTeX provides such a function out of the box in the clist (comma list) module. More exactlythe nn variant is available, which would work for the explicit list, if you need to expand commands holding a list, you can declare an ee variant as here for the second example.

\documentclass[a4paper]{article}
\usepackage{geometry}
\geometry{showframe}
\geometry{left=1cm,right=1cm,top=1cm,bottom=1cm}
\parindent0pt

\ExplSyntaxOn
\cs_generate_variant:Nn\clist_item:nn{ee}
\newcommand\fromlist{\clist_item:ee}
\ExplSyntaxOff

\newcommand\mylist{1in,2in,3in}
\begin{document}


\rule{\fromlist{1in,2in,3in}{2}}{5pt} 

\rule{\fromlist{\mylist}{3}}{5pt} 


\end{document}
4

When we say that “a command is (fully) expandable” we mean that it delivers the intended result by performing no assignment or typesetting instruction (among which there is \relax).

So your \bbb works, because its expansion is a length. TeX knows that the first argument to \rule should contain a (rigid) length, and it performs macro expansion until finding something that fits the requirement.

When TeX sees your \fromlist, it realizes it's not a length, so it does an expansion and it sees \foreach. If you do

latexdef -p pgffor -s foreach

from a terminal, you'll get

% pgffor.code.tex, line 60:
\let\foreach=\pgffor@foreach

% pgffor.code.tex, line 45:
\def\pgffor@foreach{%
    \pgffor@atbeginforeach%
    \let\pgffor@assign@before@code=\pgfutil@empty%
    \let\pgffor@assign@after@code=\pgfutil@empty%
    \let\pgffor@assign@once@code=\pgfutil@empty%
    \let\pgffor@remember@code=\pgfutil@empty%
    \let\pgffor@remember@once@code=\pgfutil@empty%
    \pgffor@alphabeticsequencefalse%
    \pgffor@contextfalse%
    %
    \let\pgffor@var=\pgfutil@empty
    %
    \pgffor@vars%
}

and you know that your plan is doomed, because even if \pgffor@atbeginforeach is expandable (it isn't actually), a bunch of \let instructions follows and one is already too many.

Is it necessary to run latexdef to see whether a command is expandable or not? Not really. In general you should not expect a command is expandable, unless somebody tells you it is.

And among the expl3 functions, you can spot those that are (fully) expandable because their name is followed by a star (filled or hollow). The descriptions are found in interface3.pdf.

Your input is a comma separated list of items, so it fits the syntax of a clist. And yes, the function \clist_item:nn has the star after it! So you might do

\ExplSyntaxOn
\cs_set_eq:NN \fromlist \clist_item:nn
\ExplSyntaxOff

but then you'll ask a new question because something like

\def\ccc{1pt,2pt,3pt}
\rule{\fromlist\ccc{2}}{3cm}

doesn't work. How can you accommodate both implicit and explicit comma lists?

My suggestion is to define \fromlist with a *-variant that accepts an implicit list:

\ExplSyntaxOn
\NewExpandableDocumentCommand{\fromlist}{smm}
 {
  \IfBooleanTF{#1}
   {% there is *
    \clist_item:Nn #2 { #3 }
   }
   {% no *
    \clist_item:nn { #2 } { #3 }
   }
 }
\ExplSyntaxOff

A full example.

\documentclass{article}

\ExplSyntaxOn

\NewExpandableDocumentCommand{\fromlist}{smm}
 {
  \IfBooleanTF{#1}
   {% there is *
    \clist_item:Nn #2 { #3 }
   }
   {% no *
    \clist_item:nn { #2 } { #3 }
   }
 }

\ExplSyntaxOff

\newcommand{\mylist}{1in,2in,3in}

\begin{document}

\rule{\fromlist{1in,2in,3in}{2}}{5pt}

\rule{\fromlist*{\mylist}{3}}{5pt}

\end{document}

enter image description here

5
  • And `\usepackage{xparse} needed?
    – lyl
    Mar 22, 2022 at 9:41
  • If the delimiter of a list is :, not comma, how to do it?
    – lyl
    Mar 22, 2022 at 9:49
  • @lyl You basically have to redefine all the clist business for colon-separated lists.
    – egreg
    Mar 22, 2022 at 11:34
  • in my Miktex, \usepackage{xparse} has to be added to your code, or it will fail to run. Does that mean I lost some packages in my system?
    – lyl
    Mar 22, 2022 at 13:01
  • @lyl You should update.
    – egreg
    Mar 22, 2022 at 16:01
2

As I commented, \fromlist is not expandable (for example, try \edef\tmp{\fromlist{{1in,2in,3in}}{2}}). Thus, \rule doesn't see the final result of \fromlist, for use as its input. It therefore throws an error.

A different approach is proposed below, using listofitems, to read and parse the list. Then, particular entries from the list can be expandably recalled. The key is to read the list before passing a list element to \rule.

I first show it as a two step process with \readlist followed by \rule. Alternatively, one can place those two steps in a macro \Zrule, as I do in the second approach.

\documentclass[a4paper]{article}
\usepackage{listofitems}
\usepackage{geometry}
\geometry{showframe}
\geometry{left=1cm,right=1cm,top=1cm,bottom=1cm}
\parindent0pt
\newcommand\Zrule[3]{%
  \readlist\rulearg{#1}%
  \rule{\rulearg[#2]}{#3}
}
\begin{document}

\readlist\rulelen{1cm,2.4cm,3cm}
\rule{\rulelen[2]}{5pt}

\Zrule{1in,2in,3in}{2}{5pt}
\end{document}

enter image description here

6
  • Thank you so much for this wonderful solution! ! And how to determine if a macro is expandable? Why is my fromlist non-expandable?
    – lyl
    Mar 22, 2022 at 2:20
  • @lyl \foreach isn't expandable (you would need to see the pgf documentation you coulduse an expl3 expandable function to access a list item as shown previously, but even if the loop macro were xpandable \foreach puts each item in a group and you make an assignmnt to \x and assignments are not expandable. You need \rule{1in} but effectively have \rule{ { \def\x{1in} } \x } which can not work. Mar 22, 2022 at 7:44
  • As for listofitems, is there any way to modify an item of the list? For example, \readlist\mylist{a,b,c}, how to change the second item to d, then get the list {a,d,c}?
    – lyl
    Mar 25, 2022 at 2:56
  • @lyl I'm afraid listofitems does not provide that feature, but another package, readarray (ctan.org/pkg/readarray) has some of those features, though it is primarily intended for 2-D and 3-D arrays. Mar 25, 2022 at 13:09
  • @lyl here is one way to do it with readarray: \documentclass[a4paper]{article} \usepackage{readarray} \usepackage{geometry} \geometry{showframe} \geometry{left=1cm,right=1cm,top=1cm,bottom=1cm} \parindent0pt \begin{document} \readarraysepchar{,} \def\z{1cm,2.4cm,3cm} \readarray\z\rulelen[-,1] \rule{\rulelen[2,1]}{5pt} \setvalue\rulelen[2,1]{6cm} \rule{\rulelen[2,1]}{5pt} \end{document} Mar 25, 2022 at 13:25
2

This s a variant

\fromlist{⟨tokens to prepend to i-th comma-list-item⟩}%
         {⟨tokens to append to i-th comma-list-item⟩}%
         {⟨TeX-⟨number⟩-quantity of value i⟩}%
         {⟨comma-list⟩}

which—within the local scope of the i-th \foreach-iteration—does

⟨tokens to prepend to i-th comma-list-item⟩{⟨i-th comma-list-item⟩}⟨tokens to append to i-th comma-list-item⟩:

\documentclass[a4paper]{article}
\usepackage{pgffor}
\usepackage{geometry}
\geometry{showframe}
\geometry{left=1cm,right=1cm,top=1cm,bottom=1cm}
\parindent0pt

\makeatletter
\newcommand\PassFirstToSecond[2]{#2{#1}}
\newcommand\fromlist[4]{%
  \foreach \x [count=\i] in #4{%
    \ifnum\i=\expandafter\@firstofone\expandafter{\number#3} %
    \expandafter\@firstofone\else\expandafter\@gobble\fi
    {%
      \breakforeach
      \expandafter\PassFirstToSecond\expandafter{\x}{#1}#2%
    }%
  }%
}%
\makeatother

\begin{document}

List provided explicitly:

\fromlist{\rule}{{5pt}}{1}{{1in,2in,3in}}

\fromlist{\rule}{{5pt}}{2}{{1in,2in,3in}}

\fromlist{\rule}{{5pt}}{3}{{1in,2in,3in}}

\newcommand\commalist{1in,2in,3in}

List provided via macro:

\fromlist{\rule}{{5pt}}{1}{\commalist}

\fromlist{\rule}{{5pt}}{2}{\commalist}

\fromlist{\rule}{{5pt}}{3}{\commalist}

\end{document}

enter image description here

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .