ntuple(m -> f(Val(m)), Val(3)) should do the trick, I think?
The compiler should eliminate the tuple construction if it is not used. (Still, it might be nice to have a foreach method that accepted a Val for static loops instead of using ntuple for this kind of thing.)
Don’t have a terminal handy, but map works better than loops for stability on tuples. Even better is probably the ntuple function. The below matches your MWE and might be unrolled like you want: ntuple(i -> f(Val(i)), Val(3))
If not, you can write a macro to manually unroll a loop (or some package probably already has it) for a set of values fixed at compile time.
EDIT: the above beat me to it. Also, the suggested @nexprs is probably easier than a loop unrolling macro, if you decide to go that way.
The (nothing, nothing, nothing) is the result of the return value from println being run each of 3 times. Yes, the compiler will usually drop these sorts of things if you don’t use them.
That said, this case of (nothing, nothing, nothing) has zero size and is type stable (the compiler knows that println returns nothing and that it’s run 3 times) so it actually only exists within the compiler and the values will never “appear” on your computer’s RAM or processor whether you use them or not.