How would you handle multiple null check?

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
1 Like

I would use the && operator.

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
9 Likes

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"}

1 Like

Then I would just tag it:

def some_func(id1, id2, id3) do
  with(
    {:A, eid1} when eid1 != nil <- {:A, EntityA.get(id1)},
    {:B, eid2} when eid2 != nil <- {:B, EntityB.get(id2)},
    {:C, eid3} when eid3 != nil <- {:C, EntityC.get(id3)},
  ) do
    # Start do something
  else
    {:A, nil} -> {:error, "Can't make EntityA happy"}
    {:B, nil} -> {:error, "Can't make EntityB happy"}
    {:C, nil} -> {:error, "Can't make EntityC happy"}
  end
end

And of course you can simplify it with helpers and whatever else as well (I’d use a helper function personally).

8 Likes

Thank you. @OvermindDL1

1 Like

I :heart: pattern matching.

1 Like

Note that you can reduce duplication and typing by replacing

{:A, foo} <- {:A, do_foo()}

with

{_, foo} <- {:A, do_foo()}

because usually your labels are longer than just “A”.

2 Likes

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). ^.^

2 Likes

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
1 Like

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.)

1 Like

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
1 Like

Ooo I knew a chaining call existed but not a block version that works with nil, nice!