Why can or be used in guard clauses but not ||?

Curious as to why this is not permitted. I suspect it has something to do with or only accepting a boolean as the first operand. But why does this matter with guard clauses?

because left || right actually compiles to something like

case left do
  v when v == false or v == nil -> right
  _ -> left
end

Which is not allowed in guards

3 Likes

Okay, so or is a more “primitive” boolean comparison operator? Is it natively implemented or does it have some other compiled form as well?

or is compiled to :erlang.or which is allowed in guards

Don’t forget to mark the solution

Is it the same for all other boolean operators, such as && and and?

Only || and && are not allowed in guards. All other boolean operators are allowed

1 Like

Yeah && and || have what is called a “short circuit” behavior where they will only evaluate the right hand side of the operand if they need to. This allows you to do things like:

value = params[:some_opt_key] || raise "this parameter is required!

If the left hand side is “truthy” then || short circuits and doesn’t evaluate the raise at all. This sort of branching behavior doesn’t even make sense in guards.

1 Like

It makes sense, and it could’ve been implemented in guards:

For example, left || right could be translated to (left == nil or left == false) and right.

The short circuit behavior cannot be implemented in guards because it doesn’t even make sense in guards. Guards don’t have branches.

The truthy vs falsey could be yes, but personally the distinction between || and or is really nice because each behaves exactly the same way whether in a guard or not.

|| applies to any terms, so this fails on several fronts:

  • if left isn’t nil or false, this returns false instead of left
  • if left is nil or false but right isn’t a boolean, it crashes

Guards can have branches: You could in principle get it into a guard using orelse/andalso. I believe that elixir compiles down to orelse and andalso for its binary or/and operators anyways

Yeah, :erlang.orelse should do. Give me some time to think about the correct one…

Yep. it is possible. Here’s the complete implementation

defmodule X do

  defguard truthy(x) when x != false and x != nil
  defguard a ||| b when
    :erlang.andalso(
      :erlang.orelse(
        :erlang.andalso(truthy(a), true),
        b
      ),
      :erlang.orelse((not truthy(a) and b), a)
    )

  defmacrop check(left, right) do
    result = left || right
    quote do
      must_be =
        case 1 do
          _ when unquote(result) -> 1
          _ -> 2
        end

      got =
        case 1 do
          _ when unquote(left) ||| unquote(right) -> 1
          _ -> 2
        end

      if must_be != got do
        IO.inspect unquote(left), label: :left
        IO.inspect unquote(right), label: :right
        IO.inspect unquote(result), label: :results_in
        IO.inspect must_be, label: :must_be
        IO.inspect got, label: :got
        raise "Stop"
      end
    end
  end

  def test do
    check(nil, true)
    check(false, true)
    check(true, true)
    check(1, true)

    check(nil, false)
    check(false, false)
    check(true, false)
    check(1, false)

    check(nil, 123)
    check(false, 123)
    check(true, 123)
    check(1, 123)

    IO.puts "success"
  end
end

X.test

@al2o3cr @ityonemo @benwilson512 , check this out

Worth noting that in guards, or is precisely the same as calling :erlang.orelse/2.

This might work for literals, but I’m not sure it works for arbitrary expressions—in general, nor successful at not evaluating the second branch.

In general there’s not much reason to want || in these cases, as we are not working with arbitrary expressions—we know that functions allowed in guards are always idempotent, and don’t have side effects, so there’s little gain in suppressing them!

1 Like

This might work for literals, but I’m not sure it works for arbitrary expressions—in general, nor successful at not evaluating the second branch.

I believe this works correctly. There might be some reevaluations

In general there’s not much reason to want || in these cases

Yeah, that’s true. I did this just to show that porting || to guards is possible

Yeah, it works fine with the expressions:

case 1 do
  _ when (1 > 2) ||| (3 < 4) -> 1
  _ -> 2
end

Not necessarily, you might want to pipe a default value into a function, for example:

is_map_key(some_map, something_nillable ||| :erlang.map_get(:default_key, settings_map))

Of course that guards have branches and or also short circuit…