annad
Why is it bad to check for values you don't want (eg nil values)?
I was reading an earlier post: Pipe obsession anti-pattern which recommended an article entitled Good and Bad Elixir.
I’m confused by one of the recommendations in the Good and Bad Elixir article. It says: “State what you want, not what you don’t.”
EXAMPLE GIVEN:
# Don't do...
def call_service(%{req: req}) when not is_nil(req) do
# ...
end
# Do...
def call_service(%{req: req}) when is_binary(req) do
# ...
end
I use guards quite a bit to ensure that the function is not being passed a nil value in error. I gracefully manage the error, sometimes sending a message to the user to try again. I don’t understand why that would be bad. The article says, “You’d prefer to raise or crash if you receive arguments that would violate your expectations.”
Could someone explain why that is the case?
Most Liked
tfwright
I feel like it needs to be emphasized that this is really not primarily an issue with guards or even “excessive” negations/not, it’s just a logic error. If your function accepts the category “binaries” and you define that category as “not nil” this is simply incorrect. You could use all negative guards and solve the logic error, and then you might have a style issue that makes your code “confusing” etc. Conversely you can make category errors like this without using not at all (if say you guard on is_list but actually only keyword lists are expected).
dimitarvp
You already had excellent answers and did a good summary yourself.
I’ll add one more thing: make full use of pattern-matching to your own advantage. Example with the code from your OP:
def call_service(%{req: req}) when is_binary(req) do
# ...
end
def call_service(other) do
# Simplified, we did it much more fine-grained with all the `is_*` guards.
YourMonitoringService.warning(
"Unexpected value for AwesomeModule.call_service/1: #{inspect(other)}"
)
end
Note the second variant of the same function. In Elixir you can have catch-all clauses for fallback actions. In this case: report an unexpected value. This is not an error condition but it’s undesirable and the devs want to be informed so they can monitor closely who and when is sending those invalid values.
I had such requirements in a few projects, one of them was a legacy API provider where the company announced an API upgrade shortly after I joined but many users of the API still didn’t understand or read the docs and upgrade guides and kept calling the new API with the old parameters.
Thanks to the catch-all / fallback mechanism Elixir gives us we managed to track down the customers who used the API wrongly and helped them off-board their previous code and write a new one.
Just one production example for you. Obviously you can do that with any programming language but Elixir made it very easy to achieve.
tfwright
If you are expecting a string, then there is no advantage to specifying not nil instead of binary. If the function actually expects any value except nil then the first variation is not incorrect.
sodapopcan
Because this is considered Defensive Programming (I didn’t fully vet that article but it has a good TL;DR). Unless nil is a meaningful value in your design, you should rewrite your code so that the function would never receive nil anyway (Parse, Don’t Validate) and if through some circumstance it did, it would be considered an “exceptional” circumstance in which case we just fail (throw an exception). You could also look at it like: Why stop at defending against nil? If you’re going to defend against one type you should theoretically be defending against every known type in your system.
benwilson512
Relatively simply: If I do call_service(%{req: %UnexpectedThing{}}), %UnexpectedThing{} isn’t nil, but it also probably isn’t what you want. So if what you actually want is a binary, then ask for it. If literally everything is valid except nil, then do the nil check.
EDIT: And to be clear the emphasis is on avoid not checks, it isn’t so much on nil.







