7

I am trying out l3keys for the first time and I am encountering the following issue. I have some boolean keys (true/false). For them it looks nice to use .bool_set:N so that I can get automatic validation of the value passed into the key and I also get the key value nicely stored in a boolean variable for later inspection.

At the same time, I would like to execute some code right after the variable is set. However, it looks like .code:n and bool_set:N cannot be used together on the same key, because the one that comes later substitutes the action of the first. In practice, there seems to be no possibility to run a hook triggered by the variable setting action.

Obviously, there are several possible workarounds:

  • run some code after every key_set:nn.
    However, this means that such code needs to check for every possible variable being set to see if it has changed and take action accordingly. If you have say 30 variables, this means checking all of them, when maybe only one of them is being changed. Having a hook invoked just for the variable that has been set seems more appropriate as long as the variables do not interact one with the other.

  • use .code:n instead of the variable setting actions.
    However, this means redoing the parsing and validation of the value.

Because all the workarounds seem to have inefficiencies of their own, I wonder if I am missing something... Any help is appreciated!

4

This is not a real solution, more like a viable workaround:

You could create a .meta:n key, that is the documented front-facing key, and two additional keys, one for the .bool_set:N, and one for the .code:n:

\documentclass[]{article}

\usepackage{xparse}
\ExplSyntaxOn
\keys_define:nn { Callegar }
  {
    my-bool .meta:n = { my-bool-bool = { #1 }, my-bool-code = { #1 } },
    my-bool-bool .bool_set:N = \l_Callegar_bool,
    my-bool-code .code:n =
      { Execute~Some~Code~for~\bool_if:NTF \l_Callegar_bool { true } { false } }
  }
\NewDocumentCommand \setmykeys { m }
  {
    \keys_set:nn { Callegar } { #1 }
  }
\ExplSyntaxOff

\begin{document}
\setmykeys{my-bool=true}
\end{document}
2

The following redefines an internal function of the l3keys module and might therefore break stuff in the future. Use with caution.

After digging a bit through the l3keys code, I thought one could add a hook to the keys. The following does so by adding a hook mechanism to \__keys_cmd_set:nn. After that one can define a hook with:

\keys_define:nn { <module> } { <key> .hook:n = { <hook code> } }

I didn't test it thoroughly, but as far as a quick view l3keys could tell, the hook mechanism should work with any key type. Note that it doesn't create any key on its own, so you have to use it on an already defined key.

\documentclass[]{article}

\usepackage{xparse}
\ExplSyntaxOn
% redefine a part of the internals of l3keys
\cs_set_protected:Npn \__keys_cmd_set:nn #1#2
  {
    \cs_set_protected:cpn { \c__keys_code_root_tl #1 } ##1
      { #2 \use:c { \c__keys_code_root_tl #1 _hook } }
  }
% add the .hook:n type
\cs_new_protected:cpn { \c__keys_props_root_tl .hook:n } #1
  {
    \tl_gset:cn { \c__keys_code_root_tl \l_keys_path_tl _hook } { #1 }
  }

\keys_define:nn { Callegar }
  {
    my-fool .bool_set:N = \l_Callegar_bool,
    my-fool .hook:n = { Execute~Some~Code~Regardless },
    my-fool / true .hook:n =
      { Execute~Some~Hook~Code~for~true },
    my-fool / false .hook:n =
      { Execute~Some~Hook~Code~for~false },
  }
\NewDocumentCommand \setmykeys { m }
  {
    \keys_set:nn { Callegar } { #1 }
  }
\ExplSyntaxOff

\begin{document}
\setmykeys{my-fool=true}

\setmykeys{my-fool=false}
\end{document}

enter image description here

A slightly different implementation that also allows .hook:n to access the value:

\cs_set_protected:Npn \__keys_cmd_set:nn #1#2
  {
    \cs_set_protected:cpn { \c__keys_code_root_tl #1 } ##1
      {
        #2
        \cs_if_exist_use:cT { \c__keys_code_root_tl #1 _hook } { { ##1 } }
      }
  }
% add the .hook:n type
\cs_new_protected:cpn { \c__keys_props_root_tl .hook:n } #1
  {
    \cs_set:cpn { \c__keys_code_root_tl \l_keys_path_tl _hook } ##1 { #1 }
  }

\keys_define:nn { Callegar }
  {
    my-fool .bool_set:N = \l_Callegar_bool,
    my-fool .hook:n = { Execute~Some~Code~Regardless~'#1' },
    my-fool / true .hook:n =
      { Execute~Some~Hook~Code~for~true },
    my-fool / false .hook:n =
      { Execute~Some~Hook~Code~for~false },
  }
  • Thanks Skillmon, that's interesting although by being a new on l3keys its hard for me to read its code to understand how you are touching its internals. If I get it correctly, \__keys_cmd_set:nn is the internal used to associate code to the keys, 1st argument is the key path and 2nd one is the code itself, is this the OK? So you are imposing that after the regular code the hook code is used, is this correct? What puzzles me a bit is the role of all those constants. Furthermore, the variant is a little obscure to me in how it passes ##1 to the hook. Could you help me understand? – Callegar Dec 18 '18 at 16:39
  • @Callegar yes, \__keys_cmd_set:nn is the internal to actually define a key and most/all key types are using it internally. All those constants are internals of l3keys, too, which are supposed to assure no naming conflicts.The variant checks whether the hook has been defined (\cs_if_exist_use:cT), and if it does it inputs the control sequence and the next argument, stripping one pair of braces. So if \foo_hook exists, \cs_if_exist_use:cT { foo_hook } { { ##1 } } expands to \foo_hook { ##1 }, and \foo_hook can absorb that argument. – Skillmon Dec 18 '18 at 17:48

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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