Why does the order for making a redirect and putting a new variable in conn matter?

I have created a simple plug for basic redirects within my app.

I needed to make sure that if the request_path is “/”, the user will be redirected to “/en”.
Also, if the domain doesn’t match, the user has to be redirected to a different domain.

Why do I get this awful error when my code is in this order

  request_path = conn.request_path

    if (request_path == "/") do
      conn
      |> Phoenix.Controller.redirect(external: "http://localhost:4000/en")
      |> Plug.Conn.halt()
    end

    Map.put_new(conn, :locale, "en")

Whereas if my code is in this order, everything works just fine

    request_path = conn.request_path

    conn = Map.put_new(conn, :locale, "en")

    if (request_path == "/") do
      conn
      |> Phoenix.Controller.redirect(external: "http://localhost:4000/en")
      |> Plug.Conn.halt()
    end

The issue here is that you are not re-binding the result of the if statement to the conn variable, so the changes you’re making to conn in the if “disappear”. The line following the if is adding locale: "end" and then returning that as the result of the function, which is not what cowboy is expecting, and it fails with that terrible message.

This code would probably work by changing it to

conn = if (request_path == "/") do
         conn
         |> Phoenix.Controller.redirect(external: "http://localhost:4000/en")
         |> Plug.Conn.halt()
       else
         conn
       end

Map.put_new(conn, :locale, "en")

but the second code makes more sense.

1 Like

:thinking: this doesn’t seem to be the case because Map.put_new(conn, :locale, "en") returns conn.

Even when we change the code to, it’ll throw an error

request_path = conn.request_path

    if (request_path == "/") do
      conn
      |> Phoenix.Controller.redirect(external: "http://localhost:4000/en")
      |> Plug.Conn.halt()
    else
      conn
    end

    Map.put_new(conn, :locale, "en")

You’re still missing the conn = if …

It does return conn, but it’s a mutation of the conn from before the if, since you’re not re-binding the result of the if to conn before using Map.put_new

:man_facepalming: you are right, firstly, I didn’t notice the assignment, and secondly, I didn’t realise we could do such a thing

@03juan @LostKobrakai thanks guys for the explanations and help, I appreciate it!

1 Like

Remember Elixir is an immutable, functional language.

Values are bound to names that become part of the scope of data available to later parts of the function code. It’s a kind of pointer to specific data in memory, so conn in the function declaration points to the data passed to the function when it was invoked (executed) from some other code. Changing this bound value means the data is “copied” to another part of memory with the desired changes.

After the change, the bound name conn still points to the original data that came from the function invocation, unless you re-bind it again like in conn = if(...)

So then Map.put_new(conn, ... can update the updated conn, and since that is the last line of code in your first example there’s no need to re-bind conn again, because this last value is what will be returned by the function.

Pointing to immutability is not really a correct way to explain this. The behaviour doesn’t have much to do with immutability, but with how the lexical scoping in elixir works. Otherwise you’d need to argue that variable shadowing/rebinding shouldn’t work as well within the same scope. Immutability is mostly handled transparently for you unrelated to scoping rules.

The lexical scoping in elixir means variables assigned in outer scopes can be accessed within inner scopes, but variable assignment happens only for the current scope and does not leak to outer scopes. expressions, do/else/… blocks as as well as anonymous functions are scope boundries.

def abc(conn) do # 1. parameter value assigned to conn in functions scope
  IO.inspect(conn) # 2. conn (function scope) accessed while being in the scope of the function

  conn = # 5. (re)assign conn (function scope) the value of the if expression
    if is_active(conn) do # 3. conn (function scope) accessed while being in the scope of the if expression
      # 4.a conn (function scope) accessed from :do block, assigned to conn in the do blocks scope
      conn = assign(conn, :active, true) 
      conn # evaluate to value of conn (do block scope)
    else
      # 4.b conn (function scope) accessed from :else block, assigned to conn in the else blocks scope
      conn = assign(conn, :active, true) 
      conn # evaluate to value of conn (else block scope)
    end

  conn # return value of conn (function scope)
end

Skipping step 5. would return the unmodified conn as passed in to the function, given there was never a reassignment for conn within the scope of the function.

The other part involved is that if/case/cond are not statements like in many other languages, but also expressions. That means they evaluate to a value you can assign to a variable. Given the scoping rules explained above this is the only way to get values out of those expressions’ “bodies”.

All this for sure aligns with the immutability story, but it’s tech. not related. You can find lexical scoping in OO languages as well, though usually only around nested or anonymous functions.

5 Likes

Thank you for the clarification