This is an interesting, if (necessarily) ill-posed question. My approach is to couch it in more general terms and attempt to clarify what is possible, natural and generalizable by exploring the meaning of "clean", "efficient" and "non-contrived". Some implications for language design are also discussed.
max = 5000;
val := 2 (2 n^2 + (y - 2) (z - 2) + x (y + z - 2) + 2 n (x + y + z - 3));
ai[i_] := Length@Solve[val == i && x >= y >= z >= 1 && n >= 1, {x, y, z, n}, Integers];
a = Table[ai[i], {i, 1, max}]
(*
- Warning: On a personal machine this is likely to take several hours;
for plausibility setting max = 100 takes less than 1 min to complete.
- Acknowledgment: This was computed at the Pawsey Supercomputing Center
( ~18 min on 12 kernels with ParallelTable replacing Table).
*)
Hence as per the OP's request, the code is written without loops and is also, I'd argue, clean, non-contrived, (space) efficient; offers lazy evaluation for a
's components and provides additional context. It is however, manifestly and grossly inferior from a time-efficiency viewpoint needing HPC resources to even confirm its equivalence with previous implementations. Its conceptualization however, dilineates likely limitations.
Background:
One point alluded to, but not emphasised in the answers/comments is that constructs like Table
, Array
, SparseArray
etc. all "explicitly define" a space, whereas For
, While
etc. loops describe processes via stopping conditions. The former commonly:
- Specify a space of elements
- Specify how that space is traversed
- Specify computations on visited elements during the traversal
directly and more naturally (arguably because humans are more visual than logical creatures). Looping constructs like For
, on the other hand, can be co-opted into defining spaces with the above features but often do so sub-optimally as their stopping conditions end up getting in the way.
The underlying space being defined in the question is not immediately obvious, as evidenced by initially resorting to loops but the deeper question seemingly being proposed is how permanent is this state of affairs? is there a systematic way of re-casting into more functional-like Table
/Array
forms? Unfortunately the answer turns out to be no, although there is usually more that can be done; first though, consider how this particular loop could be translated into a Table
form albeit in a way apparently insufficiently "magical" or "programmatical" - by analyzing val
's particular structure and mathematical properties.
First, it can be noted that the For
loop terminates since val
's quadratic increase in n
is guaranteed to eventually exceed max
. The actual iterates over which this takes place however, is not immediately obvious corresponding to the non-obviousness of converting into a Table
/Array
formulation. MichaelS2's answer, alone amongst all the responses, explicitly finds iterates thereby arriving at a non-loop solution as originally requested (it is on this basis, I'd argue, that his answer merited acceptance rather than from the stated efficiency gains (other Table
/SparseArray
solutions assumed, stopping properties related to val
[as acknowledged in the comments] while all the other answers have loops lurking somewhere within While
's or recursive procedures).
[Note that even this loop dichotomy can't be pushed too far; a stopping condition can be inserted into a Table
's iterates thereby turning it into a "Loop" whilst explicit iterates in a For
loop can augment stopping-conditions thereby turning it into a "Table
"]
Converting to a Table
formulation by finding explicit iterates however, required analyzing val
in the stopping condition with the specific nature of such analysis unavoidable thereby dashing the OP's hopes and intuition (admittedly also my own) for a generic, "magical", "programming solution" for loop conversion. This follows since to assume otherwise would mean being able to translate arbitrary For
stopping conditions into decidable procedures (those characteristic of Table
-like iterates), something impossible from the Halting Problem's unsolvability. For some examples therefore, the traversal simply has to be stepped through (imagine a random val
to simulate a black box function).
The Solve
reformulation conceptualizes a
as a vector who's i'th component counts the number of "essentially different" solutions to the Diophantine equation: val == i
. Here "essentially different", means up to re-labeling the x
, y
,z
variables and is implemented by translating For
's stopping conditions that effectively insist on the variable ordering x>=y>=z>=1
.
The explict values of Table
iterates correspond to upper bounds on the size of integers in any putative solution. Diophantine equations are often used to settle decidability questions which here emerges in the form of systematising code re-writing attempts.
In terms of (time) efficiency implications, one pertinent issue is whether or not these counts can be performed without explicitly generating actual solutions. SatisfiabilityCount
offers an interface for doing so in relation to Boolean
equations although its performance suggests solutions are first generated before being counted.
A Demonstration shows that while it is possible to generate closed-form formulae for counting solutions without their explicit generation for classes of equations (albeit over small dimensions).
In general however, counting problems of #P complexity are generally intractable and the point of couching the problem in number-theoretic terms is that it can show you what you are up against in terms of searching for efficiency gains. I suspect that existing complexity results for counting solutions of diophantine equations makes the prospects of significant improvements in this example very limited.
Intuitively, a
depends on visiting every element in the For
loop with any significant efficiency gains stemming from being able to short-cut this process. The exhibited efficiency gain (with the Table
solution) does this in a limited (if clever and useful) way by using max
(effectively bypassing the stopping condition as the means for ignoring those variable values for which val
exceeds max
) while also exploiting Mathematica's implementation of Table
(its compilability and parallelization).
The core "irreducibility" of this computation however, can be first discerned by observing a
's "randomness" for the first 5K elements:
with some order emerging from seeing the first 50K elements:
but all the while random-like upper boundary persisting as evident from viewing the first 250K elements:
(* Acknowledgement:
- The above plots used the compiled "Table" from Michael's answer.
- The last 250K plot used the resources of the Pawsey Supercomputing Centre
in Perth, Western Australia (taking ~15 min with 12 Kernels and ParallelTable)
*)
indicating the absence of a recursive reducibility (e.g. impossibility of expressing a[[i]]
in terms of a[[j]]
's for j<i
). Consider however, a similar example instead involving the expression val2
(derived from val
by replacing n^2
with n
and removing the later occurrence of 2n
). Now there is evidence of clear reducibility.
The reducibility inherent in the the "val2
computation" suggests opportunities for efficiency gains in codified mathematical knowledge, say that built-in to functions like Solve
; sure enough it outperforms the For
loop for finding a[[5000]]
(17.73 s vs 120.27 s) in contrast to its inferior performance in relation to the more "irreducible val
computation" (5.82 s vs 2.07 s).
To be sure, finding a single element of a
is Solve
's focus in contrast to For
's focus here in generating all of a
's elements. On the other hand, this also indicates one might have anticipated Solve
's more competitive performance (notwithstanding its impressive generality and suggesting its improvement by associating predicates corresponding to For
's stopping conditions together with preliminary checks on identifying/looking-up irreducibility); at any rate, the same effects would be observable given a mature counting framework in Mathematica (i.e. functions for which counting was the focus).
There are also many implications for language design here but this post is plenty wordy already.
Well some implications:
The conventional wisdom seems to be that For
's should be afforded sideways glances before being cast off into outer darkness while moving into the utopia of Table
-Array
vectorization. While this view carries a certain force (I can't imagine programming without Tables
/Arrays
) IMO such unyielding focus on functional programming can also become limiting.
Firstly, while Tables
/Arrays
represent powerful ways of exploring the computational universe, they do so in a regimented way progressively fixing variable dimensions that while easily humanly graspable potentially ignore potentially fruitful search spaces. In addition, they also tend to push answers in directions that may not even require brute-force enumeration. Even in situations where brute-force enumeration is apparently unavoidable however, (such as the irreducibility illustrated here) certain types of questions necessitate a different, "more semantic" approach. This is possibly foreshadowed in pure math initiatives but more broadly it also has implications in the scientific practice of model-building.
The process of identifying this problem (from apparently Project Euler) involved a type of reverse-engineering, analogs of which frequently occur in general modeling. To relate it back to the problem at hand: It's not necessarily the case that saying (or computing) something useful about a[[i]]
should require computing its exact value. As a toy example, in a more "semantic" computing environment, ZeroQ[a[[(googol=10^100)-1]]
should return True
without setting out to generate the structure a
or the exact value of a[[googol-1]]
(n.b. current output of PossibleZeroQ[googol]
or PossibleZeroQ[googol^googol]
)
This suggests notions of "LazyTable
", "LazyArray
" analogs (SemanticArray
, DelayedArray
or Array
overloaded?) that move gracefully between producing actual structures when immediately, computationally feasible and otherwise; accessing pre-computed databases and/or remaining unevaluated in readiness for input into computational questions about a[[i]]
(i.e. that don't require its exact value).
While the ZeroQ
question/computation here is toy, situations inevitably arise in which the computation forms a link in a useful (computational/reasoning) chain and/or it is in fact the only way to identify the origin of the original computation. Consider a (hidden) For
loop representing an irreducible natural process in which say a[[googol;;googol+100]]
, constitutes the output of 100 experimental observations (i.e. a
's index corresponds to time). Due to irreducibility it may be impossible to run the computation again to deduce such provenance but nonetheless, computations about a[googol;;googol+100]
(i.e. beyond ZeroQ
) potentially exhibit identifying fingerprints traceable back to the originating For
process. This however, requires For
's original framing and collation (or equivalents) in "lazy" terms.
This computational gap is a fundamental limitations of the (still) amazingly useful look-up tables such as the On-line Encyclopadia of Integer Sequences, (n.b. FindSequenceFunction
) but functions like "LazyTable
" at least provide a mechanism for overcoming such gaps by generalizing this collation. It portends the usefulness of (inter-related) look-up tables for lists of functions and/or symbols backed by their networked connections (integers as algorithmic fingerprints owe as much to a human's proclivity for counting things as anything else); in so doing significant parts of scientific inquiry can be automated given how modeling/simulations often embody precisely this reverse-engineering process.
val
a black-box function and just look at the loop structure. I think what that does it reasonably clear. $\endgroup$:- digits(381654729).
"(en.wikipedia.org/wiki/Polydivisible_number#Background). $\endgroup$