crabonature
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!"
Most Liked
NobbZ
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.
peerreynders
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.
Expressions — Erlang System Documentation v29.0.2
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))
OvermindDL1
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.
mudasobwa
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.
peerreynders
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.









