I run in a situation where I need to query multiple things inside database to prepare data, and want to return error when any of these things is not exists.
I run into pyramid of if. Is there better way to use pattern matching or with to handle this?
# Assuming get(id) is database hit, so we don't want to hit database for everything up-front
def some_func(id1, id2, id3) do
if EntityA.get(id1) == nil do
{:error, :not_found}
else
if EntityB.get(id2) == nil do
{:error, :not_found}
else
if EntityC.get(id3) == nil do
{:error, :not_found}
else
# Start do something
end
end
end
end
def some_func(id1, id2, id3) do
with(
eid1 when eid1 != nil <- EntityA.get(id1),
eid2 when eid2 != nil <- EntityB.get(id2),
eid3 when eid3 != nil <- EntityC.get(id3),
) do
# Start do something
else
nil -> {:error, :not_found}
end
end
Somewhat related: @OvermindDL1 say the use changes and the :error tuple needs to know which entity made the with mechanism cry. Where would you make that change? Example output would be: {:error, "Can't make EntityA happy"}
Not for me, it makes it easier to scan over (especially with guards) and I keep tag’s/label’s very short (I don’t want code going off the edge). ^.^
another way is to use monad type, there are library for that called witchcraft + algae
but you can also write a function to do similar thing
use Witchcraft
alias Algae.Maybe
def some_func(id1, id2, id3) do
result =
Maybe.from_nillable(Entity.get(id1))
>>> fn _ -> Maybe.from_nillable(Entity.get(id2)) end
>>> fn _ -> Maybe.from_nillable(Entity.get(id3)) end
case result do
%Maybe.Nothing{} -> {:error, :not_found}
%Maybe.Just{just: x} -> #do something
end
end
You can simplify that monad example too, no need to add other case:
use Witchcraft
alias Algae.Maybe
def some_func(id1, id2, id3) do
Maybe.from_nillable(Entity.get(id1))
>>> fn _ -> Maybe.from_nillable(Entity.get(id2)) end
>>> fn _ -> Maybe.from_nillable(Entity.get(id3)) end
|> case do
%Maybe.Nothing{} -> {:error, :not_found}
%Maybe.Just{just: eid3} -> #do something
end
end
Although you can’t hold the values if they need to be used, instead:
use Witchcraft
alias Algae.Maybe
def some_func(id1, id2, id3) do
Maybe.from_nillable(Entity.get(id1))
>>> fn eid1 -> Maybe.from_nillable(Entity.get(id2))
>>> fn eid2 -> Maybe.from_nillable(Entity.get(id3))
|> case do
%Maybe.Nothing{} -> {:error, :not_found}
%Maybe.Just{just: eid3} -> #do something
end end end # because elixir doesn't have a good way of handling this style natively
end
Or if we had a good functional binding operator, like just using <-, let’s fake that (like of like with but without the formatter making it look horrible and no , littered about):
require MyFancyDef
MyFancyDef.def some_func(id1, id2, id3) do
eid1 when eid1 != nil <- Entity.get(id1)
eid2 when eid2 != nil <- Entity.get(id2)
eid3 when eid3 != nil <- Entity.get(id3)
# Do whatever you want
else
nil -> {:error, :not_found}
end
(Conceptually <- in this would be like the left side being a matcher, the right being the setter, and the rest being wrapped up into a fn ... end being called with the new state, though for efficiency you can just have it early out or something by wrapping the rest in a conditional.)
after playing more with the library
find out that it have something similar to with for monad, but called chain instead
use Witchcraft
use Witchcraft.Chain
alias Algae.Maybe
def some_func(id1, id2, id3) do
chain do
e1 <- Maybe.from_nillable(Entity.get(id1))
e2 <- Maybe.from_nillable(Entity.get(id2))
e3 <- Maybe.from_nillable(Entity.get(id3))
# something with e1, e2 e3
end
|> case do
%Maybe.Nothing{} -> nil
%Maybe.Just{just: x} -> # something
end
end