Skip to content

Instantly share code, notes, and snippets.

@IceDragon200
Created February 7, 2024 19:27
Show Gist options
  • Save IceDragon200/b71cafd052ee03f65d1cadc6a19d49e6 to your computer and use it in GitHub Desktop.
Save IceDragon200/b71cafd052ee03f65d1cadc6a19d49e6 to your computer and use it in GitHub Desktop.

So here is my 2 cents in response to https://wiki.alopex.li/ElixirNitpicks:

Error Handling:

  • Every language I've known has the same statement: Don't use exceptions if you can.
  • That applies to Elixir as well, the preferred way to handle "expected" errors is by returning an error tuple: {:error, term}, bonus points if the term is a struct that provides more information on said error.
  • Exceptions should only happen when the code enters a state it straight up can't handle (think completely corrupt state, dead connections, unexpected failures, failed assertions, etc).
  • The above is where the "Let it crash" comes in, literally just that, let it crash and let your application pick up where it last (safely) left off.
  • It's easier to debug if it fails outright, sometimes the best error handling is none at all (just capture success and let it crash otherwise)

State Management:

  • Generally if you see a module that behaves like a global store that doesn't accept a name or pid, that's a code smell, and anyone from an OOP background knows that one as a singleton (exception is Application.*_env functions).
  • Yes erlang has a global registry, no, you don't have to use it that way if you don't want to, you can always capture the pid from start/start_link/spawn etc.., you can make your own registry, heck elixir has its own: https://hexdocs.pm/elixir/1.16.1/Registry.html
  • If the libraries you are using only have a singleton interface, that's their choice, even Ecto has its own registry like behaviour as you use your Repo module as the name (which is a valid option).

Imports:

  • No, no I don't want your option, let me have the different keywords since I understand the usage and meaning behind each:
    • import aliases all public functions and public macros from the target module into the current scope (by default), you can specify which functions you'd like to include or exclude by using only: [{name::atom, argv::integer}] or except: [{name::atom, argv::integer}]
    • require marks the module as a dependency of the current scope and allows using its macros (which are typically only available at compile time)
    • alias is used to rename a remote module into something local, usually for convenience, think aliasing erlang's :ets to ETS for neater code

The mixed messages:

  • Use an umbrella project if your requirement needs to break up its dependencies into smaller applications, I personally maintain a 128 application umbrella project for work, it's a monolith but it's also a microservice (using a little bit of release magic you can just dynamically load the components you need at runtime, which amounts to compile once, run anyhow), it makes testing the project as a whole easier since you have everything you need in one place.
  • Don't use live upgrades, just roll the application it's easier (at least if you deploy on k8s or something), dealing with migrating process state is a nightmare you don't want.
  • Yes, don't abuse macros they are not a silver bullet, same can be said for literally anything in programming: don't abuse eval; don't abuse Object#send; don't abuse global variables, and don't abuse processes as glorified objects either.
  • Never personally used mnesia, in most cases I needed, ets has sufficed, I know https://www.rabbitmq.com/ uses mnesia internally, and I've had no issues with that either running it for 5+ years now (it handles about a billion messages every day and its still under utilized)

Unit tests are more of a pain?

  • How? ExUnit is a breath of fresh air! Well it's not purely ExUnit, it's more of erlang and elixir as a whole that makes it a pleasant experience, pattern matching allows me to skip out map fields that I otherwise don't care about, while also validating the structure of something.
  • Have you tried binary pattern matching? Try it, you'll hate every other language once you're finished, you'll have sleepless nights thinking "how could I ever live without this?"
  • I find every other language's test frameworks to be PITA, it's either the validations are too cumbersome to write (js, f*** you in particular), or the assertions are too all or nothing (assert_equal matching EVERYTHING or nothing at all), good luck validating something that changes constantly and you really don't care about it but its in the object you're trying to validate so you have to match it or validate each field on its own (assertion chains!).

DSLs (Domain Specific Languages):

  • As someone who came from ruby, I can appreciate a good abstraction layer over some rather cumbersome code, Ecto is a good example, its query modules are literal magic to me, I don't care how it works, I just know it does and it's a pleasure because it just does.
  • Plug doesn't have that much DSL, and it's very easy to wrap your head around it: plug, match, then all your http methods for Plug.Router; outside of that, there isn't much else to worry about.
  • Absinthe shares a few keywords with Ecto, so if you know one you'll feel at home with the other, the exception is that you are working with graphql now, so you'll have to get used to its oddities (and boy do I hate dealing with GQL)

I agree on the complexity of LiveView, I don't like it either, but once I got my one and a half brain cells wrapped around it, I finally understood it and it was easy to get on with my life (and deciding I would use it sparingly if I have to).

Misc:

  • Hot code reloading has its own issues, when you have more complex module dependencies (say a central ecto schema) it can get slow, worse if you have something like absinthe in the mix (heh), it has to recompile the entire schema each time any dependencies of those modules change (you changed the aforementioned schema? Prepare to recompile everything!)
  • Reliability, yes, stuff crashes, all the time, it's normal, if your code is failing as ugly as you say, that sounds like a problem with your code and not a fault of elixir:
    • You have Result, kinda, it's called a tuple ({:ok, term} | {:error, term} is your Result).
    • you don't need the ? operator, you have powerful pattern matching so use it properly (not in an aggressive way, but most of the problems stem from not using it well enough).
  • Cultural split? I respect erlang programmers, I don't use erlang as my daily driver (because I started with Elixir), but I happily dip into the erlang ecosystem to borrow their work (hex.pm also has erlang up there you see), yes it's a bit of a pain to do the reverse, but you could just use elixir and stick to that, not sure why you'd ever want to write an erlang application and then use an elixir module in it?
    • Why not just use elixir to begin with? You can even compile your erlang code via elixir's mix with a few compiler settings in your mix file, of you really wanted to.
  • Honestly elixir's community has been quite nice from what I've seen, I've never really had to ask any questions myself, reading the code (which is a big plus in my book) generally gets me the answers I need, yes there are some edge cases where documentation is sparse (good luck trying to find anything on erlang's prim_file for example)
    • One thing no one tends to touch on is the ease of reading elixir's code compared to erlang, you can generally take one look at a random function and understand enough about what it does without having to dig around too much (have you tried reading the standard library for any other language, most are like assembly, highly optimized code that is impossible to read for a newbie, or archaic code that really needs to go but stayed around because no one has rewritten it, or can't rewrite it)

Positive notes:

  • I don't personally use Dialyzer, but I also don't write code that is that complex to require it; KISS, function specs generally inform me of what I need to pass in and what the function will do, if it "may" do something I like to prefix it with maybe_, write unit tests or property tests and let it live
  • Erlang/Elixir are dynamic languages yep, they look and feel like system's languages which is nice too, you have hot loading even in production, this is what powers the code reloading, since there are no "objects" per say, but POETs (Plain old erlang terms, I just made that up) you really just need to change the code and your data stays the same
  • Supervision trees, Supervisor.start_link(children, opts), that's all you need generally, for the most part your application will run just fine with the most simple tree, if you have a lot of different disparate components, see about breaking it up into an umbrella application, you will thank yourself later when you can unit test that specific component within the context of your entire application.

On topic of validation: Use Ecto.Schema (if you have it), just dump it into that and use the Changeset validations on it, so much easier than trying to handroll your own validations, even if it's just one field:

defmodule EmailSchema do 
  use Ecto.Schema

  import Ecto.Changeset

  @primary_key false

  embedded_schema do 
    # here is your type validation right off the bat
    field :email, :string
  end

  def validate(email) do
    %__MODULE__{}
    |> cast(params, [
      :email,
    ])
    |> validate_required([
      :email,
    ])
    |> validate_change(:email, fn field, value ->
      cond do
        not is_email_address?(value) ->
          [{"invalid email address", [validation: :email]}]

        not EmailAddresses.is_available?(value) ->
          [{"is unavailable", [validation: :email]}]

        true ->   
          []
      end
    end)
    |> apply_action(:insert)
  end
end

case EmailSchema.validate(email) do 
  {:ok, %{email: email}} ->

  {:error, %Ecto.Changeset{} = changeset} ->
    changeset.errors[:email] 
    # Can be all of these in the same list, or be any one depending on the validations
    #=> [{"is required", [validation: :required]}]
    #=> [{"invalid email address", [validation: :email]}]
    #=> [{"is unavailable", [validation: :email]}]
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment