Elixir Programming Language

repository·main·Indexed 12 days ago

https://github.com/elixir-lang/elixir

A programming language designed for building scalable and maintainable applications, running on the Erlang VM (BEAM). Includes documentation for the elixir CLI, the elixirc compiler, the IEx interactive shell, and the Mix build tool.

Tokens
79.3K
Snippets
334
Records
399
Agent score
95%

What's inside Elixir

  1. Avoid namespace trespassing in library modules

    main

    When authoring a package or library, avoid defining modules inside the namespace of another library. Because the Erlang VM can only load one instance of a module at a time, defining a module like Plug.Auth for a package named :plug_auth can cause fatal conflicts if the Plug library later introduces its own Plug.Auth module.

    Best Practice: Always use your library's name as a prefix for all modules. For example, a package named :plug_auth should define modules as PlugAuth.User or PlugAuth.SubModule instead of Plug.User.

    Exceptions:

    • Protocol implementations: These are intentionally defined under the protocol namespace (e.g., using Kernel.defimpl/2).
    • Mix tasks: Custom tasks are defined under the Mix.Tasks namespace (e.g., Mix.Tasks.MyTask).
    • Maintainer ownership: If you maintain both the parent namespace and the extension, you may define modules within that namespace, but you assume responsibility for managing future conflicts.
    # Bad: Trespassing into Plug namespace
    defmodule Plug.Auth do
      # ...
    end
    
    # Good: Using the library's own namespace
    defmodule PlugAuth do
      # ...
    end
  2. Prevent large code generation in macros

    main

    Macros that generate significant amounts of code (e.g., inside a loop or many repeated calls) can slow down compilation and increase artifact size.

    To optimize, avoid putting heavy logic or validation directly inside the quote block of the macro. Instead, wrap the logic in a standard function and have the macro expand to a call to that function. This reduces the amount of code the compiler must expand and compile for every macro invocation.

    defmodule Routes do
      defmacro get(route, handler) do
        quote do
          Routes.__define__(__MODULE__, unquote(route), unquote(handler))
        end
      end
    
      def __define__(module, route, handler) do
        if not is_binary(route), do: raise ArgumentError, "route must be a binary"
        if not is_atom(handler), do: raise ArgumentError, "handler must be a module"
        Module.put_attribute(module, :store_route_for_compilation, {route, handler})
      end
    end
  3. Choose between handle_call, handle_cast, and handle_info

    main

    When implementing a GenServer, select the appropriate callback based on the nature of the request:

    CallbackUse CaseCharacteristics
    handle_call/3Synchronous requestsThe default choice. Provides back-pressure because the caller waits for a reply.
    handle_cast/2Asynchronous requestsUse when you do not need a reply. Note that a cast does not guarantee the server has received the message.
    handle_info/2Other messagesUsed for all messages not sent via GenServer.call/2 or GenServer.cast/2, such as regular messages sent via send/2 or system messages like :DOWN (monitors).

    For a quick reference, you can use the GenServer cheat sheet.

  4. Avoid structs with 32 or more fields

    main

    Structs in Elixir are implemented as compile-time maps. Maps with up to 32 keys are represented internally as 'flat maps' (two tuples: one for keys, one for values). This representation is highly optimized because:

    1. Updating a flat map allows the key tuple to be shared, reducing memory usage.
    2. Multiple instances of the same struct in a module can share the same key tuple at compile-time.

    Once a struct reaches 32 or more fields, the Erlang VM switches to a 'hash map' representation, which is more complex and does not benefit from these key-sharing optimizations. To keep structs under the 32-field limit, consider:

    • Nesting optional fields into a single :metadata or :options field.
    • Nesting related structs within other fields.
    • Grouping frequently co-accessed fields into a tuple.
  5. Avoid complex extractions in multi-clause functions

    main

    When using multi-clause functions, extracting many variables in the function signature for both pattern matching/guards AND for use in the function body can become confusing. It becomes hard to distinguish which variables are required for the clause logic and which are just being passed through.

    Refactoring Strategy: Extract only the variables required for pattern matching or guards in the signature. Use the assignment operator (=) to extract the rest of the data (like the whole struct) and then perform specific extractions inside the function body.

    def drive(%User{age: age} = user) when age >= 18 do
      %User{name: name} = user
      "#{name} can drive"
    end
    
    def drive(%User{age: age} = user) when age < 18 do
      %User{name: name} = user
      "#{name} cannot drive"
    end
  6. How type inference works in Elixir

    main

    Elixir uses best-effort type inference to deduce types at compile time without requiring manual annotations.

    Inference Scope:

    • Elixir aims to infer types across dependencies (Standard Library and your project's dependencies).
    • Calls to modules within the same project are assumed to be dynamic().
    • Once types are inferred, the entire project is type-checked.

    Trade-offs of Inference:

    • Speed: Inference is more computationally intensive than simple type checking.
    • Expressiveness: Inferred types are a subset of what can be explicitly type-checked.
    • Incremental Compilation: Changes in a dependency can trigger a chain of re-computations.
    • Cascading Errors: Conflicting assumptions can lead to complex error messages.

    Note: Type inference is not a guarantee of correctness; it is designed to find bugs where all possible type combinations would fail.

  7. Compare Agents and GenServers

    main

    When deciding between an Agent and a GenServer for state management:

    • Agents: A simplified subset of GenServers. They are useful for storing small amounts of state in a straightforward manner.
    • GenServers: The fundamental building block for concurrent, fault-tolerant systems in Elixir. They provide a robust framework for managing state and coordinating complex interactions between processes.

    Rule of thumb: Many developers skip Agents entirely and use GenServers directly to avoid the limitations of the Agent abstraction, though Agents remain perfectly valid for simple state storage.

  8. Distinguish between compile-time and runtime configuration

    main

    Elixir provides two primary entry points for configuration, depending on when the values are needed:

    1. config/config.exs (Compile-time): Read during the build process, before dependencies are loaded or modules are compiled. Use this to control how dependencies are compiled. Access these values using Application.compile_env/2.
    2. config/runtime.exs (Runtime): Read after the application and dependencies are compiled. Use this for values that change based on the deployment environment, such as reading system environment variables via System.get_env/1. Access these values using Application.fetch_env!/2.
  9. Use trailing bang (`!`) for functions that raise exceptions

    main

    A trailing bang (!) signifies a function that raises an exception on failure. These are often the 'raising variants' of functions that return :ok/:error tuples or nil.

    • Use the non-bang version (e.g., File.read/1) when you want to handle outcomes using pattern matching (e.g., case statements).
    • Use the bang version (e.g., File.read!/1) when you expect the operation to always succeed and want a more helpful error message than a failed pattern match if it fails.

    Note: Functions will always raise an exception (like ArgumentError or FunctionClauseError) if passed invalid argument types, regardless of whether they have a bang suffix.

    # Non-bang: returns {:ok, value} or {:error, reason}
    case File.read("file.txt") do
      {:ok, body} -> # do something
      {:error, reason} -> # handle error
    end
    
    # Bang: returns value or raises exception
    File.read!("file.txt")
  10. Use function heads for multi-clause dispatch

    main

    In Elixir, you can define a function with multiple clauses to handle different input patterns elegantly. A common pattern is to define a 'function head' (a clause without a body) to document the expected arguments or define default arguments, followed by specific implementation clauses that match specific data shapes.

    This is an effective way to implement command dispatchers or pattern-matching logic without using if/else or case blocks.

    # Function head to document arguments
    def run(command, socket)
    
    # Specific implementation clauses
    def run({:create, bucket}, socket) do
      KV.create_bucket(bucket)
      :gen_tcp.send(socket, "OK\r\n")
      :ok
    end
    
    def run({:get, bucket, key}, socket) do
      # ... implementation
    end