16
$\begingroup$

I want to iterate over a list, and return the index as long as I found the first element satisfying my condition. Written in a for loop:

f[x_]:=2x

For[i=1, i<=10, i++,
  If[f[i]==10, Return[i]]
]

And I know a way to write this in Mathematica:

Position[f/@Range[10], 10, 1, 1]

However, in this code, a list of f[1], f[2], ..., f[10] is all calculated before the Position executes. In my real problem, f is a costly function. I don't want to, and it is unnecessary to calculate the rest when the first is found. How can I do this using functional style of Mathematica (no loops, ++s)?

Thanks

$\endgroup$
  • 2
    $\begingroup$ You may be able to concoct a combination of Scan with Throw and Catch ( see the "Generalization and extension" sections of the Scan docs for an example). However, perhaps you could provide the context in which this need arose. It may be that refactoring the parent problem might lend itself to a more idiomatic solution and better performance. Otherwise, this would actually seem an acceptable use of a procedural loop, at least to me. $\endgroup$ – MarcoB Jul 12 '16 at 15:02
  • 2
    $\begingroup$ Just based off your example, nn = 0; Scan[If[f[#] <= 10, nn++, Return[nn]] &, Range[10]] would work also $\endgroup$ – Jason B. Jul 12 '16 at 15:05
  • $\begingroup$ @JasonB Thanks! I see your point. The parent problem was more complicated about f but has generally the same framework of this problem. $\endgroup$ – Nick Jul 12 '16 at 15:22
16
$\begingroup$

As is demonstrated very well in this post you can use a criteria for your pattern, thereby only applying your function as long as you are searching and not to all elements. Also there is a specific FirstPosition function.

f[x_] := Module[{}, Pause[0.5]; 2 x]

AbsoluteTiming[
 Position[f /@ Range[10], 10, 1, 1]
]

AbsoluteTiming[
 FirstPosition[f /@ Range[10], 10]
]

{5.00824, {{5}}}

{5.01055, {5}}

AbsoluteTiming[
 Position[Range[10], _?(f[#] == 10 &), 1, 1]
]

AbsoluteTiming[
 FirstPosition[Range[10], _?(f[#] == 10 &)]
]

{3.00378, {{5}}}

{3.00329, {5}}

$\endgroup$
13
$\begingroup$

SelectFirst would work:

SelectFirst[Range[10], f[#] == 10 &]

You can tell that it only executes f on indexes 1-5 if you add a Print statement to the definition:

f[x_] := (Print[x]; 2 x)
$\endgroup$
  • 1
    $\begingroup$ That returns the element of the list, not the index. You would then have to pass that to Position... $\endgroup$ – Quantum_Oli Jul 12 '16 at 17:03
  • 1
    $\begingroup$ Range[10] is the list of indexes and in OP's description, the index list seems to be the one they wanted to loop over. If one needs to loop over a different list, change the test to f[list[[#]]] == 10 &. $\endgroup$ – Gerli Jul 13 '16 at 9:38
10
$\begingroup$
1 + LengthWhile[Range@10, f@# != 10 &]

5

$\endgroup$
4
$\begingroup$
TakeWhile[Range@10, f@# <= 10 &] // Length

Alternative answer that might be helpful depending on the issue at hand

If your list is sorted and your test is monotonic, instead of checking every list entry from the beginning you could use a binary search (divide and conquer approach)

Clear@f
f[x_] := (Pause@2; 2 x) (* your costly condition*)

Needs["Combinatorica`"]
BinarySearch[Range@10, 10, f] // AbsoluteTiming

{2.00142, 5}

For comparison:

TakeWhile[Range@10, f@# <= 10 &] // Length // AbsoluteTiming

{12.0076, 5}

Using the somewhat deprecated BinarySearch function from the Combinatorica package may not be such a brilliant idea but works reasonably well for this toy example. See this answer by Leonid Shifrin for a useful implementation of a binary search algorithm in Mathematica.

$\endgroup$
4
$\begingroup$

Probably this should be a comment to JasonB's post, but I have too little reputation to comment... Here is another version using Scan:

Scan[If[f@# == 10, Return@#]&, Range[10]]
(* 5 *)

Returning the index of an arbitrary list:

l = Range[1, 21, 2]
(* {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21} *)

Scan[If[f@l[[#]] == 10, Return@#] &, Range@Length@l]
(* 3 *)
$\endgroup$
3
$\begingroup$

Maybe you just want a efficient way to do this while retaining the procedural approach. If that is the case, you can consider the following:

ok = True;
f[x_] := If[ok, 2 x, Null];
For[i = 1, i <= 10, i++, If[f[i] == 10, (ok = False; Return[i])]]

(* Return[5] *)

ok = True;
Position[f /@ Range[10], 10]
(* {{5}} *)
$\endgroup$
  • $\begingroup$ I don't think the OP's qualms with a procedural approach were about the efficiency of the procedural code (since efficiency is going to be determined by the costly condition checking anyway) but more a question of coding style. $\endgroup$ – Sascha Jul 12 '16 at 17:28
3
$\begingroup$

I hesitate to add this because it's so ugly, but it works as requested. It will give the last number meeting the requirement, and will stop the first time it encounters a number that doesn't

nn = 0; Scan[If[f@# <= 10, nn++, Return@nn] &, Range@10]
(* 5 *)
$\endgroup$
2
$\begingroup$

You can use NestWhile

f[x_]:=x^2; (*your function.*)
NestWhile[#+1&,0,f[#+1]<10&]

Which returns 3 as the last number satisfying the condition. The #+1 in the expr portion increments the iterator for the NestWhile, and the #+1 in the test tests the next value of the iterator. If the next value fails the test you get the current value of the iterator back.

I'm not sure this is actually preferable to other solutions, but it's an option.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.