1. 19
  1.  

  2. 10

    Bonus hack:

    I recently wrote a function to do the following: look for a config variable, possibly under a namespace, in the given opts as well as the Application env. I’ve written this at least six times before, and none of them looked great if they could handle false values. Until I happened upon this lovely, somewhat cockeyed use of with:

      def config(namespace \\ :none, key, opts) do
        with nil <- opts[namespace][key],
             nil <- Application.get_env(:my_app, namespace)[key],
             nil <- @config_defaults[namespace][key],
             nil <- opts[key],
             nil <- Application.get_env(:my_app, key),
             nil <- @config_defaults[key] do
          nil
        end
      end
    
    1. 4

      Oh that’s gross…I love it.

      If I’m reading this correctly, it works by assuming that the clauses will return nil, and when one of them successfully does, it treats it as a matching error and implicitly returns it.

      Normally you’d write something like:

      with :ok <- do_thing() do
        :success
      else
        {:err, :reason1} -> {:error, "reason 1 happened"}
        {:err, :reason2} -> {:error, "reason 2 happened"}
        _ -> {:error, "god only knows how we got here"}
      end
      
      1. 4

        It makes sense once you start thinking of with not in terms of success or failure, but in terms of match or no-match. If any of the with clauses return non-nil, that value is returned immediately. It is surprising at first, but it’s so much less ugly than nested cases!

        1. 3

          I like your explanation. I am uncomfortable with that view but I see the utility of it. :)

          1. 5

            “That’s gross… I love it” is roughly the sentiment I got in code review :D

    2. 3

      Couple of (to some heretical) thoughts re: with.

      1. Both case and with let you use guard clauses, which is handy.
      2. It can be very helpful to use a tagged tuple in a with chain to make error handling easier: with {:step1, :ok} <- {:step1, do_thing()} can then let you match on {:step1, error} -> handle_step1_error(error) in the else branch.
      3. You should probably always be using an else branch in your with statements to show you’ve considered all error conditions.
      4. The |> is usually better handled via with due to error handling.
      5. Lots of function heads instead of a single case statement can be faster but harder to maintain.
      1. 2
        1. You’re far from the first person to advocate this style but I’ve never liked it in practice – too many else clauses feels like a smell, and having to crap up your with clauses with all those tuples starts to look worse than breaking each with clause into its own case and just having one function call another upon success. Logically, I like keeping the contents of each with clause paired with its error handling code more than I like stirring it all together.
        2. No strong feelings here but if I don’t have a #3 in this list, Markdown calls the next line #3
        3. B-b-but that isn’t letting it crash!
        1. 3

          “letting it crash” is one of my triggers at this point. A lot of people seem to have really weird ideas about what that means. :-\

          1. 1

            Heh, I kid. I recently wrote a module that runs some periodic health checks inside one of our services (run queue depth is the most relevant one) and restarts the VM upon failure. I’m not just letting it crash, I’m making damn sure of it now!

            1. 2

              But you know that you can do that with external program (for example systemd watchdog) to make it even more “bulletproof”. I have implemented that partially in systemd library for Erlang (now I see that I should add healthcheck function as well, will do in version 0.6.0).

            2. 1

              What do most people think it means? And what do you think it means?