Guards! Guards
32 points by hauleth
32 points by hauleth
I'm not an elixir programmer, but the thing that's most surprising to me about the last example is that errors in guard expressions cause the guard to be "skipped", instead of propagating to the caller. I think I can see why, but I'm not surprised it leads to counterintuitive results.
Guards are used to determine which clause to use, so if they raised to the caller that would defeat the purpose.
Ironic, given that Erlang's API design was meant to facilitate intentional programming as described by Armstrong in the Erlang thesis (p109/s4.5). In it, he outlines a set of functions for interrogating a dict so that the programmer's intention is clear (annotations mine):
% Programmer expects key to be present, so raise an error on failure
dict:fetch(Key, Dict) = Val | EXIT
% Programmer expects key to sometimes exist, and to direct control flow based on its presence
dict:search(Key, Dict) = {found, Val} | not_found.
% Programmer only needs to test for the presence of the key
dict:is_key(Key, Dict) = Boolean
Elixir's is_map_key/2 seems to break this by raising if the "dict" argument is not a dict, and exception failures (reasonably, I suppose) cause the entire guard clause to fail. I think an alternative language where or caught exceptions and coalesced them to false would be more surprising in other cases.
is_map_key/2 is actually completely vanilla Erlang!
https://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
Even more interesting, thanks for the correction. I can see compelling arguments for that function to return false as well as raise. The notes on well-behaved functions (WBFs) in s5.3.1/PDF page 138/page 126 would imply that the intuitive spec "return true if Key k is in Map m" would have to raise to meet the definition of WBFs, and I could see someone making that behaviour explicit in OTP.
I learned something. Thanks.
But...
Why avoid the Pratchett reference? Somewhere, Death is palming his forehead. Here's my quick rewrite of the code:
defmodule GuardsGuards do
def sam(x) when is_integer(x) or is_map_key(x, :vimes), do: true
def sam(x), do: false
def sybil(x) when is_map_key(x, :ramkin) or is_integer(x), do: true
def sybil(x), do: false
def carrot(x) when (is_map(x) and is_map_key(x, :corporal)) or is_integer(x), do: true
def carrot(x), do: false
end
There's two interesting points here IMO. Failing--not falsey--gaurds will fail the whole expression. And somewhat unintuitively, is_map_key does not imply a check for is_map. My added 3rd variant works as one would expect. I find is_map_key's behavior a bit inconsistent (and therefore surprising/unexpected). It would be interesting to evaluate the other is_... guards to see which ones are "bullet proof" and which ones are "must evaluate with expectations".
I was curious and went and looked and evaluated some. Near as I can tell, is_map_key is the only is_ guard that requires a certain kind of argument. All other is_ functions (which imply booleaness) will always return true|false; they never fail.
Yeah, but the same apply to hd/1, tl/1, binary_part/3, map_size/1, length/1, byte_size/1, etc.
But those ones don't start with is_. Is implies a predicate function. So you expect to use logical operators with them. I'm not likely to be inclined to try
map_size(x) or length(y)
I agree with Pratchett references, but there is a heatwave right now and my brain do not work as expected.
An interesting Elixir style question arises out of this. The example is fun and illustrative, but personally, I eschew guards in favor of matching whenever I can, generally speaking. There's always exceptions of course, but I would probably have written these functions as multi functions as such:
def a(%{foo: _x}), do: true
def a(x) when is_integer(x), do: true
def a(_), do: false
Haskell guards are slightly different, as in Haskell you can call arbitrary functions in guards. Erlang limits set of allowed functions there.
Wait, what? How?
There is fixed set of functions callable in guards. You cannot call arbitrary functions there.
Whoa. https://www.erlang.org/doc/system/expressions.html#guard-expressions
As a non-Erlang developer I find this much more surprising than the original article. But it also makes that even stranger: that there is a hand-picked list of allowed functions and it explicitly includes some partial functions. Weird.