Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion of smell: mixing extraction and conditionals in pattern matching #9

Closed
josevalim opened this issue Mar 28, 2022 · 13 comments
Labels
suggestion of smell Elixir-specific smells suggested by the community

Comments

@josevalim
Copy link
Contributor

Take the following code:

def drive(%User{name: name, age: age}) when age >= 18 do
  "#{name} can drive"
end

def drive(%User{name: name, age: age}) when age < 18 do
  "#{name} cannot drive"
end

In the code above, the pattern matching of the user is extracting fields for further usage (name) and for pattern/guard checking (age). While the example above is fine, once you have too many clauses or too many arguments, it becomes hard to know which parts are used for pattern/guards and what is used only inside the body. A suggestion is to extract only pattern/guard related variables in the signature once you have many arguments or multiple clauses:

def drive(%User{age: age} = user) when age >= 18 do
  %User{name: name} = user
  "#{name} can drive"
end

def drive(%User{age: age} = user) when age < 18 do
  %User{name: name} = user
  "#{name} cannot drive"
end

This is also related to "Complex multi-clause function".

@fishtreesugar
Copy link

fishtreesugar commented Mar 28, 2022

I am missing "non-exhaustive pattern" checking in language like Haskell. We're very easily create a partial function when conditions more complicated or more and more clauses.

or How to "make illegal states unrepresentable" in Elixir?

@josevalim
Copy link
Contributor Author

It think that’s a separate discussion as it is more about language features and this one would require a type system and even then exhaustiveness checks may not be that straightforward. In the lack of static types, you make illegal states unrepresentable by not representing them. :)

@lucasvegi lucasvegi added the suggestion of smell Elixir-specific smells suggested by the community label Apr 7, 2022
@lucasvegi
Copy link
Owner

Great suggestion @josevalim!

It is really related to "Complex multi-clause function", but with implications of its own. I'll add it to the catalog.

I thought of using a shorter and more objective name, like "Complex extraction in function signature". What do you think about him?

@lucasvegi
Copy link
Owner

Maybe "Complex extraction in signature" to get even smaller... :)

@josevalim
Copy link
Contributor Author

Either that or "Complex extraction in clauses", since it may apply to case, receive, etc. :)

@lucasvegi
Copy link
Owner

Great! "Complex extraction in clauses" sounds good!

@lucasvegi
Copy link
Owner

I just added this smell to the catalog. What do you think?

https://github.com/lucasvegi/Elixir-Code-Smells#complex-extraction-in-clauses

@tiagoefmoraes
Copy link

What do you think of leaving the Struct out of the extraction?

def drive(%User{age: age} = user) when age >= 18 do
-  %User{name: name} = user
+  %{name: name} = user
  "#{name} can drive"
end

@josevalim
Copy link
Contributor Author

Looks great, both ways are fine by me! :)

@lucasvegi
Copy link
Owner

lucasvegi commented Apr 12, 2022

Hi Tiago,

Thanks for your comment. Your way is valid too. But I think the ideal would be to standardize the extraction forms (in the function signature and internally). I particularly think that the extraction using the struct is more in line with the smell description, so I prefer to leave it as is. :)

Do you think we can close this issue (@josevalim and @tiagoefmoraes)?

@AdamT
Copy link

AdamT commented Apr 13, 2022

Hi there,

I know this is closed but just had a question about why a user name is being extracted when we can avoid most of the typing by calling dot .:

Before

def drive(%User{age: age} = user) when age >= 18 do
  %User{name: name} = user
  "#{name} can drive"
end

def drive(%User{age: age} = user) when age < 18 do
  %User{name: name} = user
  "#{name} cannot drive"
end

After:

def drive(%User{age: age} = user) when age >= 18 do
  "#{user.name} can drive"
end

def drive(%User{age: age} = user) when age < 18 do
  "#{user.name} cannot drive"
end

By removing the extraction in the method body we make the code "easier" (maybe?) to read. That also means the complexity of the function is where most of the code is, in this example it's in the function definition which means users will focus more attention on the function definition when skimming. Lastly, we know that a user has been pattern matched in, so it is safe to know we can call user.name. Just because we were previously able to pattern match a value out of a struct on the function definition doesn't mean we have to use it in the body. Once moved into the body we can re-evaluate the best way to get the name.

Maybe this is another code smell discussion 😅 "Don't use extraction when a dot will suffice" 😄

(Thanks for all the work btw!!!)

@josevalim
Copy link
Contributor Author

Good point! The dot and pattern matching are semantically equivalent in this case. You could use the dot, but if you prefer to use dot, then it is less likely you will run into this issue anyway. :D so I would personally keep it as is!

@lucasvegi
Copy link
Owner

Excellent observation @AdamT!

I will even think about the suggestion of "Don't use extraction when a dot will suffice".

Anyway, about the current smell (Complex extraction in clauses), I think @josevalim comment represents what I think too:

You could use the dot, but if you prefer to use dot, then it is less likely you will run into this issue anyway

The idea behind this smell is not necessarily to say that pattern matching/extraction is a bad practice and should not be used. I think it's more along the lines of: "If you're going to use this feature, it's better to use it this way..." 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
suggestion of smell Elixir-specific smells suggested by the community
Projects
None yet
Development

No branches or pull requests

5 participants