Guard clause on match operator

I came across this blog post (linked from Elixir Radar) about implementing a match operator that supports guard clauses. And it reminded me that this is a minor annoyance that I come across fairly frequently.

For instance:

# Invalid Syntax
{:ok, x} when is_integer(x) = my_function()

# So I have to do:
{:ok, x} = my_function()
true = is_integer(x)

It seems unlikely that it would just have been overlooked, so I assume there’s some actual reason why the guard syntax isn’t supported. I wonder if anyone knows what it is?

Is that really necessary? I would be horrified to see that kind of code in a complex project.

IMO using typespecs in this case for my_function is the way to go, otherwise there is always the option of defining a small private or lambda function for that check.

Example of one of those functions:

def with_integer(fun) when is_function(fun, 0) do
    result = fun.()

    case is_integer(result) do
      true -> {:ok, result}
      false -> {:error, "not an integer"}
    end
  end

But I think that this or any other solutions in this ballpark are much worse than using typespecs and catching possible type errors at compile-time.

1 Like

Well, typespecs are (very) far from infallible. I use them extensively, and they miss a lot. I like to program defensively, so I use guard clauses a lot to ensure that code fails early. And actually it seems this will work nicely with the upcoming typesystem. Maybe once the compiler actually can guarantee a type, I’ll start moving to compile-time type checking instead of guards.

But actually that’s all by-the-by. The interesting thing here is why guard clauses are not allowed, syntactically, for patterns to the left of match operators.

In what regard do they miss? Are we talking about bugs that are currently present in dialyzer or other static analysis tools?

Regardless of people using them that way, guards are not meant to be type assertions which is why I assume this doesn’t exist. Defensive programming goes against the whole OTP philosophy, really!

You can do this if you want:

y = case x do x when is_interger(x) -> x end

Though that is pretty gnarly.

As far as I understand it, they also won’t be strictly necessary for the type system. For example:

def concat(a, b) do
  a <> b
end

<> only works on strings, so the type checker will know what to do. Adding is_binary guards would be redundant.

2 Likes

My opinion: the main purpose of guards is to support branching, not to ensure certain invariants. If I see a match clause error, it’s almost always a bug in that code, not a bug in the caller. If I see a function clause error, it’s almost always a bug in the caller. I also feel like invariants should be checked at boundaries, not scattered throughout the code. Guards on match clauses would, I think, encourage a style of programming that is more difficult to understand and harder to debug. My 2c, opinion could change.

5 Likes

Really? To me this is an odd claim. The planned Elixir typesystem specifically intends to build on guards as type assertions. Additionally, type assertion in a guard is de-facto useful to help code fail early on unexpected input.

I’d go so far as to say that guards semantically are pattern matches. They offer programmable extension to patterns.

We must be talking at crossed purposes, because I’d say that OTP is pretty much the embodiment of defensive programming.

I think these are the same thing: if you hit a condition for which there is no matching pattern to branch to, you have failed some invariant. In other words, you are using patterns to ensure that data flowing through your functions conform to some expected pattern. We rely on this for fast and efficient failure on unexpected data.

Yeah, I definitely agree with this. But I’d also 100% prefer a pattern match error near some unexpected data over some obscure key :whatever not found in: nil waaay down in the stack.

2 Likes

Absolutely not the case. The “let it crash” philosophy explicitly says that you handle only the expected cases, not all edge cases. The runtime facilitates that extremely well.

As mentioned before, while guards can be used as type assertion, their main role is to do code branching. You start overusing them everywhere to enforce types, you get code that is not readable, has a runtime penalty and loses the value runtime offers. In that case, using a static typed language like golang will bring you more value for your invested time.

Indeed - I like my code to handle only the expected data, and crash early if it does not conform to those expectations.

1 Like

“Defensive programming” usually refers to the more imperative approach, where you try anticipate potential exceptions (fail if my arguments are null; fail if my values are outside acceptable range).

Contrast that to OTP’s approach of declaratively defining success cases and otherwise “letting it crash” (which it sounds like you are already doing in practice).

Not trying to patronize; seems like just a terminology mismatch.

1 Like

Yeah, this is definitely just a difference in where we’re drawing lines for our definitions. When I think about being defensive in my programming style, I’m including the semantics that we get from OTP. So OTP enables a defensive style by providing automated crash-handling mechanisms on unhappy paths. So the defensiveness is largely “outsourced” to the underlying frameworks when I’m coding.

But the way that manifests is that I’m very explicit about what is the happy path. And actually I think specifying exactly how valid data should look is essential to ensure code fails early and is surfaced in a way that makes it easy to solve.

So all of that to say - I absolutely believe that restricting pattern matches to define a strict happy path is important. And I include all forms of patterns and guards in that definition, which brings me back to - why no guards on match operators?

4 Likes

I have trouble imagining a scenario in my own code when I wouldn’t use a case for something that has more than one valid return value.

Also think it’s rare for me to write a function that has multiple return types (as distinct from multiple return patterns).

1 Like

{:ok, x} when match?(is_integer x, my_function())

The problem is this is a macro so these functions won’t be evaluated, but you can override the Kernel.match? With your own and using bindings you could have these parameters evaluated prior to being passed to the macro.

Inside your custom macro you would then use the Kernal.match? this will let you know whether the first arg value is of a matching pattern to the second.

Actually,


fun = fn 5 -> [ok: 5] end

match? [ok: 5], fun(5)

true

works from Kernel, but match? is considered a “case” orderflow, so it won’t work with guards. I don’t know if a custom version will have the same outcome.

1 Like

I’d rather write:

defp unwrap_integer!({:ok, x}) when is_integer(x), do: x

x = my_function() |> unwrap_integer!()

Instead of asking “why not”, I’d rather ask “how do I write what I want using what is available to me”

2 Likes

Unless I’ve lost track of your train of thought, are you saying that a success case must always have more than one valid return value? I’m on board with the top quote, for sure: I like to define the success case, and for me that means sometimes I just want to specify that the data should be an integer when I extract it from a map, for instance:

%{"my_key" => value} when is_integer(value) = my_map

I could use a case, for sure. But it would use two additional lines and add unnecessary noise. And I’m just wondering why this more concise syntax is not available.

Note that adding additional branches to a case here to handle an other case would be needlessly defensive - I don’t need to pattern-match an unexpected error case when I could just let it crash out.

1 Like

OK, so that covers the integer case. Now you need to write the same function again for every possible combination :stuck_out_tongue: Imagine I’m extracting an integer from a struct field, for instance?

This does sound sensible, but actually I question the wisdom. Naturally, we can only use what we have available to use. That is a truism. But questioning why things are the way they are can lead us to improve the whole shebang for everyone.

I thought I helped?

You have your pet peeve and I have mine. This is free software so let the do’er decide.

Definitely not!

I’m trying to imagine circumstances where return value x could have multiple possible types but you wouldn’t want to branch on them with case.

2 Likes

I don’t understand the criticism towards OP here, especially by @sodapopcan and @D4no0.

Guards “should not” be used for type checking? Why not? How does it make the code bad or unreadable or untenable? Nobody is saying “do it on every assignment”. Do it at the function edges as @zachallaun alluded to.

Not sure I am not splitting hairs here because I might be taking your comment too verbatim – let me know (you might also include pattern-matching in function heads but I assumed you didn’t).

Still, picture my case, when a very old API returns prices in three different types because the old code had subtle bugs that nobody wants to fix, so it can be an integer, a float, or a string. So here’s an ancient helper I wrote (back in summer 2018) about dealing with this and normalizing the return into normal ok/error tuples:

def get_price(%{"currency" => currency, price => int}) when is_number(int) do
  {:ok, Decimal.new(int)}
end

def get_price(%{"currency" => currency, price => float}) when is_float(float) do
  {:ok, Decimal.from_float(float)}
end

def get_price(%{"currency" => currency, price => text}) when is_binary(text) do
  case Decimal.parse(text) do
    {decimal, ""} => {:ok, decimal}
    {_decimal, remnant} => {:error, :mixed_input}
    :error => {:error, :invalid_input}
  end
end

(Yeah, there’s Decimal.cast now. Back then it didn’t exist, here’s the PR that added it, and it even had a different name: Add Decimal.from_any/1 function to handle int, float, and binary input by eqmvii · Pull Request #116 · ericmj/decimal · GitHub)


But overall I agree with @zachallaun the most here: invariants can and should be coded (and at function edges) because they increase predictability and eliminate runtime errors. That’s not necessarily defensive programming. There’s a balance and I personally don’t lean on the “don’t check for anything” extreme.

Here’s an unpopular opinion: “let it crash” is neither a panacea nor the dominating technique to deal with the external world in production Erlang / Elixir code.

Yes, you are letting stuff crash in the first iterations of your code but once you find the erroneous case via your APM / telemetry system then you protect against it.

Is making sure your production code doesn’t fall over on its face on every invalid input “defensive programming”? Hope that’s not the claim that’s being put forward in defense of “don’t use guards for type checking”.

Or maybe I misunderstood the whole thing and you guys just meant “let it crash on unrecoverable errors” in which case I’ll immediately agree.

2 Likes