Struggling with loops

I’m slowly getting into Elixir/Phoenix but I’m still struggling sometimes with the functional programming concepts. Here’s what I haven’t found a working solution for.

The below function is from a Phoenix controller module. I’d expect it to create bins and add the ids to two lists with the ones that could successfully be created and the ones that failed. The result of the function should be a tuple with the ids of the bins that could be created and the ones that failed.

Unfortunately, it doesn’t compile…

def create(%{"bins" => params}) do
  succeeded = []
  failed = []

  for bin_struct <- params do
    case UserData.create_bin(bin_struct) do
      {:ok, %Bin{}} -> succeeded = [bin_struct["id"] | succeeded]
      {:error, error} -> failed = [bin_struct["id"] | failed]
    end
  end

  {succeeded, failed}
end
1 Like

You cannot change the values of succeeded and failed from inside the for/case, because those variables are defined outside of it. Instead you need one function that will transform ALL of the params and then give you back the full result. Enum.reduce is the go-to for this. For example:

def create(%{"bins" => params}) do
  Enum.reduce(params, {[], []}, fn bin_struct, {succeeded, failed} ->
    case UserData.create_bin(bin_struct) do
      {:ok, %Bin{}} -> {[params["id"] | succeeded], failed}
      {:error, error} -> {succeeded, [params["id"] | failed}
    end
  end)
end

Here we are starting with an empty accumulator {[], []} which represents the successes and failures. Each iteration of the “loop” (each cast statement) MUST return an accumulator of the same form, but with any changes you want. So in each case the final value should be a tuple with the appropriate list prepended to, but also the other list in there, too, even if it didn’t change. Then, at the end of the loop (when all the params are handled), the Enum.reduce will return the final accumulator tuple with all your changes.

See Enum.reduce/3 for more info

7 Likes

You can use Enum.reduce :

def create(%{"bins" => params}) do
  Enum.reduce(params, {[], []}, fn bin_struct, acc ->
    {succeeded, failed} = acc
    case UserData.create_bin(bin_struct) do
      {:ok, %Bin{}} -> 
        succeeded = [params["id"] | succeeded]
        {succeeded, failed}

      {:error, error} -> 
        failed =  [params["id"] | failed]
        {succeeded, failed}

    end
  end)
end

My solution is same as @APB9785 - it was not visible when i was composing this post. :+1:

5 Likes

It’s unbelievable how fast someone get’s help in this forum! This is very much appreciated. Just learnt something new. I’ve seen the reduce function in other programming languages too, but I’ve never used it.

4 Likes

General note: you’ll get better support in places like this if you include the error message. Readers can’t try compiling this code themselves, and the error message can be helpful for debugging.

As @APB9785 mentioned, trying to rebind succeeded and failed inside a case is not going to do what you want.

The use of params is also a little confusing - the for loop suggests it’s a list of things you can pass to create_bin, but the case branches suggest it’s a map with string keys (params["id"]).

The specific response shape {list, list} here makes me think of Enum.split_with; the implementation of that looks very similar to @APB9785’s code.

6 Likes

You’re right, I should add the error message in the future. You’re also right with the confusing params. I made a mistake as I’ve simplified my actual code to present it here. I’ve corrected it in my question.

1 Like

For people new to functional programming, IMO it is better to pretend there is no list comprehension. It is a syntax sugar to write shorter code but non-essential other wise. Similar thing can be said on with.

If you limit the set of concepts to a smaller and orthogonal one, you would get better fluency and understanding.

5 Likes

for is an unfortunate construct in Elixir IMO. Especially for beginners who come from imperative languages. Because it’s not a for, it’s a list comprehension

list_of_transformed_data = for x <- some_list_of_data, do
   transform(x)
end

Is equivalent to:

%% erlang

list_of_transformed_data = [ transform(X) || X <- Some_list_of_data ]
# python

list_of_transformed_data = [ transform(x) for x in some_list_of_data ]

And for languages that don’t have comprehensions:

// Javascript

const list_of_transformed_data = some_list_of_data.map(x => transform(x));

And as Javascript example shows, you can view Elixir’s for as an Enum.map (it’ not strictly true, but it helps with the metal model):


list_of_transformed_data = for x <- some_list_of_data, do
   transform(x)
end

# is functionally the same as

list_of_transformed_data = Enum.map(list_of_data, fn x -> transform(x) end)

And in Elixir you can’t really change variables. You can only assign new values to variables that are in the same scope:

# elixir

x = %{ a: "b" }

x.a = "c" # this is invalid. you can't changed x's data this way

x = [] # this is valid, you can reassign a value to a variable

x = %{ a: "c" } # this is also valid, we're assigning a new value

So, all together this becomes:

succeeded = []
failed = []

## for returns a list with transformed data
result = for bin_struct <- params do
    case UserData.create_bin(bin_struct) do
      {:ok, %Bin{}} -> 
          # here we are in the scope of `case` inside the scope of `for`
          # `succeded =` introduces a new variable that shadows the global variable
          succeeded = [bin_struct["id"] | succeeded]
      {:error, error} -> 
          # here we are in the scope of `case` inside the scope of `for`
          # `failed =` introduces a new variable that shadows the global variable
          failed = [bin_struct["id"] | failed]
    end
end

## here `result` will contain a list all the transformations that happened on the data
## and global variables `succeeded` and `failed` will remain the same
2 Likes

this feels duplicated, is there a possible workaround where I can pass a variable outside of case?

  def handle_event("guess", %{"number" => guess} = _data, socket) do
    case guess === to_string(socket.assigns.random_number) do
      true ->
        message = "Your guess: #{guess} is right."
        score = socket.assigns.score + 1
        state = :won

        {
          :noreply,
          assign(socket, message: message, score: score, state: state)
        }

      false ->
        message = "Your guess: #{guess} is wrong."
        score = socket.assigns.score - 1
        state = :wrong

        {
          :noreply,
          assign(socket, message: message, score: score, state: state)
        }
    end
  end
1 Like

case is an expression – there are no statements in elixir at all – and returns the value of the last piece of code called within (like functions essentially).

So you can do this:

{message, score, state} = 
  case guess === to_string(socket.assigns.random_number) do
      true -> {"Your guess: #{guess} is right.", socket.assigns.score + 1, :won}
      false -> {"Your guess: #{guess} is wrong.", socket.assigns.score - 1, :wrong}
    end

 {:noreply, assign(socket, message: message, score: score, state: state)}

But also consider if/else if all you check for is a boolean value.

3 Likes

How about keeping the case and using the pin operator to match?

{message, score, state} = 
  case to_string(socket.assigns.random_number) do
    ^guess -> {"Your guess: #{guess} is right.", socket.assigns.score + 1, :won}
    _ -> {"Your guess: #{guess} is wrong.", socket.assigns.score - 1, :wrong}
  end

{:noreply, assign(socket, message: message, score: score, state: state)}
3 Likes

@LostKobrakai @APB9785

thats an ah ha moment for me, thank you for making it clear for me! :pray: :pray: :pray:

1 Like