# Outer with non-diagonal elements

Is there a way to perform Outer operation without evaluating diagonal elements like:

Outer[f, {1, 2, 3}, {1, 2, 3}], expecting the following output:

{{f[1, 2], f[1, 3]}, {f[2, 1], f[2, 3]}, {f[3, 1], f[3, 2]}}?

Shold I put condition on the function f?

Thank you

Outer[If[# == #2, ## &[], f@##] &, {1, 2, 3}, {1, 2, 3}]


{{f[1, 2], f[1, 3]}, {f[2, 1], f[2, 3]}, {f[3, 1], f[3, 2]}}

Also

Array[If[# == #2, ## &[], f@##] &, {3, 3}]


{{f[1, 2], f[1, 3]}, {f[2, 1], f[2, 3]}, {f[3, 1], f[3, 2]}}

Note: You can also use Unevaluated @ Sequence and, in versions 10+, Nothing in place of ##&[].

Update: An alternative to post-processing suggested by VitaliyKaurov is to re-define f to give Nothing for equal arguments (f[x_,x_]:=Nothing) before using it in Outer:

res1 = Block[{g = f}, g[x_,x_]:=Nothing;Outer[g, Range[10^3], Range[10^3]]  ]; //
RepeatedTiming  // First


0.56

res2 = Replace[Outer[f, Range[10^3], Range[10^3]], _[x_, x_] -> Nothing, {2}]; //
RepeatedTiming// First


0.704

res1 == res2


True

• Nothing instead of ## &[] would work too. – Vitaliy Kaurov Aug 10 '18 at 8:27
• Thank you @Vitaliy; added that variation. – kglr Aug 10 '18 at 8:32
• Block[{g = f}, g[x_, x_] :=...] sets a value for f[x_, x_] :=... (because the head of the LHS is evaluated first inside SetDelayed). Strangely Block[{f}, f[x_, x_] :=...] adds significant running time, although it seems equivalent to blocking g. – Michael E2 Aug 10 '18 at 11:42
• @MichaelE2, good point. I am puzzled too. – kglr Aug 10 '18 at 12:03
• As this was accepted I assume it's fine, but maybe as a note to future readers: This doesnot remove the diagonal from the output. It removes any elements where the arguments are the same, which might not be the same depending on the second and third argument of Outer. – Lukas Lang Aug 10 '18 at 16:25

MapIndexed and Drop removes the diagonal:

MapIndexed[Drop, Outer[f, Range, Range]]
(*  {{f[1, 2], f[1, 3]}, {f[2, 1], f[2, 3]}, {f[3, 1], f[3, 2]}}  *)


It removes the diagonal of any square array, for instance, if the array is generated with an f that is already defined or is a Function:

MapIndexed[Drop, Outer[10 #1 + #2 &, Range, Range]]
(*  {{12, 13}, {21, 23}, {31, 32}}  *)


It can be used in the operator form MapIndexed[Drop] @ Outer[..].

It's faster than replacing diagonal elements or defining f[x, x] to evaluate to Nothing.

res1 = Block[{g = f},
g[x_, x_] := Nothing;
Outer[g, Range[10^3], Range[10^3]]]; // RepeatedTiming // First
(*  0.42  *)

Clear[f];  (* needed if res1 is evaluated before res2 *)
res2 = Outer[f, Range[10^3], Range[10^3]], _[x_, x_] -> Nothing, {2}]; //
RepeatedTiming // First
(*  0.43  *)

res3 = MapIndexed[Drop, Outer[f, Range[10^3], Range[10^3]]]; //
RepeatedTiming // First
(*  0.199  *)

res1 === res2 === res3
(*  True  *)


Most of the time for the Drop method is spent generating the array:

Outer[f, Range[10^3], Range[10^3]]; // RepeatedTiming // First
(*  0.184  *)


Note: The current method of using Block[{g = f},...] still defines a value for f[x_, x_], since g evaluates to f in SetDelayed. Failing to clear f before running res2 adds about 50% to the execution time (0.61s). Using Block[{f}, f[x_, x_] :=...] takes about 25% longer (0.506s).

Because operation on diagonal elements is performed anyways you could take an alternative path of removing them post-evaluation. You should ask a question what time is spent on the special construct removing diagonal during evaluation? Perhaps an alternative post-removal would be faster.

Replace[Outer[f, {1, 2, 3}, {1, 2, 3}], _[x_, x_] -> Nothing, {2}]


Timing measures:

Replace[Outer[f, Range[10^3], Range[10^3]], _[x_, x_] -> Nothing, {2}]; // AbsoluteTiming


{0.528235, Null}

Outer[If[# == #2, Nothing, f@##] &, Range[10^3], Range[10^3]]; // AbsoluteTiming


{1.313204, Null}

Array[If[# == #2, Nothing, f@##] &, {10^3, 10^3}]; // AbsoluteTiming


{1.41581`, Null}