What do you think about using `_` vs `_other`?

I am used to this syntax for a case structure (my question is about the _ -> part):

case {1, 2, 3} do
  {4, 5, 6} ->
    "This clause won't match"
  {1, x, 3} ->
    "This clause will match and bind x to 2 in this clause"
  _ ->
    "This clause would match any value"
end

A new team member introduced this into our project:

case {1, 2, 3} do
  {4, 5, 6} ->
    "This clause won't match"
  {1, x, 3} ->
    "This clause will match and bind x to 2 in this clause"
  _other ->
    "This clause would match any value"
end

So instead of a _ he/she uses _other. I do know that technically this is no different but I wonder what the majority does/thinks. I don’t want to tell that person in a code review that _other ā€œfeelsā€ funny in case everybody but me does it. Every time I see it I stumble upon it but I understand why one would use _other.

1 Like

In this case, I would use _ because the word ā€œotherā€ adds no additional context or meaning.

I do tend to prefer the named variant in function heads with multiple clauses. For instance:

def some_function(:explicit_match, _right), do: ...
def some_function(_left, :explicit_match), do: ...
def some_function(left, right), do: ...

This can be helpful especially with longer function heads that take, say, 4+ arguments, as multiple _ can get a little hard for me to parse visually.

12 Likes

Actually I did use _other a few times, but now that I think about it I probably just should have used _.

It’s probably part of an inner monologue just like when people abuse the then function because it fits the inner monologue?

Just want to +100 this as it’s one of my biggest pet peeves. Like, no %#$@ it’s ā€œother.ā€ When it comes to scanning unfamiliar code, this type of thing is just distracting. If I see _some_var, I think that something useful is being communicated. As already said, _other as a catch-all is not at all useful.

And, technically they aren’t equivalent as _other gets bound whereas _ doesn’t. I’m not advocating that is some sort of meaningful optimization, just sayin’ :upside_down_face:

3 Likes

Yeah, I dislike _other as well but many others are valuable i.e. you have a long chain of matching that checks for several types of structs / maps and then ultimately it’s fine if below you have _not_a_map -> ... – and variations of that. A proper unused variable name really helps code readability.

2 Likes

Absolutely, that’s what I was trying to convey. Another way to look at it is that matching is basically (albeit not strictly) assignment—you wouldn’t called a variable other (at least I hope you wouldn’t). So long as there is a name that makes sense I name my _s, which is the vast majority of the time.


To take this a bit further, due to Elixir being dynamic I’d vouch for writing OP’s example like this (depending on context):

case tuple do
  {4, 5, 6} ->
    "This clause won't match"
  {1, x, 3} ->
    "This clause will match and bind x to 2 in this clause"
  {_, _, _} ->
    "This clause would match any 3-tuple"
end

and if nil is allowed, add a specific nil clause. This makes it very clear what is expected an errors out quickly if it gets junk.

With multiple clauses in pattern plain _ is even more important as:

case {1, 2} do
  {_other, _other} -> :doesnt_match
  {_, _} -> :match
end
4 Likes

Oh ya, good point!

You’ll at least get a compiler warning if you do that, though.

EDIT: wait, no you won’t since _other, _other binds as I even pointed out earlier :sweat_smile:

You actually do get a warning in this case:

> mix compile
    warning: the underscored variable "_other" appears more than once in a match. This means the pattern will only match if all "_other" bind to the same value. If this is the intended behaviour, please remove the leading underscore from the variable name, otherwise give the variables different names
    │
  4 │       {_other, _other} -> :doesnt_match
    │                ~
    │
    └─ lib/compile_test.ex:4:16: CompileTest.my_fun/0
2 Likes

Well I should probably stop snap-responding then :upside_down_face:

2 Likes

Kinda surprised no one else pointed this out but if you use Credo on your project (and you should) then – unless you’ve suppressed the rule – it will give you a warning for just ā€˜_’ and tell you to give it a more meaningful name.

Now ā€œotherā€ may not be the most descriptive variable name, but without better context of what sort of thing you are actually matching it will do in a pinch.

The TL;DR though is that Credo is the compendium of idiomatic Elixir choices and a bare underscore for a variable name is not idiomatic.

1 Like

Citation needed!

Credo is for enforcing per-project standards, not general community standards, and can be customized (which is what I do).

Also, check the root module module of Phoenix:

6 Likes

We practice documentation as code, so we encourage commenting ignored pattern matches like _error to better explain the variables:

case result do
  {:ok, data} -> do_action(data)
  _error -> fail()
end

If you’re literally talking about writing _other, that’s something we would avoid and would try to come up with a better name since ā€œotherā€ is what _ -> means. The goal is to document the program, not the programming language.

2 Likes

In the spirit of documentation as code, I write:

case result do
  {:ok, data} -> do_action(data)
  {:error, _} -> fail()
end

This leaves little up to the imagination of an unsuspecting reader: they can see the exact shape and don’t have to look anything up. It also means we get a more meaningful exception if somehow we get nil or something else completely unexpected (the entire case fails the match as opposed to the catch-all failing the match). Even with safety nets we have, Elixir is still a dynamic language so anything is possible!

For me, a bare _ perfectly conveys ā€œI literally don’t care what I get here, even if it falls outside of the subset of things I’m assuming it’s going to be.ā€ Otherwise, if you’ve been writing Elixir for more than a minute and you don’t know that the opposing match of an :ok tuple isn’t some form of :error then that’s a whole other problem and naming your matches as such isn’t going to fix that.

I say that if you know the shape of the error, match on the shape! There is confidence in a bare _ that proclaims: ā€œI will literally accept any outcome!ā€ whereas _error sorta says ā€œWell, you know, I’m assuming this is going to be an error tuple… like, probably.ā€

Please note, I’m fully aware I’m being way too overly passionate about a low-stakes stylistic point :upside_down_face: I’ll do whatever a codebase’s style guide dictates I must do.

1 Like

Hmm… I use Credo on pretty much everything and _ for placeholders both in function heads and to avoid Dialyzer unmatched return complaints… But I don’t recall seeing the Credo warning you speak of. Are you sure this is stock/default Credo?

Not quite exact f.ex. I’d also do {:error, _atom} for the sake of the future me (or another dev) who would want to do something with the value sometime in TheFutureā„¢. Could save them some time looking up the API’s @spec-s.

Let me introduce you to Integer.parse/2 :003:

case Integer.parse(input) do
  {int, ""} -> "all good"
  {_int, _string_remainder} -> "oops, only partially an integer"
  :error -> "sorry, not an integer at all"
end

Ahh, it’s disabled by default. Definitely enabled at my current job (with about a dozen Elixir projects). Pretty sure it was enabled at my last job too though it’s been a few years now and hard to remember for sure.

https://hexdocs.pm/credo/Credo.Check.Consistency.UnusedVariableNames.html

Well, maybe that’s my decade plus in Ruby-land showing through. Even though I left Ruby behind almost 6 years ago, I still have RuboCop PTSD.

Both RuboCop and Credo allow customizing rules but I’ve always felt that there was a strong impetus to adopt the defaults and keep customization to a minimum. I definitely felt that way while working hard to keep RuboCop happy back in my Ruby days.

I mean, if you make Credo or RuboCop a PR check then you have to conform to the rules in order to get your work merged and complete stories. And I don’t think that’s a bad thing (the RuboCop PTSD was because I felt that it went too far and was often a source of a lot of friction). Bike-shedding is best avoided at all costs. Consistency and readability of code saves a lot of mental overhead.

But, still, not everyone gets a say in the linter rules so you have to follow them and it sets a standard. But I think I’m misremembering this one; a) it’s not on by default (as I indicated in another response here) and b) like the Phoenix example you shared, there is clearly plenty of important code out there still using bare ā€œ_ā€.

So I’m changing the nature of my chiming in here: ā€œHey y’all, if you want to settle this and codify it in your codebase, there’s a Credo check: Credo.Check.Consistency.UnusedVariableNames — Credo v1.7.8ā€ :grinning:

2 Likes

Just to make it perfectly clear, this is not an optimization, not even a slight one. The Erlang compiler can see when a variable is unused and will emit the same code for matching _ as when matching _other or some other variable that is never used. Actually, to avoid having to handle _ as a special case, the Erlang compiler replaces _ with a freshly created variable that will never be used.

9 Likes

Ehn, fair, but I don’t think this adds too much. That’s it’s ā€œunwrappingā€ an error tuple is all the information I need, though I dunno, I’ll think about it next time I’m in this situation.

Well s :poop: t, fair enough!

Oh good to know, thank you!!

1 Like