How to run a "while" loop while still returning the successful result of the contained function?

I am trying to add a record to :mnesia for a random session id string. The idea is if the session id already exists in mnesia, it should generate a new random id and try again. We should try n number of times.

I can do this with the following test code based off the suggestions here:

    def add_session(user_name) do

        write_session_stream = fn ->

            countdown = Stream.unfold(5, fn
                0 ->
                    IO.puts("FAILED TO ADD SESSION") 
                    nil                # Stop when we reach 0
                n ->
                    session_id = RandomString.generate(10);
                    utc_now = DateTime.utc_now();
                    start_date_string = DateTime.to_iso8601(utc_now) 
                    existing_id_list = :mnesia.read({AuthSession, session_id}) # check if session id exists 

                    if length(existing_id_list) > 0 do
                        IO.puts("SESSION ID NOT UNIQUE - TRY AGAIN");
                        {n, n - 1} # Emit n and prepare for the next state
                    else
                        #DOESN'T EXIST
                        IO.puts("OKAY: ID DOESN'T EXIST " <> inspect(existing_id_list));
                        :mnesia.write({AuthSession, session_id, user_name, start_date_string})
                        nil #end loop
                    end

            end)
            Stream.run(countdown);

        end
        :mnesia.transaction(write_session_stream)

    end

This works for adding the session data to mnesia and trying maximum n times. But I am not able to get out of it the value of the entry that was written as a return from the mnesia addition.

ie. I can’t return out: {session_id, user_name, date_string}

It instead returns {:atomic, :ok} which is useless.

This is presumably because I am returning nil from the countdown and the transaction returns Stream.run(countdown); which is just :ok. So that is all I can get back.

This is something that would be very simple in any other programming language. How can I do this in Elixir? Thanks for any help.

Oh I just realized a write transaction only returns :ok as shown here:

https://elixirschool.com/en/lessons/storage/mnesia

So I need to return what I want manually in there.

I re-wrote it as two functions recursively and this seems to work as say add_session_loop(user, 5).

    def add_session_loop(user_name, num_try)do

        if (num_try>0)do
            result = add_session_once(user_name)
            case (result)do
                {:atomic, nil} ->
                    add_session_loop(user_name, num_try - 1)
                _->
                    result
            end
        else
            nil
        end
    end

    def add_session_once(user_name) do

        write_session = fn ->

            session_id = RandomString.generate(10);
            utc_now = DateTime.utc_now();
            start_date_string = DateTime.to_iso8601(utc_now) 
            existing_id_list = :mnesia.read({AuthSession, session_id})

            result = if length(existing_id_list) > 0 do
                IO.puts("SESSION ID NOT UNIQUE - TRY AGAIN");
                nil
            else
                #DOESN'T EXIST
                IO.puts("OKAY: ID DOESN'T EXIST " <> inspect(existing_id_list));
                :mnesia.write({AuthSession, session_id, user_name, start_date_string})
            end
            IO.puts("WRITE RESULT " <> inspect(result))
            result = case result do
                :ok ->
                    IO.puts("DONE OK")
                    { session_id, user_name, start_date_string }
                _->
                    nil
            end
            result;

        end
        :mnesia.transaction(write_session)
    end

Why is write_session a function variable? Why not a normal separate function at the module level?

Also I’d rewrite add_session_loop like this:

  def add_session_loop(user_name, 0), do: nil

  def add_session_loop(user_name, num_try) do
    result = add_session_once(user_name)
    case result do
      {:atomic, nil} ->
        add_session_loop(user_name, num_try - 1)
      _ ->
        result
    end
  end

Seems you’ll benefit if you setup auto-formatting on file save in your IDE, by the way.

2 Likes

Thanks I’ll use your suggestions. I appreciate the pointers.

What is the difference? I tried moving it to a function but I still have to pass in an anonymous function to the :mnesia.transaction or it won’t work.

ChatGPT confirms this saying:

To pass a non-anonymous function into :mnesia.transaction, you typically need to wrap your function in an anonymous function. This is because :mnesia.transaction expects a function that takes no arguments.

So any difference is just stylistic at that point. I like having the logic stylistically right where it is being run.

Or is it faster to use :mnesia.transaction(fn -> MyModule.my_function(arg) end) and have the function separately?

I think the way I wrote it is likely faster. Could be more explicit to say :mnesia.transaction(fn -> [full function logic here] end) all at once but the compiler I presume will figure that out. No point in splitting to two functions except style if it is only ever run as one combined function.

Unfortunately that I can’t do. :joy: I am using VS Code with the Elixir LS add-on. If you turn on auto-formatting it forces 2 space tab indentations on you (rather than 4 space tabs), and that is not something I can survive.

Unless there is a way around that?

From their page

  // Note: While it is possible to override this in your VSCode configuration, the Elixir Formatter
  // does not support a configurable tab size, so if you override this then you should not use the
  // formatter.
  "editor.tabSize": 2,

I think I’m stuck on that. But thanks.