I agree with @peerreynders, and I’m no expert when it comes to functional programming (or anything in general for that matter), but I have found that the more I use elixir the less I want to use the regular imperative way of doing data flow.
I still get entangled in it sometimes, but I would say that elixir’s warnings most of the time will nudge me into a better solution.
What you’ve shown could be written as:
def execute_logic(%{optional_key1: opt_k1, optional_key2: opt_k2}), do: execute_logic2()
def execute_logic, do: something_else()
Which makes it trivial to add a new case, such as, imagine, now you want to do another thing if only optional key1 is present:
def execute_logic(%{optional_key1: opt_k1}), do: execute_for_key1(opt_k1)
Or say that you want to do something when k1 is a certain value, instead of yet another branch of logic and checks, you can:
def execute_logic(%{optional_key1: "special", optional_key2: opt_k2}), do: execute_logic3()
If you place these all together (in the correct order - funnily enough this pattern makes your code position in a file dictate the behaviour of your program):
def execute_logic(%{optional_key1: opt_k1, optional_key2: opt_k2}), do: execute_logic2(opt_k1, opt_k2)
def execute_logic(%{optional_key1: "special", optional_key2: opt_k2} = all_data), do: execute_logic3(all_data)
def execute_logic(%{optional_key1: opt_k1}), do: execute_for_key1(opt_k1)
def execute_logic(something), do: something_else(something)
You can see that not only does it take less lines (although you could reduce the ones in the sample - and also knowing the number of lines is not the ultimate measure either for simplicity or correctness), but it also takes care of 2 new other cases.
I think this is much more readable than anything you could come up with if else.
Not only that, the code is also slighty more optimized, because (if I understand correctly) erlang/elixir will be able to optimize the function calling when pattern matching on function heads (this is most likely not gonna impact the real usage of the program unless at scale, but still…).
It’s also more useful, because now you could call execute_logic from anywhere without duplicating the if-else logic on another function.
And lastly, it can be useful to create pipeable flows of data, because you now have no branching, if you design the functions correctly you can do execute_logic(something) |> process_logic_result