Here's a simple implementation of what you want.
It is not very robust, as it may lose braces and spaces in the argument, and I didn't test with corner cases: basically it works if the argument consists of "normal" (non-brace and non-space) tokens. It's supposed to be instructive rather than practical. For a robust implementation I'd use expl3 to have a finer-grained control of different types of tokens and expansion.
That said: your code didn't work because your implementation of \split is not expandable (it uses assignments), so you can't have function-like behaviour (evaluate the argument, and then evaluate the caller). To have that, \split has to be expandable.
In this case it's rather easy to build a recursive \splitacc that doesn't rely on assignments. You can have it take one more argument which will be the accumulator, so you just put things there and they will carry on to further calls to the macro. I defined \splitacc{<accum>}<token><token-list>\nil so that it adds each <token> to <accum> until the end of the <token-list> is reached. When the input is over, \splitaccend loops through the separated tokens, accumulating them in the output.
Then, to have it work as argument to \testbis I defined a \exparg macro that (relies on the \expanded primitive and) fully expands the first argument to a macro. Use it as \exparg\macro{<argument>}. It is the same as expl3's \exp_args:Ne.
I also added a macro to trim spaces around the items in \testbis so that spacing is implementation-independent. You can add spaces in the separator.
\documentclass[12pt,a4paper]{article}
\makeatletter
% a quark
\def\qmark{\qmark}
% a macro to trim spaces (not very robust: may lose braces)
\def\trimspace#1{%
@firstofone{\expandafter@trimspace
@firstofone#1 \nil@trimspace} \nil@@trimspace\qmark}
\def@trimspace#1 \nil#2{#2#1\nil@trimspace}
\def@@trimspace#1\nil#2\qmark{#1}
% \split starts with an empty accumulator and ends
% with a \qmark to identify the end.
\newcommand\split[1]{%
\splitacc{}#1\qmark\nil}
% \splitacc checks if the end is reached. If so leaves the accumulator,
% otherwise recurses with #1|#2 (this adds a leading | in the first
% iteration which is removed at the end).
\def\splitacc#1#2#3\nil{%
\ifx\qmark#2%
\expandafter@firstoftwo
\else
\expandafter@secondoftwo
\fi
{\splitaccend#1\qmark}% use the accumulated string (remove leading marker)
{\splitacc{#1{#2}}#3\nil}}% add to the accumulator and loop
\def\splitaccend#1#2{%
#1%
\ifx\qmark#2
\expandafter@gobble
\else
|\expandafter@firstofone
\fi{\splitaccend{#1#2}}}
\newcommand\testbis[1]{%
\merge#1|\nil}
\def\merge#1|#2\nil{%
\trimspace{#1}%
\if\relax\detokenize{#2}\relax\else
$>$\merge#2\nil%
\fi}
% \exparg expands one argument of a macro. Simlar to \exp_args:Ne
\def\exparg#1#2{%
\expandafter#1\expanded{{#2}}}
\begin{document}
\split{1234} % ---> 1 | 12 | 123 | 1234
\testbis{1 | 12 | 123 | 1234}
\exparg\testbis{\split{1234}}
\end{document}
As requested, and expl3 implementation, for didactic purposes. The idea here is to loop on the argument token list, and take a different action depending on the type of the next token (space, group of tokens, or “normal”—everything else). The entry level macro just sets the environment for the main looping macro. This looping macro looks at the next token in the input and selects one of three macros depending on the type of said token. Then, each dedicated macro does the actual work of the function.
This conditional acting allows you to process spaces and groups of tokens properly (depending on the behaviour you want). Using xparse allows you to easily define optional arguments to change what tokens are looked for and what tokens are used as replacement.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewExpandableDocumentCommand \split { O{|} m }
{ \mbc_split:Nn #1 {#2} }
% Entry-level function:
\cs_new:Npn \mbc_split:Nn #1 #2
{ __mbc_split_loop:Nnw #1 { } #2 \q_recursion_tail \q_recursion_stop }
% Looping function to choose type of token:
\cs_new:Npn __mbc_split_loop:Nnw #1 #2 #3 \q_recursion_stop
{
\tl_if_head_is_N_type:nTF {#3}
{ __mbc_split_ntype:NnN }
{
\tl_if_head_is_group:nTF {#3}
{ __mbc_split_group:Nnn }
{ __mbc_split_space:Nnw }
}
#1 {#2} #3 \q_recursion_stop
}
% Action for 'normal' tokens:
\cs_new:Npn __mbc_split_ntype:NnN #1 #2 #3
{
\quark_if_recursion_tail_stop:N #3
\tl_if_empty:nTF {#2}
{ \exp_not:n { #3 } }
{ \exp_not:n { #1 #2#3 } }
__mbc_split_loop:Nnw #1 {#2#3}
}
% Action for grouped tokens:
\cs_new:Npn __mbc_split_group:Nnn #1 #2 #3
{
\exp_not:n { #1 #2{#3} }
__mbc_split_loop:Nnw #1 { #2{#3} }
}
\cs_new:Npn __mbc_split_space:Nnw #1 #2 ~
{
\exp_not:n { #1 #2~ }
__mbc_split_loop:Nnw #1 { #2~ }
}
%
%
\NewExpandableDocumentCommand \testbis { s D(){|} O{$>$} m }
{
\IfBooleanTF{#1}
{ \mbc_replace:Nne #2 {#3} {#4} }
{ \mbc_replace:Nnn #2 {#3} {#4} }
}
\cs_generate_variant:Nn \mbc_replace:Nnn { Nne }
% Entry-level function:
\cs_new:Npn \mbc_replace:Nnn #1 #2 #3
{ __mbc_replace_loop:Nnw #1 {#2} #3 \q_recursion_tail \q_recursion_stop }
% Looping function to choose type of token:
\cs_new:Npn __mbc_replace_loop:Nnw #1 #2 #3 \q_recursion_stop
{
\tl_if_head_is_N_type:nTF {#3}
{ __mbc_replace_ntype:NnN }
{
\tl_if_head_is_group:nTF {#3}
{ __mbc_replace_group:Nnn }
{ __mbc_replace_space:Nnw }
}
#1 {#2} #3 \q_recursion_stop
}
% Action for 'normal' tokens:
\cs_new:Npn __mbc_replace_ntype:NnN #1 #2 #3
{
\quark_if_recursion_tail_stop:N #3
\token_if_eq_charcode:NNTF #1 #3
{ \exp_not:n {#2} }
{ \exp_not:n {#3} }
__mbc_replace_loop:Nnw #1 {#2}
}
% Action for grouped tokens:
\cs_new:Npn __mbc_replace_group:Nnn #1 #2 #3
{ {#3} __mbc_replace_loop:Nnw #1 {#2} }
% Action for space tokens:
\cs_new:Npn __mbc_replace_space:Nnw #1 #2 ~
{ ~ __mbc_replace_loop:Nnw #1 {#2} }
\ExplSyntaxOff
\begin{document}
\split{1234}
\split{1 2{\textit{3}}4}
% * argument forces expansion
\testbis*{\split{1234}}
\testbis*{\split{1 2{\textit{3}}4}}
% ()-delimited argument is the token searched (must be a single token)
% []-delimited argument are the replacement tokens
\testbis(-)[$+$]{1-1 2-1 2{\textit {3}}4}
\end{document}
\testbis{\split{1234}}won't work because\splitis not expandable, so you can't have function-like behaviour (evaluate the argument\split{1234}then evaluate the caller\testbis).\testbiswill see\split{1234}. You need to write\splitto be expandable. – Phelype Oleinik Aug 04 '20 at 14:39