Commands defined with \NewDocumentCommand behave in an indirect way; the situation is similar to the one addressed by letltxmacro when dealing with commands defined with \DeclareRobustCommand, which is easier to describe.
When you do
\DeclareRobustCommand{\foo}[1]{...#1...}
LaTeX actually defines two macros, in a way that's essentially
\edef\foo{\noexpand\protect\expandafter\noexpand\csname foo \endcsname}
\expandafter\newcommand\csname foo \endcsname[1]{...#1...}
Note the trailing space in the name of the macro that does the real job. So, if you naively do
\let\origfoo\foo
\DeclareRobustCommand{\foo}[1]{\origfoo{#1}?}
the call \foo{x} in normal text, where \protect is \relax, would give, in succession, (using • to denote the trailing space in macro names having it)
\protect\foo•{x} % first level expansion
\foo•{x} % \relax disappears
\origfoo{x}? % replacement text of redefined \foo•
\protect\foo•{x}? % first level expansion of \origfoo
\foo•{x}? % \relax disappears
Yikes! Infinite loop!
If you try
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand \example { o m }
{
#1 ~ #2
}
\cs_show:N \example
the terminal output would be (reformatted for better reading)
> \example=\protected macro:->\int_zero:N \l__xparse_processor_int
\tl_set:Nn \l__xparse_args_tl {\example code }
\tl_set:Nn \l__xparse_fn_tl {\example }
\__xparse_grab_D:w []{-NoValue-}
\__xparse_grab_m_1:w \l__xparse_args_tl .
So when you do \let\origexample\example you just make it the same as this macro, but a subsequent
\RenewDocumentCommand{\example}{...}{...}
would change the meaning of \example and of \example•code (again with a space in the name) which is actually the macro doing the real work. The same kind of infinite loop would arise, because the newly defined \example•code macro will contain a call of \origexample that will eventually call \example•code. Infinite loop.
Building \LetDocumentCommand is not conceptually difficult, nor it is adding such commands to the ones managed by xpatch or regexpatch, since they follow a common pattern just like \DeclareRobustCommand.
However the situation is very different: the main reason is that the way macros defined with \NewDocumentCommand work is not cast in stone. It could change abruptly if the team decides so: the usage of “private” functions with __ in their name means exactly that no package developer should rely on this particular implementation.
The situation with \DeclareRobustCommand is different, because the LaTeX2e kernel publishes the interface (and indeed some packages exploiting this exist, not only letltxmacro and xpatch).
The reasons why \LetDocumentCommand will not be provided by xparse have been explained by Joseph Wright. You now have the tools for managing it, if you dare. I won't. ;-)
xparse-defined commands should be interfaces to documented code-level functions. Thus if you need a different interface the correct way is to define a new command from scratch. We are of course still working on getting everything working this way! – Joseph Wright Jan 02 '16 at 10:58siunitx. The\SImacro for instance calls to\__siunitx_…functions. – Henri Menke Jan 02 '16 at 11:01siunitxv2 before we'd got all of thexparsestuff revised (and indeed part of the point of writingsiunitxinexpl3was to push development of the latter). I'm well-aware that it needs revising to have defined interfaces, and the development version (https://github.com/josephwright/siunitx/tree/master) is going that way. (It's hard work as there are many things I would probably not do, with hindsight, but can't break existing documents.) – Joseph Wright Jan 02 '16 at 11:02siunitxyou would have to model new interfaces using 'internal' code functions (regrettably). However, I assume the question is general and we are aiming at best-practice (which is _still developing). – Joseph Wright Jan 02 '16 at 11:05\LetDocumentCommandinxparse, but I became aware of that when I wanted to overwrite\numfromsiunitx. – Henri Menke Jan 02 '16 at 11:07letltxmacro(which cannot be used here, though). – egreg Jan 02 '16 at 11:07\LetDocumentCommandas it misses the entire point ofxparse. I'll add an answer to this effect. – Joseph Wright Jan 02 '16 at 11:10