2

I'm trying to create an option with l3keys such that, if it receives a value, even if an empty one, it gets added to a property list, but if it receives no value, it gets removed from the property list.

However, I don't seem to be able do distinguish key from key= in a .code:n "action". I know that .default:n captures that situation, but it cannot receive a code block, just a value. I also know that I can distinguish the situation in \keyval_parse:nnn, but I'd like this behavior in some options among the whole set, and using \keyval_parse:nnn for the whole thing would become complicated.

The MWE illustrates the situation:

\documentclass{article}

\ExplSyntaxOn
\prop_new:N \l__myprops_prop
\keys_define:nn { options }
  {
    mykey .code:n =
      {
        \tl_if_novalue:nTF {#1}
          { \prop_remove:Nn \l__myprops_prop { mykey } }
          { \prop_put:Nnn \l__myprops_prop { mykey } {#1} }
        \prop_show:N \l__myprops_prop
      } ,
    mykey2 .code:n =
      {
        \tl_if_empty:nTF {#1}
          { \prop_remove:Nn \l__myprops_prop { mykey2 } }
          { \prop_put:Nnn \l__myprops_prop { mykey2 } {#1} }
        \prop_show:N \l__myprops_prop
      }
  }
\ExplSyntaxOff

\begin{document}

\ExplSyntaxOn
\keys_set:nn { options } { mykey }
\keys_set:nn { options } { mykey= }
\keys_set:nn { options } { mykey=val }
\prop_clear:N \l__myprops_prop
\keys_set:nn { options } { mykey2 }
\keys_set:nn { options } { mykey2= }
\keys_set:nn { options } { mykey2=val }
\ExplSyntaxOff

\end{document}

The log for this document contains:

The property list \l__myprops_prop contains the pairs (without outer braces):
>  {mykey}  =>  {}.
<recently read> }
                 
l.27 \keys_set:nn { options } { mykey }
                                       
The property list \l__myprops_prop contains the pairs (without outer braces):
>  {mykey}  =>  {}.
<recently read> }
                 
l.28 \keys_set:nn { options } { mykey= }
                                        
The property list \l__myprops_prop contains the pairs (without outer braces):
>  {mykey}  =>  {val}.
<recently read> }
                 
l.29 \keys_set:nn { options } { mykey=val }
                                           
The property list \l__myprops_prop is empty
> .
<recently read> }
                 
l.31 \keys_set:nn { options } { mykey2 }
                                        
The property list \l__myprops_prop is empty
> .
<recently read> }
                 
l.32 \keys_set:nn { options } { mykey2= }
                                         
The property list \l__myprops_prop contains the pairs (without outer braces):
>  {mykey2}  =>  {val}.
<recently read> }
                 
l.33 \keys_set:nn { options } { mykey2=val }

Which shows that neither key nor key= pass the "no value" test, and both key and key= pass the emptiness test. Unfortunately, I cannot use emptiness to "signal" the removal, since "empty" is a valid value for the key in question. Is indeed \keyval_parse:nnn the only choice here?

5
  • Unfortunately, it is not possible with \keys_set:nn (using documented interfaces). There is a private boolean variable that holds the value/no-value state, but it's not used when the .code:n handler is used. It would be nice to distinguish these cases, but I don't think a change is feasible at this point... Sep 9, 2021 at 0:02
  • Hi @PhelypeOleinik, I feared as much... :-( I'll have to rethink this here then. From the OPs perspective, your comment is a full answer. If you'd like to make it one, I'd be glad to accept it. Either way, thank you!
    – gusbrs
    Sep 9, 2021 at 0:20
  • Conceptually, l3keys works on the idea that keys are properties, and thus there is no such thing as 'not having a value' for any key - once you create a key, it always has some value even if that value is 'empty'.
    – Joseph Wright
    Sep 9, 2021 at 17:14
  • @JosephWright Thanks for the comment. What I'm trying to do is to get different behaviors for key and key= in a .code:n handler. So I might have expressed myself poorly, but since \keyval_parse:nnn recognizes this difference, I was hoping to grab that piece of information in \keys_set:nn too. And indeed, Skillmon provided a way which seems to be the proper one to do it.
    – gusbrs
    Sep 9, 2021 at 17:23
  • @JosephWright Let me see if I get what you meant correctly. Are you saying that, even though I can technically recognize the difference between key and key=, I should not use that distinction at the user level?
    – gusbrs
    Sep 9, 2021 at 17:42

2 Answers 2

2

This builds on the same basic idea as @Phelype's answer, namely use \c_novalue_tl as a marker, but instead of wrapping \keyval_parse:nnn around \keys_set:nn this uses the .default:x handler to set the default value to \c_novalue_tl:

\documentclass{article}

\ExplSyntaxOn
\prop_new:N \l__myprops_prop
\cs_new_protected:Npn \__myprops_handle:n #1
  {
    \tl_if_novalue:nTF {#1}
      { \prop_remove:NV \l__myprops_prop \l_keys_key_str }
      { \prop_put:NVn   \l__myprops_prop \l_keys_key_str {#1} }
    \prop_show:N \l__myprops_prop
  }
\keys_define:nn { options }
  {
     mykey  .code:n = \__myprops_handle:n {#1}
    ,mykey  .default:x = \c_novalue_tl
    ,mykey2 .code:n = \__myprops_handle:n {#1}
    ,mykey2 .default:x = \c_novalue_tl
  }
\ExplSyntaxOff

\begin{document}

\ExplSyntaxOn
\keys_set:nn { options } { mykey }
\keys_set:nn { options } { mykey= }
\keys_set:nn { options } { mykey=val }
\prop_clear:N \l__myprops_prop
\keys_set:nn { options } { mykey2 }
\keys_set:nn { options } { mykey2= }
\keys_set:nn { options } { mykey2=val }
\ExplSyntaxOff

\end{document}
10
  • Oh, wow! We can have both .code: and .default: and .code: is executed after .default:. That's clever! Thank you! How did you find that out?
    – gusbrs
    Sep 9, 2021 at 10:35
  • 1
    @gusbrs knowing much of the implementation helps :P But more seriously, take a look at the documentation in texdoc interface3, the description of the .default:n property is: "Creates a <default> value for <key>, which is used if no value is given. This will be used if only the key name is given, but not if a blank <value> is given: [code example]" The code example covers exactly your use case (well, not the property setting, but using the .code:n property). The .default:n property does nothing by itself, it just sets a default value used with whichever other property is defined.
    – Skillmon
    Sep 9, 2021 at 10:51
  • :-) Well, I had. Currently texdoc interface3 is deserving a dedicated keybinding here. And I was acquainted with the documentation of .default: in particular. I just didn't figure it out it could be used alongside .code:, much less of their execution order. The docs do say that some of the "actions"/handlers are mutually exclusive, but just gives some clear examples, not an exhaustive list. And I'd be willing to bet there's no word about .code: coming after .default:. I guess it takes "right eyes" to see it. :-) Thanks once again!
    – gusbrs
    Sep 9, 2021 at 11:18
  • "The code example covers exactly your use case". In hindsight, indeed it does. Damn, it is hard to have a thick skull! ;-)
    – gusbrs
    Sep 9, 2021 at 11:33
  • 1
    @gusbrs quick list of exclusive handlers: Every one except .default:n, .initial:n, .groups:n. Additionally .value_required:n and .value_forbidden:n are only mutually exclusive to each other and not to any other handler. (I hope I didn't forget something here...)
    – Skillmon
    Sep 9, 2021 at 11:41
3

There is no built-in way to distinguish mykey from mykey={} when it's declared with the .code:n handler. Internally these cases are different, and there is a boolean variable that keeps track of that, but all using private variables from l3keys.

What you can do using the available interfaces is use \keyval_parse:nnn, as you suggested, to differentiate both cases, and then when mykey is used, turn that into mykey = \c_novalue_tl, then the code in your key can detect that using \tl_if_novalue:nTF. This makes the parsing a bit slower than just using \keys_set:nn because some common bookkeeping code in \keys_set:nn is executed more often, but that's likely minimal.

The example below implements \gusbrs_keys_set:nn, which passes \c_novalue_tl to a key if no value is given:

\documentclass{article}

\ExplSyntaxOn
\cs_new_protected:Npn \gusbrs_keys_set:nn #1 #2
  {
    \keyval_parse:nnn
      { \__gusbrs_keys_set:nn {#1} }
      { \__gusbrs_keys_set:nnn {#1} }
        {#2}
  }
\cs_new_protected:Npn \__gusbrs_keys_set:nn #1 #2
  { \use:x { \keys_set:nn {#1} { #2 = \c_novalue_tl } } }
\cs_new_protected:Npn \__gusbrs_keys_set:nnn #1 #2 #3
  { \keys_set:nn {#1} { #2 = #3 } }
\ExplSyntaxOff


\ExplSyntaxOn
\prop_new:N \l__myprops_prop
\keys_define:nn { options }
  {
    mykey .code:n =
      {
        \tl_if_novalue:nTF {#1}
          { \prop_remove:Nn \l__myprops_prop { mykey } }
          { \prop_put:Nnn \l__myprops_prop { mykey } {#1} }
        \prop_show:N \l__myprops_prop
      } ,
  }
\ExplSyntaxOff

\begin{document}

\ExplSyntaxOn
\gusbrs_keys_set:nn { options } { mykey }
\gusbrs_keys_set:nn { options } { mykey= }
\gusbrs_keys_set:nn { options } { mykey=val }
\ExplSyntaxOff

\end{document}

This is printed on the terminal:

The property list \l__myprops_prop is empty
> .
<recently read> }
                 
l.35 \gusbrs_keys_set:nn { options } { mykey }
                                              
? 
The property list \l__myprops_prop contains the pairs (without outer braces):
>  {mykey}  =>  {}.
<recently read> }
                 
l.36 \gusbrs_keys_set:nn { options } { mykey= }
                                               
? 
The property list \l__myprops_prop contains the pairs (without outer braces):
>  {mykey}  =>  {val}.
<recently read> }
                 
l.37 \gusbrs_keys_set:nn { options } { mykey=val }
                                                  
?
13
  • Ah, interesting approach! Thank you very much!
    – gusbrs
    Sep 9, 2021 at 1:06
  • But, one question, what happens if I have other keys besides mykey and some of them receive no value (they either have a .default:n or are .meta:n action with value forbidden), what will happen to them?
    – gusbrs
    Sep 9, 2021 at 1:14
  • @gusbrs Hm... Is “won't work” an acceptable answer? ;-) Sorry, I didn't think of that. Maybe something with \keys_set_groups:nnn, and placing mykey (and other similar keys) in a separate group might work (guessing). It's getting late, so I'll give it a try tomorrow, if you haven't managed by then Sep 9, 2021 at 1:21
  • It is. ;-) The technical question is interesting, of course, but in practical terms, I think I'm likely approaching the problem from the wrong side. I have probably to reconsider how I'm organizing the UI. Possibly I'm overdoing the "user flexibility" side of things. Just so that I don't leave you in the dark, I have "translations" and an overriding value for "any language" which goes in this property. Once set globally, there is currently no way to unset it, only to set it to another value. But, does it need to be? Anyway, a problem for me to solve. Thanks again!
    – gusbrs
    Sep 9, 2021 at 1:27
  • 1
    @PhelypeOleinik yay :) But I think making the boolean public that stores whether a value was present or not would do no harm, not cost anything (well, 5 lines of documentation), but give additional options to users.
    – Skillmon
    Sep 9, 2021 at 15:45

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.