PragTob

PragTob

Pattern matching in function declarations when not needed for multiple clauses - yes or no?

Title basically says it all - to be more precise: When writing function do you already pattern match on everything you need in the function head even if it is not relevant to selecting multiple clauses?

Afaik the rule in elixir-core is to only match on things that are needed to select the correct clause and other matching is done in the function body.

To illustrate a bit here is an example from benchee and the alternative option courtesy of @devonestes:

  # match everything in the head
  def run_before_scenario(
        %Scenario{
          before_scenario: local_before_scenario,
          input: input
        },
        %ScenarioContext{
          config: %{before_scenario: global_before_scenario}
        }
      ) do
    input
    |> run_before_function(global_before_scenario)
    |> run_before_function(local_before_scenario)
end

or:

  # match in body
  def run_before_scenario(scenario, scenario_context) do
    %Scenario{
      before_scenario: local_before_scenario,
      input: input
    } = scenario

    %ScenarioContext{
      config: %{before_scenario: global_before_scenario}
    } = scenario_context

    input
    |> run_before_function(global_before_scenario)
    |> run_before_function(local_before_scenario)
  end

Admittedly long pattern matches in the function definition like above are very unusual starting out but they’ve grown on me. I see them almost as a “structual” type definition of my function “this function needs 2 maps with the following keys” and I really like that.

The second version almost has the same benefits just more in the body and a much cleaner/non multi line function head. Also as this is what is done in elixir-core I should probably trust them :smiley:

So, what’s everyone doing - multiline function heads without clause match need, Yay! or Nay!

Most Liked

rvirding

rvirding

Creator of Erlang

I agree you should definitely be careful in moving internal knowledge out of a function to its caller. I don’t agree with James Hague here as it means you are forcing knowledge of the internals of the data structure out where it is probably not supposed to be visible. If you were to follow this rule for every record-like data structure then you would have to know about their internals everywhere. And yes, this is pushing it ad absurdum but you get the idea.

idi527

idi527

Yay.

The only problem I’ve faced with this approach so far are cryptic error messages on a failed match:

** (FunctionClauseError) no function clause matching in Test.run_before_scenario/2

With a failed match inside the function body the error messages are a bit less cryptic since they at least provide the unmatched value:

** (MatchError) no match of right hand side value: nil
dom

dom

I think heavy struct pattern matching is a sign that the function is getting passed too much stuff through its arguments. James Hague puts it like this:

I’d go so far as to say that if you pass a record-like data structure to a function and any of the elements in that structure aren’t being used, then you should be operating on a simpler set of values and not a data structure. Keep the data flow obvious.

The easiest way you can make it obvious that the function only uses this and that is to pass them directly as arguments, without using a struct:

    run_before_scenario(input, scenario.before_scenario, scenario_context.config.before_scenario)

    def run_before_scenario(input, before_scenario, global_before_scenario)
      ...
    end

It’s not immediately pretty, but it makes it easier to see what the function is about: it just takes some
kind of callbacks and applies the non-nil ones. We can generalize it to make that clearer and reduce the clutter, something like:

    callbacks = [scenario_context.config.before_scenario, scenario.before_scenario]
    scenario_input = run_optional_callbacks(scenario.input, callbacks)

    def run_optional_callbacks(input, callbacks)
      callbacks
      |> Enum.reject(&is_nil/1)
      |> Enum.reduce(input, fn (callback, input) -> callback.(input) end)
    end

This removes the “deep pattern matching in function head” problem, and the helper function becomes a lot easier to unit test, since it only takes generic Erlang types as arguments. It’s also easier to change the struct when it’s not weaved through all layers of the application.

Where Next?

Popular in Discussions Top

mbenatti
Following https://github.com/tbrand/which_is_the_fastest |> https://raw.githubusercontent.com/tbrand/which_is_the_fastest/master/imgs...
New
und0ck3d
Hello everyone! A few days ago I’ve created a topic here about how people were creating CMSs with Elixir and Phoenix. I’ve been studying...
New
pillaiindu
In django there is a cache framework backed by memcached. Rails also puts a lot of emphasis on caching, and even the idea of russian-doll...
New
owaisqayum
I have a sample string sentence = "Hello, world ... 123 *** ^%&*())^% %%:>" From this string, I want to only keep the integers, ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
cvkmohan
The upcoming Phoenix 1.6 release looks very interesting. Became a habit to watch the commits - and - what they are bringing in. phx.gen...
New
arcanemachine
https://nitter.net/josevalim/status/1744395345872683471 https://twitter.com/josevalim/status/1744395345872683471
New

Other popular topics Top

grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 54006 488
New
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
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
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
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 44139 214
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

We're in Beta

About us Mission Statement