overcomeoj

overcomeoj

When to use perform_action("type", ...) vs perform_type(...)

What is the community guidelines on when you should pattern match a function vs when you should write its own? Eg:

Style 1

Extract something to match on and pass all the data down:

def perform_action(%{action: type} = data), do: perform_action(type, data)
def perform_action(:save, data), do: :ok
def perform_action(:delete, data), do: :ok
def perform_action(:revert, data), do: :ok
def perform_action(_, data), do: :error

Pros

  • Easier to organize like with like?
  • Allows flexibility in what data should contain at each handler
    • Including error handling (checking for required keys, etc)

Cons

  • Only one @doc block allowed
  • All functions are public (may not really be an issue)

Style 2

Get type and then call specific:

def perform_action(%{action: type} = data) do
  case type do
    :save -> perform_save(data)
    :delete -> perform_delete(data)
    :revert -> perform_delete(data)
    _ -> :error
  end
end

def perform_save(data), do: :ok
def perform_delete(data), do: :ok
def perform_revert(data), do: :ok

Pros

  • Can have per-function @doc blocks
  • Still flexible in what “downstream” functions accept and error handling is local to them
  • Probably easier to write “sub match” function headers for each action (eg: perform_save(%{sneaky: true} = data))
  • Specific functions can be private if needed
  • Lists all supported operations in one place?

Cons

  • First case could get very large?

Style 3

Extract specific data from top and pass down to functions:

def perform_action(%{action: type} = data) do
  case type do
    :save -> perform_save(data.id, data.content)
    :delete -> perform_delete(data.id)
    :revert -> perform_delete(data.id, data.date)
    _ -> :error
  end
end
def perform_save(id, content), do: :ok
def perform_delete(id), do: :ok
def perform_delete(id, date), do: :ok

Pros

  • @doc per function
  • More explicit interfaces, very obvious that save needs id + content.

Cons

  • Puts “must have key” checks in first function, which might bloat,
    • Though it could match on the type before dispatching to the specific
      function (but maybe thats just #1 with more steps…

From this extremely scientific research, it seems that #2 is maybe the best? Mostly because you can have per-function docs (though if they’re private this doesn’t actually count, @docp when?) It also seems really common with OTP to just have one named function and match on literally everything too but this is probably more a side effect of needing a generic interface - not intentionally blessed?

I am sure the most appropriate answer is, “well it depends.”, but discarding that, what does the community like to write?

Most Liked

sodapopcan

sodapopcan

If you’re dispatching based on external data (like a message) use pattern matching, but if you’re implementing known domain behaviour, definitely use a function for the various reasons you mentioned. Concrete concepts should have concrete names like Accounts.login_user or for CRUD operations Accounts.update_user. This way, you’re explicitly naming things your app does. This is especially true with CRUD because it’s highly possible that not all of your entities will support the full set of CRUD operations, eg, maybe you can only hide a post but never actually delete it. This would be far less discoverable if you’re passing atoms to a generic perform_action function (this is one thing I don’t miss about ORMs that automatically give you all CRUD actions whether you want them or not). On the other hand, things like GenServer use pattern matching as they are providing an extremely generic API to an abstract concept and, as you likely know, it’s super common to wrap their implementations in concrete function calls.

RudManusachi

RudManusachi

In this particular case they all could be variants of the same function pattern matching on action type right in the function head

def perform_action(%{action: :save} = data), do: :ok
def perform_action(%{action: :delete} = data), do: :ok
def perform_action(%{action: :revert} = data), do: :ok
def perform_action(data), do: :error

But to your question:
You probably have seen examples when private functions have the same name as public function but prefixed with do_ like

def perform_action(%{action: type} = data)  do
  do_perform_action(type, data)
end

defp do_perform_action(:save, data), do: ...
...

So that pattern is discouraged now[0] in favor of coming up with a better name (however, you still could find examples even in the source code of elixir, e.g. here)

That would make something in between Style 1 and Style 2
Name public and private functions differently from each other, but all private functions could have the same name.

def perform(%{action: type} = data), do: perform_action(type, data)

defp perform_action(:save, data), do: :ok
defp perform_action(:delete, data), do: :ok
defp perform_action(:revert, data), do: :ok
defp perform_action(_, data), do: :error

[0] - Add describe and module attributes by blatyo · Pull Request #9181 · elixir-lang/elixir · GitHub

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 42633 214
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement