Search code examples
elixirecto

A logical condiditon in "with" operator doesn't work


I have this code:

  def edit(conn, params) do
    with m1 <- Repo.get(Model1, params["model1_id"]),
      m2 <- Repo.get(Model2, params["model2_id"]),
      !is_nil(m1) and !is_nil(m2)
    do
      # 1
      res = !is_nil(m1) and !is_nil(m2)
      IO.puts("***** res: #{res}")                              # ===> false

      IO.puts("***** m1: #{Kernel.inspect(m1)}")                # ===> prints a struct
      IO.puts("***** m1 is_nil: #{is_nil(m1)}")                 # ===> false

      IO.puts("***** m2: #{Kernel.inspect(m2)}")                # ===> nil
      IO.puts("***** m2 is_nil: #{is_nil(m2)}")                 # ===> true

    else
      #2
      _ -> raise ArgumentError, "not found"
    end
  end

The flow #1 gets executed even though m2 is nil. How can it be? How to fix it? Goal - ensure that m1 and m2 aren't nil and then execute the flow #1.


Solution

  • The with

    The with expression is used strictly for pattern matching. It is not a "chainable replacement" for if-else conditionals.

    Basically the with will go through all your clauses and try to pattern match them to the left side of the <- arrow. It will only execute one of the error clauses when the first pattern match fails (doesn't match).

    The issue with your code

    Your third line in the with is !is_nil(m1) and !is_nil(m2), which always successfully pattern matches even though the expression itself equals to false.

    The fix

    To make the code do what you actually want you should add a left side to the third line so that it is forced to pattern match:

    with m1 <- Repo.get(Model1, params["model1_id"]),
          m2 <- Repo.get(Model2, params["model2_id"]),
          {false, false} <- {is_nil(m1), is_nil(m2)} do
     ...
    

    Idiomatic Elixir

    To make the code a bit more idiomatic Elixir, you can also use Guards, which is_nil is allowed as. This would make your code look like:

    with m1 when not is_nil(m1) <- Repo.get(Model1, params["model1_id"]),
          m2 when not is_nil(m2) <- Repo.get(Model2, params["model2_id"]) do
     ...
    

    Even better readability

    One last tip would be to always focus on readability. You are writing your code for humans to read, so less stuff happening on a line is usually easier to read.

    Your code would be even more readable as:

    m1 = Repo.get(Model1, params["model1_id"])
    m2 = Repo.get(Model2, params["model2_id"])
    
    with m1 when not is_nil(m1) <- m1,
          m2 when not is_nil(m2) <- m2 do
     ...
    

    Do you actually need a with ?

    Your with is doing nothing except making sure m1 and m2 are not nil. This can easily be done with a case or if as well, since you don't actually need any pattern matching here:

    m1 = Repo.get(Model1, params["model1_id"])
    m2 = Repo.get(Model2, params["model2_id"])
    
    if !is_nil(m1) && !is_nil(m2) do
     ...