9

What is the recommended way to emulate pgfkeys' /.code 2 args (or more generally /.code n args) handler with l3keys? For example, with pgfkeys one can do

\documentclass{article}

\usepackage{pgfkeys}

\newcommand{\cmdA}{init A}
\newcommand{\cmdB}{init B}

\pgfkeys{
    somekey/.code 2 args={%
        \renewcommand{\cmdA}{#1}%
        \renewcommand{\cmdB}{#2}%
        }
    }

\begin{document}

\cmdA\ \cmdB

\pgfkeys{somekey={new A}{new B}}

\cmdA\ \cmdB

\end{document}

img1

With the .code:n handler of l3keys, only a single parameter #1 is allowed.

\ExplSyntaxOn
\keys_define:nn { mymodule } {
    somekey .code:n = {
        \tl_set:Nn \cmdA { #1 }
        %\tl_set:Nn \cmdB { #2 } % want to be able to do this
        }
    }
\ExplSyntaxOff

I've figured out how to do it by expanding the argument and passing it to an auxiliary macro:

\documentclass{article}

\newcommand{\cmdA}{init A}
\newcommand{\cmdB}{init B}

\newcommand{\auxmacro}[2]{%
    \renewcommand{\cmdA}{#1}%
    \renewcommand{\cmdB}{#2}%
    }
    
\ExplSyntaxOn
\keys_define:nn { mymodule } {
    somekey .code:n = { \exp_after:wN \auxmacro #1 }
    }
\ExplSyntaxOff

\begin{document}

\cmdA\ \cmdB

\SetKeys[mymodule]{somekey={new A}{new B}}

\cmdA\ \cmdB

\end{document}

But is there a way to use more parameters #2, #3, etc. directly in the key definition?

4
  • 1
    I think it is a deliberate design choice. One of the reasons to prefer a key-value interface to something like \newcommand[9]{...} is that it makes it difficult to remember which argument is which and what syntax it uses. Of course, you can have as many arguments with \NewDocumentCommand or the like, but I think the ideal is intended to be minimising the number of distinct arguments by having one accept a list of key-values for processing. If your keys allow multiple arguments, people will use them and you'll end up recreating the opacity of \newcommand[9]....
    – cfr
    Commented Sep 24, 2023 at 1:59
  • @cfr Thanks for your insight. In general, I agree with your assessment. However I can see a case specifically for two arguments when the order is obvious (e.g. left/right) and the arguments set similar things. For example, thmtools' key notebraces={<left brace>}{<right brace>} makes more sense to me than two separate keys leftbrace and rightbrace
    – mbert
    Commented Sep 24, 2023 at 2:59
  • 2
    I wasn't expressing a personal view. I was explaining what I think is the view of the architects. (Doubtless one or more of said architects will correct me if I'm wrong.) The other point is that the splitting of arguments may be more appropriately done with document-level commands, which are intended to parse user input and call internal functions.. So if an argument needs to be split, it should arguably be split before it is ever passed to \keys_set:nn. I'm not sure this fully addresses the issue, but it ought to go some way towards doing so.
    – cfr
    Commented Sep 24, 2023 at 3:07
  • 1
    There are also facilities for doing lower-level processing of keys. However, there is no (easy?) way to define a new handler, as far as I can tell. This is in contrast to both pgfkeys and the document-level idea of argument processors. It somewhat feels as if the key processing is at a slightly strange level, so there's something about the design I don't properly understand. That is, to me, the key stuff would most naturally go with argument processors etc. at the document level. But, aside from a scaled down version for package options, it's not there but here. I'd like to understand better.
    – cfr
    Commented Sep 24, 2023 at 3:11

2 Answers 2

7

As noted in comments, it is a deliberate decision not to provide 'divide up the value' functionality in l3keys. Instead, additional processing is expected to be delegated to a dedicated function that takes the value as input

\keys_define:nn { foo }
  {
    my-key .code:n = \__foo_process_my_key:n {#1}
  }
\cs_new_protected:Npn \__foo_process_my_key:n #1
  {
    % Whatever is appropriate here
  }
5
  • Could you explain why the keys stuff is in the programming layer, since mostly people seem to think of key-value interfaces as a way to handle user input? Is this explained anywhere?
    – cfr
    Commented Sep 25, 2023 at 20:58
  • @cfr If you put the keyval interface strictly in the document layer, then every (current) key has to have a documented programming-level interface to set it. That is of course doable but I suspect for most people would feel like massive overkill.
    – Joseph Wright
    Commented Sep 25, 2023 at 21:18
  • How is that different? Don't you already have to have a documented programming-level interface to set each kind of key? (I don't have any problem with the current design, because I generally find the expl3 layer easier than the was-xparse layer, but I think there's something I fundamentally don't get about the why.)
    – cfr
    Commented Sep 25, 2023 at 23:25
  • 1
    @cfr I mean that if you treat keys as part of the document layer, then every one also has to have a documented variable, whereas at present only the keys have to be documented (e.g. most point to internal data structures). Also, with keys at the document level, one would imagine that different interfaces could exist with different key names
    – Joseph Wright
    Commented Sep 26, 2023 at 5:34
  • @JW, Thank-you.
    – cfr
    Commented Sep 26, 2023 at 6:11
1

To illustrate, the two-step process described by Joseph Wright allows #1 to be open-ended: for example, it could be a data-structure:

The same foo, three different outputs:

abcde

(Somewhat on the analogy of DNA\RNA.)

MWE

\documentclass{article}
\usepackage{xcolor}

\ExplSyntaxOn

\keys_define:nn { foo }
  {
    my-key .code:n = \__foo_process_my_key:n {#1}
  }

\cs_new_protected:Npn \my_func:n #1 { 

    \tl_set:Nx
        \l_tmpb_tl
        {
            \seq_item:Nn
                    \_tmpa_seq
                    { #1 }
        }


    \seq_set_split:NnV
            \_tmpb_seq
            { ; }
        \l_tmpb_tl

%   \seq_show:N
%           \_tmpb_seq

    \tl_set:Nn
        \l_tmpb_tl
        {
        \textcolor { \seq_item:Nn \_tmpb_seq    { 3 }} { \seq_item:Nn \_tmpb_seq    { 1 } }
        }

    \tl_use:N
        \l_tmpb_tl
 }


\cs_new_protected:Npn \__foo_process_my_key:n #1
  {
    % Whatever is appropriate here

    \tl_set:Nn \l_tmpa_tl { #1 }

    \seq_set_split:NnV
            \_tmpa_seq
            { / }
            \l_tmpa_tl 

    \int_step_function:nnnN { 1 } { 1 } { \seq_count:N \_tmpa_seq } \my_func:n

  }

\NewDocumentCommand { \foo } { o } {
    \keys_set:nn { foo } { #1 }
}

\ExplSyntaxOff


\begin{document}

\foo[my-key=
ABCDE $\mapsto$~;;black
/A;z;red
/B;y;green
/C;x;blue
/D;w;yellow
/E;v;cyan
]

\foo[my-key=
The cat;;black
/~sat;;red
/~on the mat;;black
].


\foo[my-key=
The quick~;;black
/{\large brown};;red
/\kern-0.4pt\llap{{\large brown}};;blue
/~jumps over the lazy dog;;black
].


\end{document}

EDIT

In relation to the comment about code obscurity, a help option could be built into the code itself, separating code from front-end. A bit like the back and front of a tapestry.

This is from an attempt at expl3-ing an SE answer about multi-coloured letters.

obscurity

As an aside, expl3 makes some things, like lettrining something, almost trivial (but not uncomplex - the problem to be solved drives the complexity-level of any solution; here it was legibility and something pleasant enough to read).

MWE

\documentclass{article}
\usepackage{fontspec}
\usepackage{xcolor}
\setmainfont{NotoSerif}
\newfontfamily\fcmd{NotoSans}[Colour=blue]
\newcommand\cmd[1]{{\fcmd#1}}

\newcommand\trimahelptext{%
letter in the string of characters can be overprinted up to \cmd{number-of-colours=} times (max of 4) using colours \cmd{trimacolour[a-d]=} clipped from the left in increasing order with \cmd{trimawidth[a-d]=} and formatted with switches such as \cmd{\textbackslash bfseries} (bold) and \cmd{\textbackslash itshape} (italic) using \cmd{format=}. For example, the above heading's trima scheme was done with:\\\\
{\ttfamily
\textbackslash trima[\\
trimawidtha=1pt,\\
trimawidthb=2.5pt,\\
trimawidthc=5pt,\\
trimawidthd=8pt,\\
trimacoloura=red,\\
trimacolourb=blue,\\
trimacolourc=red!60!yellow,\\
trimacolourd=green,\\
]\{\} \\
}
}

    
\ExplSyntaxOn


\tl_new:N \l_trima_coloura_tl
\tl_new:N \l_trima_colourb_tl
\tl_new:N \l_trima_colourc_tl
\tl_new:N \l_trima_colourd_tl

\dim_new:N \l_trima_widtha_dim
\dim_new:N \l_trima_widthb_dim
\dim_new:N \l_trima_widthc_dim
\dim_new:N \l_trima_widthd_dim

\int_new:N \l_trima_numcolours_int

\tl_new:N \l_trima_format_tl




\keys_define:nn { trima }
  {
    trimacoloura .tl_set:N = \l_trima_coloura_tl,
    trimacolourb .tl_set:N = \l_trima_colourb_tl,
    trimacolourc .tl_set:N = \l_trima_colourc_tl,
    trimacolourd .tl_set:N = \l_trima_colourd_tl,

    trimacoloura .default:n = { \tl_set:Nn \l_trima_coloura_tl {red} },
    trimacolourb .default:n = { \tl_set:Nn \l_trima_colourb_tl {blue} },
    trimacolourc .default:n = { \tl_set:Nn \l_trima_colourc_tl {red!40!yellow} },
    trimacolourd .default:n = { \tl_set:Nn \l_trima_colourd_tl {black} },

    trimacoloura .initial:n = { \tl_set:Nn \l_trima_coloura_tl {red} },
    trimacolourb .initial:n = { \tl_set:Nn \l_trima_colourb_tl {blue} },
    trimacolourc .initial:n = { \tl_set:Nn \l_trima_colourc_tl {red!40!yellow} },
    trimacolourd .initial:n = { \tl_set:Nn \l_trima_colourd_tl {black} },

    number-of-colours .int_set:N = \l_trima_numcolours_int,
    number-of-colours .initial:n = 4,
    number-of-colours .default:n = 4,

    format .tl_set:N = \l_trima_format_tl,

    trimawidtha .dim_set:N = \l_trima_widtha_dim,
    trimawidthb .dim_set:N = \l_trima_widthb_dim,
    trimawidthc .dim_set:N = \l_trima_widthc_dim,
    trimawidthd .dim_set:N = \l_trima_widthd_dim,

    trimawidtha .default:n = 2em,
    trimawidthb .default:n = 1.5pt,
    trimawidthc .default:n = 3.0pt,
    trimawidthd .default:n = 4.0pt,

    trimawidtha .initial:n = 2em,
    trimawidthb .initial:n = 1.5pt,
    trimawidthc .initial:n = 3.0pt,
    trimawidthd .initial:n = 4.0pt,
    
    help .code:n = { \trimahelp },
    
  }





\box_new:N \l_trima_cola_box
\box_new:N \l_trima_colb_box
\box_new:N \l_trima_colc_box
\box_new:N \l_trima_cold_box

\tl_new:N \l_trima_result_tl
\tl_new:N \l_trima_text_tl


    
%--------------------------
\cs_new_protected:Npn \trima_dotrim:n #1 {

\tl_set:Nn \l_trima_text_tl { 
    \group_begin: 
        \tl_use:N \l_trima_format_tl 
        #1 
    \group_end:
}
%\tl_show:N \l_trima_coloura_tl

\hbox_set:Nn 
    \l_trima_cola_box 
    { \textcolor{\tl_use:N \l_trima_coloura_tl}{ \tl_use:N \l_trima_text_tl } }

\hbox_set:Nn 
    \l_trima_colb_box 
    { \textcolor{\tl_use:N \l_trima_colourb_tl}{ \tl_use:N \l_trima_text_tl } }
\box_set_trim:Nnnnn 
    \l_trima_colb_box 
    {\dim_use:N \l_trima_widthb_dim} {0pt} {0pt} {0pt}
\box_clip:N 
    \l_trima_colb_box

\hbox_set:Nn 
    \l_trima_colc_box { 
    \textcolor{\tl_use:N \l_trima_colourc_tl}{ \tl_use:N \l_trima_text_tl } }
\box_set_trim:Nnnnn 
    \l_trima_colc_box 
    {\dim_use:N \l_trima_widthc_dim} 
    {0pt} {0pt} {0pt}
\box_clip:N 
    \l_trima_colc_box

\hbox_set:Nn 
    \l_trima_cold_box 
    { \textcolor{\tl_use:N \l_trima_colourd_tl}{ \tl_use:N \l_trima_text_tl } }
\box_set_trim:Nnnnn 
    \l_trima_cold_box 
    {\dim_use:N \l_trima_widthd_dim} 
    {0pt} {0pt} {0pt}
\box_clip:N 
    \l_trima_cold_box


    \tl_clear:N \l_trima_result_tl
    \int_case:nnTF
        { \l_trima_numcolours_int }
        {
                { 1 } { 
                            \tl_set:Nn 
                                \l_trima_result_tl
                                {
                                    \box_use:N \l_trima_cola_box 
                                    }
                            }
                { 2 } { 
                            \tl_set:Nn 
                                \l_trima_result_tl
                                {
                            \box_use:N \l_trima_cola_box
                            \llap{\box_use:N \l_trima_colb_box} 
                                    }
                            }
                { 3 } {
                            \tl_set:Nn 
                                \l_trima_result_tl
                                {
                            \box_use:N \l_trima_cola_box
                            \llap{\box_use:N \l_trima_colb_box} 
                            \llap{\box_use:N \l_trima_colc_box} 
                                    }
                            }
                { 4 } { 
                            \tl_set:Nn 
                                \l_trima_result_tl
                                {
                            \box_use:N \l_trima_cola_box
                            \llap{\box_use:N \l_trima_colb_box} 
                            \llap{\box_use:N \l_trima_colc_box} 
                            \llap{\box_use:N \l_trima_cold_box} 
                                    }
                            }
        }
        {}{}

%   {
    
                            \tl_use:N 
                                \l_trima_result_tl
%   }

  }


\NewDocumentCommand { \trimahelp } { } {

\trima[
trimawidtha=1pt,
trimawidthb=2.5pt,
trimawidthc=5pt,
trimawidthd=8pt,
trimacoloura=red,
trimacolourb=blue,
trimacolourc=red!60!yellow,
trimacolourd=green,
]{{\rule{\textwidth}{0.5pt}}} 

\section*{\trima{Introductory \ Tutorial\ to\ \textbackslash} trima\ \trima{Command}}

    \input Acorn.fd

    \hbox_set:Nn
        \l_tmpa_box
        {
        \fontsize{60pt}{72pt}\usefont{U}{Acorn}{xl}{n}
        E
        } 
        
    \box_use:N \l_tmpa_box
    \vspace{-1.3\box_ht:N \l_tmpa_box}
    \hangindent=\dim_eval:n {0.4em+\box_wd:N \l_tmpa_box}
    \hangafter=-\fp_to_int:n { 2 + \box_ht:N \l_tmpa_box / \baselineskip }
    \noindent
    \kern-0.3em\textsc{ach}\ \trimahelptext
    \tex_par:D
    \noindent
    :\hfill\rule{0.32\textwidth}{0.5pt}\hfill :

}


\NewDocumentCommand { \trima } { o m } {


%       \tl_show:N \l_trima_colourd_tl
%   \dim_show:N \l_trima_widthc_dim

        \tl_if_novalue:nF{#1}
        {\keys_set:nn { trima } { #1 } }
        
%       \tl_show:N \l_trima_colourd_tl
        
    \tl_set:Nn \l_tmpa_tl { #2 }

    \mode_leave_vertical: 
    \tl_map_function:NN 
        \l_tmpa_tl 
        \trima_dotrim:n 
        
}

\ExplSyntaxOff

%\newcommand\trima[1]{%
%    \textcolor{red}{#1}%
%\llap{\clipbox{2.5pt 0pt 0pt 0pt}{\textcolor{blue}{#1}}}%
%\llap{\clipbox{3.0pt 0pt 0pt 0pt}{\textcolor{red!40!yellow}{#1}}}%
%\llap{\clipbox{4.0pt 0pt 0pt 0pt}{\textcolor{black}{#1}}}%
%}

\begin{document}

\large
a 
\trima[
trimacoloura=red,
trimacolourb=blue,
trimacolourc=red!40!yellow,
trimacolourd=black,
number-of-colours=4,
trimawidtha,
trimawidthb,
trimawidthc,
trimawidthd,
format=\itshape,
]{abxyz}
\trima{abxyz}

\trima[number-of-colours=2,
]{QWERTY}


\trima{uiop}
\trima[
number-of-colours=4,
trimacolourd=green,%!60!yellow!40,
]{qazdxc}
\trima[
format=\Huge,
]{Q}

\trima[format=\normalsize,
]{{\rule{1.5em}{1em}}} 
The 
\trima[
trimacoloura=black,
trimacolourb=black,
trimacolourc=black,
trimacolourd=black,
format=\itshape\bfseries,
]{cat} 
sat on the 
\trima{mat}.

\trima[
trimawidtha=1pt,
trimawidthb=2.5pt,
trimawidthc=5pt,
trimawidthd=8pt,
trimacoloura=red,
trimacolourb=blue,
trimacolourc=red!60!yellow,
trimacolourd=green,
]{{\rule{1.5em}{1em}}} 

\section{\trima{Introduction}}

\trima[help]{}


\end{document}
5
  • Doesn't this make for unnecessary obscurity in the code?
    – cfr
    Commented Sep 25, 2023 at 21:03
  • @cfr Both "Probably not." and "Totally, after 5 minutes." :) For structure, the use-case is libraries of sets of sets of information (linguistic glosses, legal referencing, biological, etc), with, say, user-defined legal citation formatting style(s) for the legal materials, all done in a Biblatex datamodel etc. (I also tend to separate code from documentation/tutorials/walkthroughs etc, if that helps.) Not enough space for everything! For names, they're somewhat algebraic, true: "Let the parameter #1 be the data structure d ...".
    – Cicada
    Commented Sep 26, 2023 at 6:05
  • In truth, it reminds me of aspects of prooftrees's cross-referencing system ;).
    – cfr
    Commented Sep 26, 2023 at 6:14
  • 2
    Please, don't use \cs_new:N(p)n for function doing assignments. You must use \cs_new_protected:N(p)n.
    – egreg
    Commented Sep 28, 2023 at 14:33
  • @egreg Corrected.
    – Cicada
    Commented Sep 30, 2023 at 11:52

You must log in to answer this question.

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