expl3 – Using lthooks to Locally Hook Into Commands: A Detailed Approach

amsthmexpansionexpl3hookslthooks

Following a previous question of mine, as an exercise I'm trying to mimic the behavior of thmtools with amsthm using l3keys and lthooks. Now I am stuck on the hooks part. thmtools defines prehead, posthead, prefoot, and postfoot hooks both for each theorem environment and generically (applied to all theorems). The prehead and postfoot hooks are easy. For the posthead hook, I've figured out that the code should be added after the amsthm command \deferred@thm@head. (Or perhaps \@begintheorem? The difference is an \ignorespaces.)

For the generic hook, one can just do

\NewHook{ amsthm-keys/allthms/posthead }
\AddToHook { cmd/deferred@thm@head/after } { \UseHook { amsthm-keys/allthms/posthead } }

But for the local hooks, I only want to add code after \deferred@thm@head for that specific environment. As far as I can tell, adding to hooks is always global, so I don't see how to do this without possibly changing the definition of \deferred@thm@head.

Here's a MWE:

\documentclass{article}
\usepackage{amsthm,kantlipsum,tcolorbox}

\ExplSyntaxOn

\keys_define:nn { mbert/thm }
  {
    name         .tl_set:N = \l_mbert_thm_name_tl,
    preheadhook  .tl_set:N = \l_mbert_thm_preheadhook_tl,
    postheadhook .tl_set:N = \l_mbert_thm_postheadhook_tl,
    prefoothook  .tl_set:N = \l_mbert_thm_prefoothook_tl,
    postfoothook .tl_set:N = \l_mbert_thm_postfoothook_tl,
  }

\tl_new:N \l_mbert_thm_defaultkeys_tl
\keys_precompile:nnN { mbert/thm }
  {
    name         = \text_titlecase:n { \l_mbert_thm_envname_tl },
    preheadhook  = {},
    postheadhook = {},
    prefoothook  = {},
    postfoothook = {},
  }
  \l_mbert_thm_defaultkeys_tl

\NewHook{ amsthm-keys/allthms/prehead }
\NewHook{ amsthm-keys/allthms/posthead }
\NewHook{ amsthm-keys/allthms/prefoot }
\NewHook{ amsthm-keys/allthms/postfoot }

\AddToHook { cmd/deferred@thm@head/after } { \UseHook { amsthm-keys/allthms/posthead } }
% How to hook into \deferred@thm@head locally?

\NewDocumentCommand { \NewThm } { m O{} }
  { 
    \tl_set:Nn \l_mbert_thm_envname_tl { #1 }
    \tl_use:N \l_mbert_thm_defaultkeys_tl
    \keys_set:nn { mbert/thm } { #2 }
    \exp_args:NnV \AddToHook { env/#1/begin } \l_mbert_thm_preheadhook_tl % local prehead hook
    \AddToHook { env/#1/begin } { \UseHook { amsthm-keys/allthms/prehead } } % generic prehead hook
    \AddToHook { env/#1/end } { \UseHook { amsthm-keys/allthms/postfoot } } % generic postfoot hook
    \exp_args:NnV \AddToHook { env/#1/end } \l_mbert_thm_postfoothook_tl % local postfoot hook
    \mbert_thm_new:ne { #1 } { \l_mbert_thm_name_tl }
  }

\cs_new_eq:NN \mbert_thm_new:nn \newtheorem
\cs_generate_variant:Nn \mbert_thm_new:nn { ne }

\ExplSyntaxOff

\NewThm{theorem}[
    preheadhook=\begin{tcolorbox},
    postfoothook=\end{tcolorbox}
    ]
\NewThm{cor}[
    name=Corollary,
    postheadhook=ABC
    ]
\NewThm{lemma}

\AddToHook{amsthm-keys/allthms/posthead}{***}
\AddToHook{amsthm-keys/allthms/postfoot}{END THEOREM}

\begin{document}

\begin{theorem}
\kant[2][1]
\end{theorem}

\begin{cor}
\kant[2][1]
\end{cor}

\begin{lemma}
\kant[2][1]
\end{lemma}

\end{document}

enter image description here

I obviously can't add something like

\exp_args:NnV \AddToHook { cmd/deferred@thm@head/after } \l_mbert_thm_postheadhook_tl

to the definition of \NewThm as then each declaration of postheadhook= would add code to \deferred@thm@head.

Essentially I want the effect of

\AddToHook{env/theorem/begin}{\apptocmd{\deferred@thm@head}{CODE}{}{}}

but using the kernel hooks. From this answer, I know there's no direct way to get the behavior of \apptocmd. Is there general advice for locally hooking into commands from other packages like this?


Additional attempt

With @cfr's help in the comments, the idea of nesting \AddToHookNext{cmd/deferred@thm@head/after} inside \AddToHook{env/<envname>/begin} seems promising. Indeed, adding e.g.

\AddToHook { env/cor/begin } { \AddToHookNext { cmd/deferred@thm@head/after } {HHH} }

has the desired effect. However, when I try to make this automatic in the definition of \NewThm, nothing happens when postheadhook is given a value:

\NewDocumentCommand { \NewThm } { m O{} }
  { 
    \tl_set:Nn \l_mbert_thm_envname_tl { #1 }
    \tl_use:N \l_mbert_thm_defaultkeys_tl
    \keys_set:nn { mbert/thm } { #2 }
    \exp_args:NnV \AddToHook { env/#1/begin } \l_mbert_thm_preheadhook_tl % local prehead hook
    \AddToHook { env/#1/begin } { \UseHook { amsthm-keys/allthms/prehead } } % generic prehead hook
%%% This next line is new.
    \AddToHook { env/#1/begin } { \exp_args:NnV \AddToHookNext { cmd/deferred@thm@head/after } \l_mbert_thm_postheadhook_tl }
    \AddToHook { env/#1/end } { \UseHook { amsthm-keys/allthms/postfoot } } % generic postfoot hook
    \exp_args:NnV \AddToHook { env/#1/end } \l_mbert_thm_postfoothook_tl % local postfoot hook
    \mbert_thm_new:ne { #1 } { \l_mbert_thm_name_tl }
  }

Is it perhaps an expansion issue?

Yet another attempt

I don't understand why, but my above attempt seems to work if I prefix the added line with \exp_args:Nnf, as in

\exp_args:Nnf \AddToHook { env/#1/begin } { \exp_args:NnV \AddToHookNext { cmd/deferred@thm@head/after } \l_mbert_thm_postheadhook_tl }

o-type expansion does nothing, and e-type expansion produces an error if postheadhook contains something other than pure text. Just lucky, or is this how to use f-type expansion?

Best Answer

But for the local hooks, I only want to add code after \deferred@thm@head for that specific environment. As far as I can tell, adding to hooks is always global, so I don't see how to do this without possibly changing the definition of \deferred@thm@head

[…]

Essentially I want the effect of

\AddToHook{env/theorem/begin}{\apptocmd{\deferred@thm@head}{CODE}{}{}}

but using the kernel hooks.

Here are 5 different options:

\documentclass{article}
\pagestyle{empty}

\makeatletter \ExplSyntaxOn
    \def\pretext#1{before <#1>}
    \def\posttext#1{after <#1>}

    \NewDocumentEnvironment
        { one }
        { }
        { \pretext{one} }
        { \posttext{one} }

    \NewDocumentEnvironment
        { two }
        { }
        { \pretext{two} }
        { \posttext{two} }


    %%%%%%%%%%%%%%%%
    %%% Option 1 %%%
    %%%%%%%%%%%%%%%%
    %% Use \globaldefs to trick \hook_gput_code into actually behaving like a
    %% hypothetical \hook_put_code. This is a complete hack and wildly
    %% unsupported, so PLEASE DON'T ACTUALLY DO THIS.
    %
    % \hook_gput_code:nnn { env / two / begin } { . } {
    %     \PackageWarning{BAD~ IDEA!}{PLEASE~ DON'T~ ACTUALLY~ DO~ THIS!}
    %     \int_set:Nn \globaldefs { -1 }
    %     \hook_gput_code:nnn { cmd / pretext / after } { . } { \textbf { NEW } }
    %     \int_set:Nn \globaldefs { 0 }
    % }
    %%% End Option 1


    %%%%%%%%%%%%%%%%
    %%% Option 2 %%%
    %%%%%%%%%%%%%%%%
    %% Add the patch using a "cmd/..." hook at the beginning of the
    %% environment, and remove it at the end.
    %
    % \hook_gput_code:nnn { env / two / begin } { . } {
    %     \hook_gput_code:nnn { cmd / pretext / after } { . } { \textbf { NEW } }
    % }
    %
    % \hook_gput_code:nnn { env / two / end } { . } {
    %     \hook_gremove_code:nn { cmd / pretext / after } { . }
    % }
    %%% End Option 2


    %%%%%%%%%%%%%%%%
    %%% Option 3 %%%
    %%%%%%%%%%%%%%%%
    %% Add a one-time "cmd/..." hook at the beginning of the environment.
    %
    % \hook_gput_code:nnn { env / two / begin } { . } {
    %     \hook_gput_next_code:nn { cmd / pretext / after } { \textbf { NEW } }
    % }
    %%% End Option 3


    %%%%%%%%%%%%%%%%
    %%% Option 4 %%%
    %%%%%%%%%%%%%%%%
    %% Locally patch the command by directly using expl3 commands.
    %
    % \cs_generate_variant:Nn \cs_set:Npn { NpV }
    %
    % \cctab_const:Nn \c_package_cctab {
    %     \cctab_select:N \c_document_cctab
    %     \char_set_catcode_letter:N @
    % }
    %
    % \hook_gput_code:nnn { env / two / begin } { . } {
    %     \tl_set_rescan:Nnx
    %         \l_tmpa_tl
    %         { \cctab_select:N \c_package_cctab }
    %         { \cs_replacement_spec:N \pretext }
    %
    %     \tl_put_right:Nn \l_tmpa_tl { \textbf { NEW } }
    %     \cs_set:NpV \pretext #1 { \l_tmpa_tl }
    % }
    %%% End Option 4


    %%%%%%%%%%%%%%%%
    %%% Option 5 %%%
    %%%%%%%%%%%%%%%%
    %% Globally patch the command, but make the patch value depend on the current
    %% environment.
    %
    % \prop_new:N \g__example_aftertext_prop
    %
    % \hook_gput_code:nnn { cmd / pretext / after } { . } {
    %     \prop_item:NV \g__example_aftertext_prop \@currenvir
    % }
    %
    % \prop_gput:Nnn \g__example_aftertext_prop { two } { \textbf { NEW } }
    %%% End Option 5
\ExplSyntaxOff \makeatother

\begin{document}
    \begin{one}
        \emph{body}
    \end{one}

    \begin{two}
        \emph{body}
    \end{two}

    \begin{one}
        \emph{body}
    \end{one}

    \begin{two}
        \emph{body}
    \end{two}
\end{document}

sample output

I don't understand why, but my above attempt seems to work if I prefix the added line with \exp_args:Nnf, as in

\exp_args:Nnf \AddToHook { env/#1/begin } { \exp_args:NnV \AddToHookNext { cmd/deferred@thm@head/after } \l_mbert_thm_postheadhook_tl }

o-type expansion does nothing, and e-type expansion produces an error if postheadhook contains something other than pure text. Just lucky, or is this how to use f-type expansion?

e-type expansion will fail if postheadhook contains anything fragile. o-type expansion doesn't work because it expands exactly once, and \exp_args:NnV takes way more expansions than that to work.

f-type expansion is inappropriate 99% of the time, but it seems fine here. I'd slightly prefer

\exp_args:Nnf \AddToHook { env/#1/begin } {
    %          v   vvvvvvvvvvvv
    \exp_args:NNnV \exp_stop_f: \AddToHookNext { cmd/deferred@thm@head/after }
    \l_mbert_thm_postheadhook_tl
}

just in case \AddToHookNext were to blow up when expanded, but it (and most other LaTeX commands) are protected, so this doesn't really make a difference in this case.

(What would be best would be something like \hook_gput_code:nne … \hook_gput_next_code:ne … \exp_not:V \l_mbert_thm_postheadhook_tl, but that throws an error for some unknown reason.)