Write while loop equivalent in elixir

Ultimately it depends on how determined you are:

Functional Programming in Java (Pragprog)

p.123 - Chapter 7: Optimizing Recursions - Turning to Tail Recursion

https://media.pragprog.com/titles/vsjava8/code/recur/fpij/Factorial.java

https://media.pragprog.com/titles/vsjava8/code/recur/fpij/TailCall.java

https://media.pragprog.com/titles/vsjava8/code/recur/fpij/TailCalls.java

https://www.google.com/search?tbm=bks&hl=en&q=import+fpij.tailcalls.call

1 Like

Mmm, no? A lot of languages have TCO support, not just the beam, it’s actually quite common


Oracle’s JVM doesn’t, but some others do.

1 Like
def interest_calculator(interest, amount, no_of_days) do
    interest_per_day = ((interest/100)/30) |> Float.floor(4) 
    amount_interest_per_day = interest_per_day * amount
    total_balance = amount_interest_per_day + amount
    case no_of_days > 0 do
      true  -> calculator(interest, total_balance, no_of_days-1)
      false ->  amount
    end
  end

In real life I can use while(some_condition) for repeating an action unknown number of times. Like:

do {
  name = generate_new_name();
} while(name.exists?)

So I can’t really do any sort of enumeration here. Should I do the recursion in such case? And if I wanted to limit the depth?

In the simplest terms I think this would be similar to your code above:

def while(fun) when is_function(fun) do
  if (name = fun.() && exists?(name)), do: while(fun), else: name
end

# Call it as:
name = while(&generate_name/1)

Your code example here implies mutation, so “real life” may look a little differently in a functional language. For example if you want to generate a list of new names until they stop existing you can do something like:

Stream.repeatedly(&generate_name/0) |> Enum.take_while(&exists?/1)

There are a hundred little variations on that you can do, combining various stream generators like repeatedly/1, unfold/1 and so forth with Enum.take* variants.

If those patterns aren’t a good fit, manual recursion is always a simple function a way.

2 Likes

Well if you want to limit the depth, you can always pass along a depth counter. But you probably won’t want to limit depth–other than to make sure that the recursive call is the last thing your function does, so that tail call optimization can limit the actual stack depth to 1.

3 Likes

In a typical case I need one name, which is not yet taken



 and I want to limit the number of checks so that if I tried - say - sixteen times and still don’t get a valid, non-yet-existing one, I can stop and notify that something’s probably wrong somewhere (namespace exhausted, misconfiguration, generator bug, etc.), instead of trying “forever”.

@silverdr OK sure, here’s a non manual loop version:

Stream.repeatedly(&generate_name/0) |> Stream.take(16) |> Enum.find(&not_taken?/1)

This will return the first generated name that isn’t taken, OR quit after 16 tries. It will only generate as many names as are needed. Enumerable works great here too. Of course, you can always use manual recursion.

4 Likes

Huh, that’s all so different from what I am well used to that after many years of successfully doing it for a living (using too many languages/stacks to list) I feel like I am learning programming from scratch again :wink: That’s both refreshingly challenging and intimidating at the same time. For example looking at the line, I wouldn’t guess that it will generate “only as many as needed”. Thanks.

1 Like

That’s the beauty of lazy streams :slight_smile: the original stream is actually infinite, but it only gets realized in a finite number of elements due to Stream.take and Enum.find.

2 Likes

Yes, this is commonly known as “streams” or “lazy iterators”. This mean that the values will be computed only as needed and this is common pattern in languages that have “functional” feel (Haskell is lazily evaluated in general, Rust iterators are lazy by default, etc.). Even some “old” languages are getting such features (like Stream in Java).

1 Like

One good “brain hack” is to think of Enum as “the result of this will be immediately calculated and passed to the next function” and of Stream as “this will be calculated at the first occurence of Enum down the line”.

3 Likes

I would understand that the “repeatedly” is infinite, but then my first guess would be that the second part “takes” 16 instances from the infinite stream and passes to the third part, which enumerates over them, or so. But it seems like backward thinking in the context.

Right - think it backwards :slight_smile:

Key is that the docs for Stream.take start off “Lazily takes
”, meaning elements not evaluated until requested. So it immediately returns, yes, and the return is an enumerable that will yield 16 values, and that enumerable is passed into the Enum.find? call, but the values themselves are not evaluated by the enumerable until requested, if ever.

To add one more answer here,

I’ve created a macro while macro https://hex.pm/packages/while

import While

total = 10

while_with total, total != 0 do
   IO.puts "hello"
   total - 1
end

IO.puts "total is #{total}"

Explanation:
The while_with binds the variable name total to both the expression total != 0 and the body. The body result (last line of body) always becomes the new value for the bound variable, like in a Enum.reduce

   IO.puts "hello"
   total - 1

Addendum:
If you’re only working with globals (or processes) you can also use the simple while without binding a name:

  import While

  ref = :counters.new(1, [:atomics])
  while :counters.get(ref 1) < 10 do
    :counters.add(ref 1, 1)
  end

  IO.puts("Current value is #{:counters.get(ref, 1)}")
1 Like

An equivalent with potentially infinite running time (useful when polling an API for results in a quick Mix.install/2 script):

Stream.unfold(1, fn
  acc ->
    :timer.sleep(1_000)
    IO.puts("Waiting...")
    # TODO here: poll API and decide based on output status whether to leave or not
    if acc >= 5, do: nil, else: {acc, acc + 1}
end)
|> Stream.run()

Thanks @LostKobrakai for the inspiration, I wouldn’t have thought about Stream.unfold for this!

2 Likes