To answer my own question, using only expandable macros and avoiding the use of \prg_new_conditional:Npnn the following example just works fine and is flexible in the way that all used tests can be adapted to specific problems and do not rely on predefined code like \__int_to_roman:w suggested by egreg.
\documentclass{article}
\usepackage{expl3}
\begin{document}
\ExplSyntaxOn
\bool_new:N \true_bool
\bool_new:N \false_bool
\bool_gset_true:N \true_bool
\bool_gset_false:N \false_bool
\prg_new_conditional:Npnn \is_digit:N #1 { TF }
{
\bool_if:nTF
{
\token_if_eq_charcode_p:NN 0 #1 ||
\token_if_eq_charcode_p:NN 1 #1 ||
\token_if_eq_charcode_p:NN 2 #1 ||
\token_if_eq_charcode_p:NN 3 #1 ||
\token_if_eq_charcode_p:NN 4 #1 ||
\token_if_eq_charcode_p:NN 5 #1 ||
\token_if_eq_charcode_p:NN 6 #1 ||
\token_if_eq_charcode_p:NN 7 #1 ||
\token_if_eq_charcode_p:NN 8 #1 ||
\token_if_eq_charcode_p:NN 9 #1
}{
\prg_return_true:
}{
\prg_return_false:
}
}
\cs_new:Npn \is_integer_p:n #1
{
\tl_if_empty:nTF { #1 }
{
% We are done if the token list is empty
\bool_if_p:n { \true_bool }
}{
\exp_args:Nf \is_digit:NTF { \tl_head:n { #1 } }
{
\exp_args:Nf \is_integer_p:n { \tl_tail:n { #1 } }
}{
\bool_if_p:n { \false_bool }
}
}
}
\bool_if:nTF { \is_integer_p:n { 1234 } } { true } { false }
\ExplSyntaxOff
\end{document}
Interestingly, trying the same with \prg_new_conditional:Npnn doesn't seem to be possible. The following throws errors, although I don't understand why. I would assume that both implementations result in equivalent code, but apparently this isn't true.
\prg_new_conditional:Npnn \is_integer_prg:n #1 { p }
{
\tl_if_empty:nTF { #1 }
{
% We are done if the token list is empty
\prg_return_true:
}{
\exp_args:Nf \is_digit:NTF { \tl_head:n { #1 } }
{
\exp_args:Nf \is_integer_prg_p:n { \tl_tail:n { #1 } }
}{
\prg_return_false:
}
}
}
Maybe somebody can clarify, why the \prg_new_conditional:Npnn solution doesn't work.
Edit: Eventually I figured out how to do the trick with \prg_new_conditional:Npnn. The problem in my previous attempt was that a predicate conditional is no legal "return value" of \prg_new_conditional:Npnn. This means that
\prg_new_conditional:Npnn \foo: { p }
{
\int_compare_p:n { 1 = 2 }
}
fails, while
\prg_new_conditional:Npnn \foo: { p }
{
\int_compare:nTF { 1 = 2 } { \prg_return_true: } { \prg_return_false: }
}
works, which of course makes sense in hindsight. So the above attempt can be corrected to the following working example:
\documentclass{article}
\usepackage{expl3}
\begin{document}
\ExplSyntaxOn
\prg_new_conditional:Npnn \is_digit:N #1 { TF }
{
\bool_if:nTF
{
\token_if_eq_charcode_p:NN 0 #1 ||
\token_if_eq_charcode_p:NN 1 #1 ||
\token_if_eq_charcode_p:NN 2 #1 ||
\token_if_eq_charcode_p:NN 3 #1 ||
\token_if_eq_charcode_p:NN 4 #1 ||
\token_if_eq_charcode_p:NN 5 #1 ||
\token_if_eq_charcode_p:NN 6 #1 ||
\token_if_eq_charcode_p:NN 7 #1 ||
\token_if_eq_charcode_p:NN 8 #1 ||
\token_if_eq_charcode_p:NN 9 #1
}{
\prg_return_true:
}{
\prg_return_false:
}
}
\prg_new_conditional:Npnn \is_integer:n #1 { p, TF }
{
\tl_if_empty:nTF { #1 }
{
% We are done if the token list is empty
\prg_return_true:
}{
\exp_args:Nf \is_digit:NTF { \tl_head:n { #1 } }
{
\exp_args:Nf \is_integer:nTF { \tl_tail:n { #1 } }
{
\prg_return_true:
}{
\prg_return_false:
}
}{
\prg_return_false:
}
}
}
\bool_if:nTF { \is_integer_p:n { 1234 } } { true } { false },~
\bool_if:nTF { \is_integer_p:n { 12ab } } { true } { false }
\ExplSyntaxOff
\end{document}
As expected, this results in the text true, false in the document. Of course, as pointed out by Manuel, the use of \exp_args:Nf can be avoided by defining appropriate variants of \is_digit:NTF and \is_integer:NTF. Marking as solved here.