Best practices: duplicating argument fields for pattern matching

I am working my way through the Pragmatic Studio intro to Elixir & OTP course. I really like it so far, but I have a question about a pattern I’m seeing crop up repeatedly. They will frequently have a struct or map that is being passed as an argument to a function. If a certain field in the struct is going to be used as an argument in a subsequent function call, particularly if function definitions will try pattern matching on that argument, the examples will duplicate the field as as separate argument. This will duplicate the data, which seems inefficient to me.

As an example:

def route(%Conv{method: "POST", path: "/api/bears"} = conv) do
  Servy.Api.BearController.create(conv, conv.params)
end
# in the Servy.Api.BearController module:
def create(conv, %{"name" => name, "type" => type}) do
  %{ conv | status: 201, resp_body: "Created a #{type} bear named #{name}!" }
end

So conv.params is essentially being passed twice. My instinct would have been to instead do this:

def route(%Conv{method: "POST", path: "/api/bears"} = conv) do
  Servy.Api.BearController.create(conv)
end
# in the Servy.Api.BearController module:
def create( %Conv{ params: %{"name" => name, "type" => type}} = conv) do
  %{ conv | status: 201, resp_body: "Created a #{type} bear named #{name}!" }
end

Is there a particularly “standard” or preferred way to handle this?

1 Like

In this particular case seems unnecessary

I don’t know about the context, but it seems like the controller is a phoenix controller, which requires conn and arguments from router.

Using separate args is useful when there is another module (e.g. phoenix router or route/1 in your example) taking care of extracting args (e.g. parsing request body based on content-header, merging route parameters, etc.)

Please note that it is technically possible to store those additional data into conn (e.g. private or assign) - but probably phoenix does not use it since it may passing larger conn between functions when those data is not necessary.

1 Like

It’s not a Phoenix project but did sort of parallel one by writing a server from scratch.

And I can see passing the arg separately from the conn to a function that only utilizes the one or two fields but if you also have to pass the conn anyway, I wouldn’t think to duplicate the data. If this is the de facto standard in Phoenix maybe that explains it.

I’m also wondering why Phoenix uses a separate argument for args in controller functions, since Plug.Conn has fields for them actually (correction to my previous comment)

  • body_params
  • query_params
  • path_params - this can be populated from phoenix router
  • params

Yeah Phoenix/Plug and Ruby on Rails has the same abstraction, which merges data from multiple places to “params”.

I agree that this is unnecessary - actually I never uses the second parameter, since I always want to explicitly match on specific params (e.g. I expect params from body, not from query string for instance).

  # in the exmaple
  def show(conn, %{"id" => id}) do
  end

  # I prefer this:
  def show(%Plug.Conn{path_params: %{"id" => id}} = conn) do
  end

However maybe there are other reasons for doing that?

I’m still a very inexperienced programmer but I spent some time learning Elm before picking up Elixir and the notion of avoiding conflicts between the state in one function versus the overall application state has stuck with me. Setting up functions like this to require a second parameter when your intent is really to act on the overall application state which is being drawn out into that parameter at least in theory opens up the possibility of calling that function with a parameter that is not in sync with the overall application state. I guess that safety net is sacrificed for readability and maintainability?

Follow up question:

The original post assumes that code like this “duplicate[s] the data” (in memory). Is that actually true? Or does the Erlang VM still only keep one copy since the data is immutable?

foo(conn, conn.params)
2 Likes

I think they want to make conn as opaque as possible. Also it is shorter to write. Don’t know about you, but I dislike very long function head

That was my first guess, but those fields are documented built-in fields not special fields actually. See the Plug.Conn docs.

And using only args introduces collision of variable names in different sources - especially between router config and request - which may lead to a bug. For example renaming path params in routers may shadow body params - so they are incorrectly coupled.

For long function heads - I prefer long correct heads over short but potentially incorrect heads, for specifying and enforcing the entry condition of the functions. I don’t think all functions should do this though - but first-line functions for outside input - such as phoenix controller - would be better to avoid such potential bugs due to one or two less lines in this example.

No, the data is not duplicated as long as the function is called from the same process. Erlang -- Processes

2 Likes

Thank you! That makes it much clearer that there is no tradeoff in efficiency for making it more explicit which arguments are going to be addressed.