Multiple guards in Elixir

I’m still quite new to Elixir.

As I understand we got in Elixir “multi guards” as convention to simplify one large guard with or’s?:

def boolean(value) when map_size(value) < 1 or tuple_size(value) < 1 do
  :guard_passed
end

is almost the same as:

def multiguard(value)
    when map_size(value) < 1
    when tuple_size(value) < 1 do
  :guard_passed
end

There are multiclause anonymous functions as well:

larger_than_two? = fn
    n when is_integer(n) and n > 2 -> true
    n when is_integer(n) -> false
  end

But why there is no multi guard functions? As I understand instead of them I need to write cond expression or use multi clousers, but if I got in a function definition a lot of parameters or complicated pattern matching, only cond stays quite readable.

For example in Haskell:

bmiTell :: (RealFloat a) => a -> String  
bmiTell bmi  
    | bmi <= 18.5 = "You're underweight!"  
    | bmi <= 25.0 = "You're supposedly normal."  
    | bmi <= 30.0 = "You're little too much!"  
    | otherwise   = "No way!"  

It can be handy if we got something like this in Elixir, functions with multi guard bodies? What you think? Was there some talks in past about it?

@spec bmi_tell(float()) :: String.t()
def bmi_tell(bmi)
  when bmi <= 18.5, do: "You're underweight!"
  when bmi <= 25.0, do: "You're supposedly normal."
  when bmi <= 30.0, do: "You're little too much!"
  when true,        do: "No way!"
2 Likes

It could be written as:

@spec bmi_tell(float()) :: String.t()
def bmi_tell(bmi)
  when bmi <= 18.5, do: "You're underweight!"
def bmi_tell(bmi)
  when bmi <= 25.0, do: "You're supposedly normal."
def bmi_tell(bmi)
  when bmi <= 30.0, do: "You're little too much!"
def bmi_tell(bmi)
  when true,        do: "No way!"

Yes, it’s a bit more verbose, but AFAICT the syntax you proposed is impossible to use as is, since def is a macro and it expects the keyword argument do to follow after guards.

2 Likes

I think you may be fixating on guards too much. It’s important to keep in mind that guards only allow a restricted set of expressions,

The reason for restricting the set of valid expressions is that evaluation of a guard expression must be guaranteed to be free of side effects.

Erlang -- Expressions

and that guards are a conditional extension to pattern matching - i.e. they can appear whenever a pattern match is possible. There are other alternatives for conditionals such as case/2 (which is based on pattern matching and therefore also supports guards) and cond/1.

To me cond seems most appropriate for your bmi_tell/1 example:

defmodule Demo do

  def bmi_tell_cond(bmi),
    do: (
      cond do
        bmi <= 18.5 -> "You're underweight!"
        bmi <= 25.0 -> "You're supposedly normal."
        bmi <= 30.0 -> "You're little too much!"
        true        -> "No way!"
      end)

  def bmi_tell_case(value),
    do: (
      case value do
        bmi when bmi <= 18.5 -> "You're underweight!"
        bmi when bmi <= 25.0 -> "You're supposedly normal."
        bmi when bmi <= 30.0 -> "You're little too much!"
        _                    -> "No way!"
      end)

end

IO.inspect(Demo.bmi_tell_cond(18.0))
IO.inspect(Demo.bmi_tell_cond(24.0))
IO.inspect(Demo.bmi_tell_cond(29.0))
IO.inspect(Demo.bmi_tell_cond(31.0))

IO.inspect(Demo.bmi_tell_case(18.0))
IO.inspect(Demo.bmi_tell_case(24.0))
IO.inspect(Demo.bmi_tell_case(29.0))
IO.inspect(Demo.bmi_tell_case(31.0))
3 Likes

not sure I entirely understand what you mean by multi guard…

but elixir 1.6 is bringing in defguard and defguardp - which might be relevant…

https://github.com/elixir-lang/elixir/blob/v1.6/CHANGELOG.md#defguard-and-defguardp

Yep I know it could be that way. But as I said if i got a lot of paramters it becomes quite unreadable like this below:

  defp do_something([y1, y2, m1, m2, d1, d2 | _])
    when d1 < 2, do: process(16, y1, y2, m1, m2, d1, d2)

  defp do_something([y1, y2, m1, m2, d1, d2 | _])
    when d1 < 4, do: process(20, y1, y2, m1, m2, d1, d2)

  defp do_something([y1, y2, m1, m2, d1, d2 | _])
    when d1 < 6, do: process(26, y1, y2, m1, m2, d1, d2)

  defp do_something([y1, y2, m1, m2, d1, d2 | _])
    when d1 < 8, do: process(32, y1, y2, m1, m2, d1, d2)

The only readable way is use cond as peerreyanders states. For me cond is just like if and case, trying to avoid it as I can.

PS. bmi is just an example code from Haskell tutorial.

By multi guards I mean function definition with multiple bodies depending on specified guard, just like cond but simplified:

 defp do_something([y1, y2, m1, m2, d1, d2 | _])
    when d1 < 2, do: process(16, y1, y2, m1, m2, d1, d2)
    when d1 < 4, do: process(20, y1, y2, m1, m2, d1, d2)
    when d1 < 6, do: process(26, y1, y2, m1, m2, d1, d2)
    when d1 < 8, do: process(32, y1, y2, m1, m2, d1, d2)
  end 

just like definition with cond:

 defp do_something([y1, y2, m1, m2, d1, d2 | _])
    cond do
      d1 < 2 -> process(16, y1, y2, m1, m2, d1, d2)
      d1 < 4 -> process(20, y1, y2, m1, m2, d1, d2)
      d1 < 6 -> process(26, y1, y2, m1, m2, d1, d2)
      d1 < 8 -> process(32, y1, y2, m1, m2, d1, d2)
    end
  end 

Why? Anti-if campaign?‡ Ultimately that campaign was driving towards the Replace Type Code with Polymorphism refactoring - there is nothing wrong with conditionals as long as they are used with discipline.

And ultimately multiple function clauses are just a case/2 in disguise!

‡ yet another example of “software development by slogan”

It is also important to recognize that the function clauses are not separate functions - they define parts of the same function.

The Replace Conditional with Lambda approach is closer to Replace Type Code with Strategy.

2 Likes

Wow there is even such campaign? Didn’t know about it.

There is nothing wrong about conditionals at all. I just try to avoid them if iI got such opportunity. For me personally code becomes little more readable.

Don’t get it personal peerreynders: and case/2 probably under the hood got goto instructions :wink:

Thanks for links. :slight_smile:

Well, goto is useful but unfortunately it got abused so it got abolished. Similarly mutable state can be useful but immutability by default has distinct advantages.

1 Like

or and multiple whens are not equivalent, especially when there are exceptions in the game:

iex(1)> defmodule M do                                                          
...(1)> def guard_or(x) when x <> "foo" == "xfoo" or is_integer(x), do: true    
...(1)> def guard_or(_), do: false                                              
...(1)> 
...(1)> def guard_when(x) when x <> "foo" == "xfoo" when is_integer(x), do: true
...(1)> def guard_when(_), do: false                                            
...(1)> end
{:module, M,
 <<70, 79, 82, 49, 0, 0, 4, 172, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 105,
   0, 0, 0, 11, 8, 69, 108, 105, 120, 105, 114, 46, 77, 8, 95, 95, 105, 110,
   102, 111, 95, 95, 9, 102, 117, 110, 99, ...>>, {:guard_when, 1}}
iex(2)> M.guard_or(1)                                                           
false
iex(3)> M.guard_or("x")
true
iex(4)> M.guard_when(1)             
true
iex(5)> M.guard_when("x")
true

When using or an exception makes the whole guard false, while with multiple whens only the sub-expression is considered false.

3 Likes

Literally even!

Something like this:

def blah(a) when a<0, do: 0
def blah(1), do: 1
def blah(a) when a>0, do: 2

Quite literally compiles ‘almost’ identically to (‘almost’ meaning it is more lambda’ish internally):

def blah(a) do
  case a do
    a when a<0 -> 0
    1 -> 1
    a when a>0 -> 2
    _ -> raise %MatchError{...}
  end
end

Actually it compiles into a simple dispatch tree. It’s not like ‘way’ smart in that it can combine all parts possible, but it is pretty decent. In general you can think of it as ‘testing’ the first branch, if that fails it tests the second, if that fails it tests the third, and so on. It is a linear slowdown with the number of cases (though linear more in C terms so it is still blazing fast compared to anything you could write explicitly, this is also why guard calls are restricted).

Precisely. Multiple when’s compiles to different branches in the dispatch tree, where or’s get compiled into the same dispatch branch.

3 Likes

The only difference between case and function heads is the exception generated when no clause matches - CaseClauseError vs FunctionClauseError - in internal passes the only thing the compiler works with is case, everything else is lowered into that.

Yep, an internal pass creates a trailing ‘accept anything’ case that throws that exception, but you can always add that manually yourself too. :slight_smile:

It is pretty cool how the erlang compiler works if anyone is bored and wants to look. :slight_smile: