Learning Elixir and Ecto
Tags: development, elixir, learning • Categories: Learning
I’m finally posting a long-running learning document that I wrote as I I continued work on my original Elixir side project. I stopped working on this for at least a year and recently picked it back up as part of exploring some technologies for my next startup.
This post got way longer than I expected, but hopefully, it’s a great compendium of notes and learnings from someone trying to learn Elixir who has a strong understanding of ruby, python, javascript, etc.
What I’m learning
Here’s what I’m going to be learning:
- How does Ecto work?
- Supervisor, tasks, processes, etc.
- "Let it Crash" philosophy. What exactly does this mean in practice?
- Macros/metaprogramming
-
Testing
Persistence in Elixir using Ecto
-
Ecto was split into
ecto
andecto_sql
a couple of versions back. All of the DB stuff (adapters, migrations, etc) is in theecto_sql
package. This is a recent-ish change: old articles assume that you are using the older packages and you won’t be able to find the migration docs in the ecto docs. -
I want to store lat, long, and a JSON payload. I don’t need to query against the payload, but I need to query against lat/long. What field types should I use?
- Jsonb is the best column type for storing JSON in Postgres. In your ecto schema file, you can use
field :payload, {:array, :map}
to define a jsonb which is an array of objects/maps. In your migration, you’ll want to useadd :payload, :map
- I decided to use PostGIS for my app, although it’s most likely overkill. In your schema, use
field :coordinates, Geo.PostGIS.Geometry
and%Geo.Point{coordinates: {lng, lat}, srid: 4326}
to generate the point to insert into your DB. Unfortunately, you need to manually run some SQL in order to setup the field in your migration:execute("SELECT AddGeometryColumn ('table_name','coordinates',4326,'POINT',2);")
- Jsonb is the best column type for storing JSON in Postgres. In your ecto schema file, you can use
-
Ecto has a different mental model than ActiveRecord. Associations and model schema is specified in the "schema" block of the module with
use Ecto.Schema
and specified in the database migrations (priv/repo/migrations/*
). Fields are not automatically sourced from the table data like ActiveRecord, you need to explicitly tell Ecto what fields exist on the model. -
There’s a bunch of generators available through
mix phx.gen.*
. If you want to generate a migration & schema, without the view layer:mix phx.gen.schema DataCache data_cache lat:float lng:float payload:map
-
Model |> first() |> Repo.one()
is similar to#first
in rails. -
Use
Repo.get(ExampleModule, 1)
forExampleModel.get(1)
in rails. -
Geo.PostGIS.Geometry
needs to be used instead ofGeo.Point
. A fix is outlined in this issue. Here’s another good post on postgres GIS stuff. -
Ecto.Query
is confusing as well. It looks like there is a ton of metaprogramming/macro magic that happens when youimport Ecto.Query
that lets you ‘naturally’ write queries without too much special syntax on top. It feels weird at first, and I’m still not sure how the macros work, but I think I like it. -
embedded_schema
andembeds_*
allow you to embed documents within a database insertion and validate a specific document schema. I could see this being super useful, surprised more ORMs don’t adopt this. Would be neat if this could tie into the postgres json schema validation extension. -
The order in a field definition’s tuple matters.
field :payload, {:array, :map}
is an "array of maps" vsfield :payload, {:map, :array}
is a "map of arrays" (this isn’t really possible). Helpful if you are backingpayload
with ajsonb
postgres column. - If you want a field to accept an array or a map, you need to define a custom type. This seems a bit crazy to me: pretty heavyweight for something that should be really simple.
-
Undo a migration and then rerun it:
mix ecto.rollback -n 1 && mix ecto.migrate
-
Streaming is really neat.
Ecto.Repo(fn -> Ecto.Repo.stream |> Task.async_stream |> Stream.run end, timeout: :infinity)
is the magic incantation you are looking for to run an operation across multiple processes. By default, the number of cores available on your machine is used to determine the number of concurrent processes to execute. Super easy to setup parallel processing that still feels functional. So beautiful. - Schemas are designed to be distinct from contexts, which expose an interface for the rest of your app to interface with the schema/data layer. The idea being that the schema module is only concerned with generating changesets and defining fields. A context is a view layer for your data layer.
-
You can run raw SQL via
Ecto.Adapters.SQL.query!
which returns an array of an array (looks like a CSV). One big limitation here is you only run a single statement. Here’s how to run multi-statement SQL in Elixir. -
The ecto struct/schema does not allow you to access fields via
[]
, although it’s just a map under the hood, which makes accessing field values frustrating sometimes.Map.fetch!
is a good alternative -
When
:numeric
is defined in the migration schema,:decimal
type must be used in the ecto schema. -
There is not a built in way to log SQL, with all params in place. The best you can get, without plugging into ecto (or db_connection) telemetry events, is the SQL with a list of arguments in the log line. This is because the Ecto library actually sends the parameterized SQL to Postgres (interesting that Postgres supports parameterized queries out of the box, what an amazing database)
- There is a library you can use to add full SQL logging to Ecto It worked pretty well.
- In
dev.exs
you’ll want to prevent logs from being truncated, it’ll make debugging much more challenging:config :logger, truncate: :infinity
- It’s not recommended to decorate your schemas with generated attributes. You should use contexts instead.
-
field :id, :integer
is auto-added to all schemas which is why you’ll get an error when trying to add anid
field explicitly to Elixir schemas.
Core Language Concepts
- The "let it crash" philosophy is confusing. I think the idea is more to build deterministic code paths and create high-level retry strategies so you don’t need to clutter your code with try/catch blocks, although I’ve yet to really see examples of this in practice.
- Changelog (the app) has a really great example of using a custom schema module. This enables you to add a bunch of methods that will be inherited by all schema modules.
~e
allows you to ‘cast’ a heredoc string as a eex template. These custom sigils for heredocs are a really neat feature of the language. Surprised other languages haven’t copied this.- You can pass a block directly to a method (ruby-style), but it’s executed immediately (doesn’t wait for a
yield
). Adddo: block
to your function argumentdef test(do: block)
.block
will contain the result of the block and can not beyield
ed repeatedly. - There is a built-in for checking the existence of an object in an array
action in [:edit, :update, :delete]
- You can copy code into a production
iex
to test code live in your production environment. Dangerous, but awesome. I’m a big fan of languages providing dynamic runtime access in production, it’s a huge lever for high dev velocity. - You can use matches in a conditional, but it’s not recommended.
if {:ok, nil} == {:ok, [] |> List.first}
- Specifying a default parameter uses
\\
i.e.javascriptFunction(opt = default)
=>elixirFunction(opt \\ default)
. If you are defining a method multiple times, define the default in the first definition of a function and then it will flow through automatically to the other function definitions. This is a bit hard to reason about. - You can define variables that are accessible to all functions within a module. These are called module attributes. However, by default, they are only used at compile-time and cannot be accessed dynamically (in a repl for instance). I’m guessing these are inlined by the compiler.
- Keyword tuples are special:
[value: 'default'] == [{value: 'default'}]
. You can omit the[]
when it’s the last option of a function call. Ordering matters on these for pattern matching, so don’t pattern match on keyword tuples unless you have to.- Relatedly, the order of keywords matters if you define them explicitly in the function signature.
- One of my favorite ERB patterns isn’t possible in elixir:
<%= some_content_function if condition %>
- All of the config is essentially a massive hash that can be inspected at runtime
Application.get_env(:logger, :console)
, or for all of the configurationIO.inspect(Application.get_all_env(:app))
- There are so many shared concepts from rails. It was very smart for the Elixir team to do this.
%w
,Kernel
, symbols/atoms, etc. erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell
gets the OTP version installed on the system.<>
concats strings instead of+
in most languages- Elixir takes lexical scopes really seriously. Rebinding a variable in an
if ... do ... end
block does NOT reassign the variable in the function scope. Instead, you need tothevar = if ..., do: ...
. This makes sense given how Elixir treats variables (pattern matching vs assignment) but is frustrating regardless. - I’m going to need a plug to copy the IP address of the client through the nginx proxy so my GeoIP stuff works. Let’s learn plug!
- Super simple middleware stack. You define a set of transformations that conform to a very simple protocol.
- Routing plug options are defined like this.
- I wanted to test the plug I was using with the latest plug. To update plug in a package:
mix deps.update plug
- Plugs are added via
endpoint.ex
. Specific "functions" of plugs are called by defining a pipeline in your router. - Here’s how to compose plugs
- All of the
~
tricks (~w(word)
,w(atom)a
, etc) are called Sigils Hash#dig
in ruby-land ==Map.get_in
- Phoenix routing can’t do optional URL parameters easily. Surprised that this isn’t supported.
- If you have an atom-keyed map you can use the dot syntax to access them
map.key
. Atoms aren’t garbage collected, don’t use them for external data. Use atoms kind of like constants. - You can add filters on array iterators (
for i <- list
) similar to what you might do with aarr.filter { |i| ... }.each { |i| ... }
in ruby:for i <- list, i > 2, do: i
- Map update shorthand:
%{expenses | groceries: 150, commute: 75}
- "Erlang allows only one assignment per variable name (SSA-like) the ‘Elixir variables’ will have non-meaningful names in Erlang code." ref
- Single-quote character-list strings are included for compatibility with Erlang. In most cases, you don’t need or want to use them. You can use
~s
if you want a binary list (double quote) string - Elixir has the ability to create strong boundaries between applications using Umbrella apps.
- There’s not a clean way to conditionally add logic to a pipeline. I get the logic here: you should pull that logic into a separate method, but there are cases (i.e. ecto queries) where it would be easier to read to do something like
|> where(field: table.field != ^var) if condition
. https://stackoverflow.com/questions/46961244/how-do-i-pipeline-an-if - Opus is an interesting take on developing ‘business logic stacks’ isolated in a module.
- You can call a function reference with
.()
, for example:Module.function/0.()
- You can construct lambda functions with multiple definitions/guards.
- If you are like me, you installed elixir & erlang with brew to start and then moved to
asdf
. You’ll want to make sure you uninstall erlang and elixir from brew and specify both the erlang and elixir versions in your.tool-versions
file. - Since
if
is more cumbersome compared to ruby,condition && Thing.to_execute()
is a great way to guard code with a conditional. - Hackney is an underlying erlang http library that is used by most HTTP abstractions
:hackney_pool.get_stats :default
- You can use
=~
to check if a string contains a substring. The right hand side doesn’t need to use a regex, but it can.html_response(conn, 200) =~ "Site Title"
"hi ho here we go" =~ ~r/ho.*/
- You can use the "vertical bar" operator to destructure a list.
[ first_element | remainder_elements ] = [1, 2, 3]
self
is not a reference to the module or object (remember, we are in functional world). It’s a reference to the current process.- Updating a map with a string key
%{some_json | "url" => "https://www.1/ofm/cic/"}
- Elixir 1.15 makes it more challenging to check if an optional package is loaded from the parent application.
prune_code_paths: false
fixes this, but you lose all of the benefits of the Elixir 1.15 compiler. You can runmix deps.compile --force
once which fixes the issue as well. - You have to add additional flags to your environment to get extended help support in ElixirLS. Here’s how I did this.
- If you modify source code in
deps/
it will not automatically recompile. It looks like changes to dependencies are not automatically picked up. - If you reference a module that does not exist, you will not get an error. Even if you use Dialyzer, you will not get a notification about an invalid module. You will get a notification/warning about an invalid method reference, so this is really only problematic when dealing with config (or other areas where you reference a module and it’s not called).
- When destructuring a named struct, you have to use the name/type of the struct
%Geocoder.Coords{lat: lat, lon: lon} = coord
- There is not a way to call private
defp
methods. You used to be able to useapply
, but this doesn’t work for private methods anymore. - There’s some nice utilities for comparing verions
Version.compare(System.version(), "1.15.0") in [:eq, :gt]
- It looks like stack traces get lost or mutated in some way. Similar to how async/await breaks stack traces in javascript / node.
- This is a core feature of the underlying BEAM VM. There’s not a way for Elixir to hack around this. It makes debugging much more challenging, especially in more complex systems, since you can’t identify where an issue originated from.
- You can do some really interesting data manipulation tricks:
metadata = [:boom] meta = %{boom: "hi"} for key <- metadata, value = meta[key], do: {key, value}, into: %{}
Hacking & Debugging
- Use
IEx.started?
to determine if we are in an IEx session. However, in productionIEx.started?
returns false in prod.exs + runtime.exs when started viaiex -S mix
- Use
Module.module_info
to inspect a module in the REPL (likemethods
in ruby). There’s alsoModule.__info__(:functions)
- Want to debug a mix task?
iex -S mix app.task --trace
- When opening up a pry session within a task or DB transaction it’ll timeout and cancel the session. This makes it much harder to debug things. One of the things I hate about Javascript and love about ruby + python.
- You can use
--trace
andtimeout: :infinite
to get around this, but it takes modifying the source in most cases, which makes debugging much more cumbersome.
- You can use
- It doesn’t look like it’s possible to break automatically on uncaught exceptions. I love this feature from Javascript/Ruby, it’s a shame that Elixir doesn’t support this.
- You can define default iex config in a local
.iex.exs
or~/. iex.exs
. Here’s mine - If you want to execute multi-line pipelines in a REPL use parens around the code
respawn
gets you out of apry
session- I couldn’t get the graphical debugger in VS code working. I was able to get the erlang debugger started via
:debugger.start()
, but wasn’t able to pick a function to break on. Tooling needs a lot of improvement here; IEx.pry() seems like the easiest way to debug. IO.inspect
returns the first input arg, which makes it really easy to throw this in your pipeline to output a var part-way through the pipeline.Ctrl-\
exits an iex shell likectrl-c
in most other repls- You can enable shell history for iex, but it’s not a per-project history, it’s global.
export ERL_AFLAGS="-kernel shell_history enabled"
- Some pry tips
IEx.Helpers
contains a bunch of great helpersbinding()
exposes local variables
- The best option you have is setting breakpoints. You can easily do this programmatically, which is neat.
- Exceptions aren’t just
{:error, ...}
tuples. You have to use atry
block to catch them. For some reason I figured exceptions got transformed into error tuples and you could use the standard Elixir control flow to manage them.
Metaprogramming
My favorite thing about ruby is its metaprogramming.
Is it dangerous? Absolutely. Is it powerful? Yes. Power is helpful when you are moving fast, even if it’s dangerous. Strong metaprogramming enables you to debug applications quickly and hack in changes in a flash.
Here’s how Elixir’s metaprogramming works:
- Much of the Elixir language is a macro.
if
,def
, etc are essentially just macros. The idea is, you can modify and extend the core language. When you think of it in this way, Elixir is a Lisp. - Metaprogramming happens at compile time, which means that it’s possible for some code to not be included in your application build. Ex: if logging is disabled, the logging statements could just not exist in your compiled binary. This is important if you are using Elixir releases for deployment: the ENV that is present when the application is compiled could change the code.
- All metaprogramming is done via
quote
,unquote
, anddefmacro
. use
triggers the__using__
macro on the module that’s passed in. Kind of likeincludes
in ruby.__MODULE__
is an atom of the module’s name.- There’s no monkeypatching. You can’t reach into arbitrary modules and classes and modify them at will.
- However, you can override/patch a module by copy/pasting the entire module definition, modifying it, and then including that file in your application’s source. It’s unclear to me exactly how the load order works, but this is a neat trick when you are in a pinch.
Applications, GenServer, and Processes
- You can think of processes in Elixir as ‘free’ threads. You don’t need to worry about creating too many of them. They are extremely efficient.
- The magic of the GenServer is you get the
await
for free. Since everything is immutable by default, this makes debugging individual errors much cleaner: you can copy the args passed in, extract into a test case, create a fix, and move on. This flow is a great argument for building functional-by-default in any programming language. - I was wondering why some packages used a process ‘manager’ like
poolboy
instead of the built-in GenServer.- GenServer gives you fault tolerance (will restart your processes when they fail), but does not allow you to define a worker pool.
- Worker pools are helpful if you are communicating with an external system (like postgres or redis) that has limits on the number of connections allowed concurrently. An interesting aspect of the Erlang architecture is there aren’t different (traditional unix) processes running (worker, web, scheduler, etc). You don’t need to ‘budget’ connections to external services between them—you can have a single worker pool that services all aspects of your application.
- If processes are nearly free, and you don’t need to manage an external resource, why would you use a GenServer? If you need to manage state. For instance, if you are writing to an in-memory cache using ETS
- Get all running applications in app
Application.started_applications()
- You can stop & start each individual application
Application.stop(:geocoder)
andApplication.start(:geocoder)
- It looks like
use Application
magically generates aSupervisor
module within the moduleuse
is declared. For instance:Supervisor.which_children(Geocoder.Supervisor)
- When
GenServer.call
is executedhandle_call
on the module (which must haveuse GenServer
) is called. - If your process contains some state that you want to clear before each test run you can do something like:
setup do :ok = Supervisor.terminate_child(Geocoder.Supervisor, Geocoder.Store) {:ok, _} = Supervisor.restart_child(Geocoder.Supervisor, Geocoder.Store) :ok end
- If you want to remove a process completely from the supervisor tree (so it doesn’t get automatically restarted)
IO.puts("Disabling quantum scheduler") Supervisor.terminate_child(App.Supervisor, App.Scheduler) Supervisor.delete_child(App.Supervisor, App.Scheduler)
- The above snippet is super helpful in a
.iex.exs
file in your project to disable specific processes (like any cron-like operations, which is what is disabled in this case) when you launch an interactive iex session.
Logging
- Looks like structured logging is built-in, but not enabled by default. You’ll need to modify all of your env config to get things working.
metadata: :all
is required on the logger config. You can inspect the config in your current environmentApplication.get_env(:logger, :console)
.- You also need something like
format: "$time $metadata[$level] $message\n"
. Themetadata
var is not included by default. - Modifying config on the fly via
Application.put_env(:logger, :level, :debug)
doesn’t work on the fly. You need to useLogger.configure(level: :debug)
orLogger.configure_backend(:console, metadata: :all)
(depending on what config you are trying to edit) - Exception reporters like sentry add a separate log backend that listen for error events with specific metadata (specifically
crash_reason
).
- Not everything converts to a string by default. For instance, a map won’t automatically be formatted as a string when passing it to
Logger.info
.- This seems to have changed in recent Elixir versions.
- You should use a lambda function as the input to your
Logger
call when it’s expensive to generate the method. However, with compile time elimination of specific log levels this isn’t as useful.- I personally don’t like the idea of compile time filtering of log messages. This makes it harder to interactively debug production.
- It’s not easy to convert
{kind, reason}
from acatch
statement to acrash_reason
which will throw an error to Sentry (or another error tracker). Here’s the details and a workaround.
Custom Elixir Structured Logger
I’m a huge fan of structured logging, this one of the big devprod tips I took away from my time at Stripe. By adding consistent key=value
pairs to your logs and avoiding string interpolation in your log message, you can easily search + filter logs and run analytics (I’m still trying to figure out the best local log analysis stack, send me your recommendations!).
Elixir almost comes with structured logging support out of the box, you just need a bit of customization
defmodule CustomLoggerFormatter do
# change this depending on what default context you don't care about
@exclude_keys [:domain, :pid, :file, :line, :node, :mfa, :application]
@pattern Logger.Formatter.compile("$time [$level] $message: $metadata\n")
def format(level, msg, timestamp, metadata) do
metadata =
metadata
|> Keyword.drop(@exclude_keys)
# convert all values to strings
|> Enum.map(fn
{k, v} when is_list(v) -> {k, inspect(v)}
pair -> pair
end)
# Use the default Logger.Formatter.format/4 for the rest.
Logger.Formatter.format(@pattern, level, msg, timestamp, metadata)
end
end
Then, in config.exs
:
config :logger, :console,
format: {CustomLoggerFormatter, :format},
metadata: :all
Package Management
Elixir is definitely much more modular than the ruby ecosystem. The stdlib is much smaller and you end up having to mix in a lot more little packages to handle common tasks (like URL parsing). Node is similar, mostly because so many packages are abandoned or nearly duplicates and it’s hard to determine which group of packages you should use. I’m finding I have the same frustration with the Elixir ecosystem, although there are fewer packages and it’s more obvious which packages are the canonical solution for various problems.
It’s nice how documentation for packages is hosted on a consistent site with a well-looking layout. This improves the developer experience quite a bit! I know Ruby has this (rdoc) but it’s bad so I never end up using it. Python does a similar thing with readthedocs.io, but it’s also poorly designed (no cmd+k jump, poor search, etc).
mix.lock
isn’t as descriptive asbundle.lock
(that identifies which gems require which version of a dependent gem).mix deps.tree
is a really nice alternative to digging through theGemfile.lock
Mix.install
is an interesting feature that allows you to write one-off scripts and define dependencies on the top of the file. However, you cannot use this within a directory that has a mix project.- Dependencies are not removed from your lockfile by default. Use
mix deps.clean --unlock --unused
- If I got into weird trouble, here’s how I would wipe all local build data
rm -rf _build .elixir_ls deps
- Rebar is the Erlang package manager. I don’t fully understand how/when it’s used, but you’ll see it referenced a good bit.
- Drift in the erlang version used to run Elixir code can cause weird differences between how packages operate on dev, prod, CI, etc; make sure to use consistent versions via asdf.
- Unlike bundler and npm, you can’t override a package for local development with a CLI command. You need to modify your
mix.exs
, here’s the format I used to more easily switch between a hex-hosted, github-hosted, and locally-sourced elixir package:
defp defps do
[
{:geocoder, "~> 2.0.0",
[
# path: "~/Projects/elixir/geocoder",
github: "iloveitaly/geocoder",
branch: "default-worker-config"
]}
]
end
Mix Tasks
- Mix tasks are like rake tasks. Create one via
lib/mix/tasks/the_task.ex
. - There seems to be a very consistent template for creating these, but there’s not a generator for it yet. Look at an example app and copy/paste a task.
- Once you put the task in that directory it magically appears in
mix help
. - Mix tasks don’t work with releases. Mix was designed only for development tasks and there is no production task runner.
- However, if you use a heroku-style deployment you can use mix tasks in production. I find it super helpful to have a friendly task runner which is one of the reasons I ended up not using releases for deployment.
Testing
mix test
ormix test test/the_file_test.exs:20
to run a specific line or file. The test runner is built into the library. Nice!- What about fixtures? You can use heredocs (
"""
) for embedding JSON in tests. - There’s a fixture library built for Ecto as well.
- Mocks were not originally recommended, but now there’s a library for mocks available.
- However, using mocks take quite a bit of work: you need to refactor your code to been nicely abstracted into to plug a mock into the right section of the stack. This is hugely problematic for my engineering style: using mocks to build unit tests for trick + error-prone code is really helpful. Not having this is a pretty large downside.
- Database resets seem to be built into the Ecto framework. Not sure how they work (maybe they just a transaction?)
- To [open a REPL](<https://adamdelong.com/iex-pry-test/
MIX_ENV=test mix ecto.reset
>) inside of a testiex -S mix test --trace
- I wish I could break on unhandled exceptions in tests. I love this functionality from rails.
- Phoenix comes with a couple of different test helpers out of the box that are throw in
support/
Linting & Editor Support
- Elixir editor support is surprisingly great. The main two tools you want are Credo + Elixir LS.
- Formatting is built into the language and Elixir LS exposes it to the editor really nicely.
exs
files are not processed in the same way asex
files. Basic things like missing variables won’t be exposed in the editor for exs.
Here are the two extensions you want .vscode/extensions.json
{
"recommendations": ["JakeBecker.elixir-ls", "pantajoe.vscode-elixir-credo"]
}
Here’s the settings.json
that worked for me.
{
"cSpell.enableFiletypes": [
"elixir"
],
"[elixir]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "JakeBecker.elixir-ls"
},
"emmet.includeLanguages": {
"html-eex": "html"
},
"elixirLS.fetchDeps": true,
"elixirLS.suggestSpecs": false,
}
Upgrading Elixir, Erlang, and Phoenix
- I originally wrote this post ~2 years ago on Phoenix 1.5 and Elixir 1.2. I wanted to upgrade the application and see how much work it would take to get everything working.
- The upgrade process wasn’t too bad and there were helpful error messages along the way.
- There were great guides walking through the pheonix upgrade process.
- The
applications
struct that was required in older versions can cause issues when upgrading. - This guide was great at moving to a bootstrap/scss-based compilation process with phoenix 1.7
- My biggest issue was I missed an invalid module reference in my config that had me pulling my hair out for a long time. Remember that invalid module references in config will not error at all. Hopefully, the new gradual typing system that Elixir is working on will resolve these sort of issues.
Secret Management
config/
structure was updated in 1.11, older blog + forum posts will reference the old architecture (which assumes system environment variables are used at compile time).prod.secrets.exs
can be consolidated into aconfig_env() == :prod
block insideconfig/runtime.exs
which is always executed in the runtime. This allows you to use system environment variables within this config since it’s not executed- The
config/
system is an Elixir thing, not a Phoenix thing. This is really nice: you can use this in any packages without Phoenix. Elixir does a great job managing configuration. - I like to use separate local databases for testing and development. Here’s the snippet in
config/runtime.exs
I used to implement this:
database_url =
System.get_env(if config_env() == :test, do: "TEST_DATABASE_URL", else: "DATABASE_URL") ||
raise """
environment variable DATABASE_URL (or TEST_DATABASE_URL) is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
config :app, App.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6
Here’s the docker-compose.yml
I used (with orb!) to host databases locally:
version: '3.8'
services:
postgres-gis:
image: postgis/postgis:15-3.3
container_name: app_postgres_gis_container
environment:
POSTGRES_DB: app_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5555:5432"
volumes:
- postgres-gis-data:/var/lib/postgresql/data
volumes:
postgres-gis-data:
Here’s the .envrc
(used with direnv) to automatically source the variables:
export PLUG_EDITOR=vscode://file/__FILE__:__LINE__
export DATABASE_URL=postgresql://postgres:postgres@localhost:5555/app_dev
export TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5555/app_test
dotenv_if_exists .env
(I’ll leave my secret configuration approach with .envrc
and a separate .env
to a separate post)
Open Questions
- Callbacks (what does @behaviour do?). They are some sort of metaprogramming magic, but I never fully understood them.
- Processes/GenServer/GenStage. Although I did work with packages that create their own processes, I didn’t work with Gen{Server,Stage} from scratch.
- Clusters/Nodes (connecting multiple erland VMs together to load balance)
- What are more advanced functional programming concepts? I’d like to dig into this in a deeper way.
- Is there a style guide for Elixir?
- What’s the best background job library?
- What’s up with
@spec
? - Learn more about supervisor trees.
- How do built in ETS tables work?
- How and when is code actually loaded?
:code.get_path
- How does property testing work?
- Is there a good capybara-like library for Elixr?
- https://github.com/Adzz/ecto_morph and https://github.com/supabase/realtime look like interesting tools.
- This is a really interesting grammar parsing library. I’d love to understand how to write my own language grammar + parser at some point. Seems interesting.
Resources for learning Elixir
General:
- https://docs.timber.io/setup/languages/elixir#installation
- https://elixirschool.com/en/lessons/basics/testing/
- http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/
- https://medium.com/@brucepomeroy/accepting-optional-options-in-elixir-65e7eaed11ac
- https://culttt.com/2016/05/09/functions-first-class-citizens-elixir/
- https://nts.strzibny.name/elixir-phoenix-after-two-year/
- https://blog.appsignal.com/2021/11/16/pitfalls-of-metaprogramming-in-elixir.html
- https://underjord.io/are-contexts-a-thing-in-phoenix-web-apps.html
- https://elixirforum.com/t/elixirconf-2021-videos-are-up/43289
- https://www.erlang.org/blog/type-based-optimizations-in-the-jit/
Specific Topics:
- Streaming results: https://medium.com/@dinojoaocosta/elixir-findings-asynchronous-task-streams-7f6336227ea
- Streaming: https://learn-elixir.dev/uses-of-elixir-task-module
- Testing: https://medium.com/onfido-tech/the-not-so-magic-tricks-of-testing-in-elixir-1-2-89bfcf252321
- https://www.youtube.com/watch?v=OR2Gc6_Le2U
- Tasks: https://learn-elixir.dev/uses-of-elixir-task-module
- Debugging: https://medium.com/@diamondgfx/debugging-phoenix-with-iex-pry-5417256e1d11
- Postgres jsonb column: https://www.compose.com/articles/faster-operations-with-the-jsonb-data-type-in-postgresql/
- When you need Redis: https://dashbit.co/blog/you-may-not-need-redis-with-elixir
- Guide & comments about OTP: https://news.ycombinator.com/item?id=24637121
- Debugging in Elixir
- https://elixirforum.com/t/running-debugging-tests-in-vscode-with-elixirls/33837. Good post to track for updates to the VS Code
- https://zorbash.com/post/debugging-elixir-applications/
- https://www.youtube.com/watch?v=w4xMarVUZQ4
- https://zorbash.com/post/debugging-elixir-applications/
- http://blog.plataformatec.com.br/2016/04/debugging-techniques-in-elixir-lang/
- Processes/Supervisors
Metaprogramming:
- Macros: https://nts.strzibny.name/elixir-macros-return-ast/
- https://elixirschool.com/en/lessons/advanced/metaprogramming/
- https://zohaib.me/use-in-elixir-explained/
- https://elixirforum.com/t/why-how-when-to-use-the-using-which-macro-in-phoenix-controller-view-etc/14001/3
Open Source Apps
Great to throw in a local folder for ripgrep
ing:
- https://github.com/bors-ng/bors-ng/
- https://github.com/pedromtavares/moba
- https://github.com/wojtekmach/oop Really interesting use of metaprogramming to implement OOP-like syntax in Elixir.
- https://github.com/akshaykmr/redex Redis protocol implemented in Elxir.
- https://github.com/discord/instruments Discord has a lot of great open source code
- https://github.com/akira/exq resque & sidekiq compatible job queue
- https://github.com/arjan/decorator interesting library which adds function decorators. Great example of what you can do with elixir metaprogramming.
- https://github.com/papercups-io/papercups