Defining \zip in the first way is quite easy:
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\zip}{mm +m}
{
\seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #1 }
\seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #2 }
\cs_set:Nn __iagolito_zip:nn { #3 }
\seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq __iagolito_zip:nn
}
\seq_new:N \l__iagolito_zip_a_seq
\seq_new:N \l__iagolito_zip_b_seq
\ExplSyntaxOff
\begin{document}
\zip{a,b,c}{1,2,3}{#1-#2 }
\end{document}
You can check this prints
a-1 b-2 c-3
If the two lists have different number of elements, the loop ends when either list ends.
The more complex features can be accomplished as well. Beware that the two optional argument must both appear, if the complex processing is needed.
The idea is to populate another sequence where the two lists are merged and then an auxiliary macro can be applied.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\zip}{oomm +m}
{
\IfNoValueTF { #1 }
{ \iagolito_zip_simple:nnn { #3 } { #4 } { #5 } }
{ \iagolito_zip_full:nnnnn { #1 } { #2 } { #3 } { #4 } { #5 } }
}
\seq_new:N \l__iagolito_zip_a_seq
\seq_new:N \l__iagolito_zip_b_seq
\seq_new:N \l__iagolito_zip_c_seq
\cs_new_protected:Nn \iagolito_zip_simple:nnn
{
\seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #1 }
\seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #2 }
\cs_set:Nn __iagolito_zip:nn { #3 }
\seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq __iagolito_zip:nn
}
\cs_new_protected:Nn \iagolito_zip_full:nnnnn
{
\seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #3 }
\seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #4 }
\seq_clear:N \l__iagolito_zip_c_seq
\cs_set:Npn __iagolito_zip_process:w #1 \q_stop #2 \q_stop { #5 }
\seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq __iagolito_merge:nn
\seq_map_inline:Nn \l__iagolito_zip_c_seq { ##1 }
}
\cs_new_protected:Nn __iagolito_merge:nn
{
\seq_put_right:Nn \l__iagolito_zip_c_seq { __iagolito_zip_process:w #1 \q_stop #2 \q_stop }
}
\ExplSyntaxOff
\begin{document}
\zip{a,b,c}{1,2,3}{#1-#2 }
\zip[#1/#2][#3::#4]
{a/A,b/B,c/C}
{1::I,2::II,3::III}
{Grand #1 is #2 but grand #3 is #4.\par}
\end{document}

If you want to pass macros expanding to lists, do one step expansion.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\zip}{oomm +m}
{
\IfNoValueTF { #1 }
{ \iagolito_zip_simple:oon { #3 } { #4 } { #5 } }
{ \iagolito_zip_full:nnoon { #1 } { #2 } { #3 } { #4 } { #5 } }
}
\seq_new:N \l__iagolito_zip_a_seq
\seq_new:N \l__iagolito_zip_b_seq
\seq_new:N \l__iagolito_zip_c_seq
\cs_new_protected:Nn \iagolito_zip_simple:nnn
{
\seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #1 }
\seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #2 }
\cs_set:Nn __iagolito_zip:nn { #3 }
\seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq __iagolito_zip:nn
}
\cs_generate_variant:Nn \iagolito_zip_simple:nnn { oo }
\cs_new_protected:Nn \iagolito_zip_full:nnnnn
{
\seq_set_from_clist:Nn \l__iagolito_zip_a_seq { #3 }
\seq_set_from_clist:Nn \l__iagolito_zip_b_seq { #4 }
\seq_clear:N \l__iagolito_zip_c_seq
\cs_set:Npn __iagolito_zip_process:w #1 \q_stop #2 \q_stop { #5 }
\seq_mapthread_function:NNN \l__iagolito_zip_a_seq \l__iagolito_zip_b_seq __iagolito_merge:nn
\seq_map_inline:Nn \l__iagolito_zip_c_seq { ##1 }
}
\cs_generate_variant:Nn \iagolito_zip_full:nnnnn { nnoo }
\cs_new_protected:Nn __iagolito_merge:nn
{
\seq_put_right:Nn \l__iagolito_zip_c_seq { __iagolito_zip_process:w #1 \q_stop #2 \q_stop }
}
\ExplSyntaxOff
\newcommand{\listA}{a,b,c}
\newcommand{\listB}{1,2,3}
\newcommand{\listC}{a/A,b/B,c/C}
\newcommand{\listD}{1::I,2::II,3::III}
\begin{document}
\zip{a,b,c}{1,2,3}{#1-#2 }
\zip{\listA}{1,2,3}{#1-#2 }
\zip{a,b,c}{\listB}{#1-#2 }
\zip{\listA}{\listB}{#1-#2 }
\zip[#1/#2][#3::#4]
{a/A,b/B,c/C}
{1::I,2::II,3::III}
{Grand #1 is #2 but grand #3 is #4.\par}
\zip[#1/#2][#3::#4]
{\listC}
{1::I,2::II,3::III}
{Grand #1 is #2 but grand #3 is #4.\par}
\zip[#1/#2][#3::#4]
{a/A,b/B,c/C}
{\listD}
{Grand #1 is #2 but grand #3 is #4.\par}
\zip[#1/#2][#3::#4]
{\listC}
{\listD}
{Grand #1 is #2 but grand #3 is #4.\par}
\end{document}
The output is the same as before, just repeated four times for each instance.
interface3! If you don't mind, I'll still accept egreg's answer because the optional arguments are even more flexible there :) – iago-lito Jul 21 '20 at 07:07