The implementation of lazy tuples [here][1] pretty much contains the solution to the lazy `Outer` problem. I will take the relevant parts from that code.
The following code constructs a function `take`, which would, given the start and end positions in the flat list of the resulting combinations, extract the elements:
ClearAll[next];
next[{left_, _}, dim_] :=
{left - dim*(# - 1), #} &[IntegerPart[(left - 1)/dim] + 1];
ClearAll[multiDims];
multiDims[dims_] := Rest @ Reverse @ FoldList[Times, 1, Reverse @ dims];
ClearAll[multiIndex];
multiIndex[pos_, dims : {__Integer}] :=
Rest@FoldList[next, {pos, 0}, multiDims@dims][[All, 2]]
ClearAll[take];
take[lists : {__List}, {start_, end_}] :=
With[{rend = Min[end, Times @@ Map[Length, lists]]},
Transpose @ MapThread[
Part,
{lists, multiIndex[Range[start, rend], Length /@ lists]}
]
];
For example,
take[{{1, 2, 3}, {4, 5, 6}}, {3, 7}] == Tuples[{{1, 2, 3}, {4, 5, 6}}][[3 ;; 7]]
(* True *)
The difference is of course, that `take` only computes those elements that have been requested, so can be used as a basis for a lazy implementation.
Here is then an implementation of an iterator, that would return consecutive combinations in chunks of specified length:
ClearAll[makeTupleIterator];
makeTupleIterator[lists:{__List},chunkSize_Integer?Positive]:=
With[{len=Times@@Length/@lists},
Module[{ctr=0,active=False},
If[ctr>=len,
{},
(*else*)
With[{taken=take[lists,{ctr+1,Min[ctr+chunkSize,len]}]},
ctr+=Length[taken];taken
]
]&
]
];
Here is an example: we construct an iterator with the chunk size of 10 elements:
iter = makeTupleIterator[{{"11", "12", "13"}, {"21", "22"}, {"31"}, {"41", "42"}}, 10];
Now we use it:
iter[]
(*
{
{"11","21","31","41"},
{"11","21","31","42"},
{"11","22","31","41"},
{"11","22","31","42"},
{"12","21","31","41"},
{"12","21","31","42"},
{"12","22","31","41"},
{"12","22","31","42"},
{"13","21","31","41"},
{"13","21","31","42"}
}
*)
iter[]
(* {{"13", "22", "31", "41"}, {"13", "22", "31", "42"}} *)
iter[]
(* {} *)
When we get an empty list, this tells us that the iterator has been exhausted.
This basically implements lazy tuples, and therefore also lazy `Outer`, more or less. You gain efficiency by picking large enough chunks, since chunk extraction (`take` function) is pretty fast, compared to the top-level iteration that would be needed to extract element by element.
[1]: https://gist.githubusercontent.com/lshifr/56c6fcfe7cafcd73bdf8/raw/LazyTuples.m