Elixir: When to use .ex and when .exs files

For some more concrete details, here are some patterns or rules of thumb I've noticed about when to use .ex vs .exs files:

Almost all code goes in an .ex file in my application's 'library' directory tree, e.g. under lib (or maybe web if the application is an old Phoenix webapp). The code can be called (if it defines public functions or macros) elsewhere in the application or from iex. This is the default.

Probably the most important consideration for deciding between .ex and .exs is where (and when) I want to call that code:

  1. In the application itself – .ex
  2. From iex (e.g. iex -S mix) – .ex, if the code is also (or probably will be) used by the application; .exs for 'ad-hoc' scripts, e.g. for a specific bug or other issue
  3. From the shell, via mix.ex for custom Mix tasks, or as data in mix.exs (for simple task aliases)
  4. From the shell, i.e. as a 'regular script' – .exs and run via elixir some-script.exs.

What may be surprising is that a good bit of code that starts as one of [2], [3], or [4], eventually ends up in [1] anyways.

And a really important thing to keep in mind is that it's EASY to move code from, e.g. an .exs file to an .ex file (or vice versa).

For [2], and for one application in particular, I setup some extra 'scripts mode' Mix environments for use with iex. Basically, they run a basic 'dev' local instance of (parts of) the app, but connect to the production/staging DB (or other backend services). I have some helper functions/macros for these 'command environments' but mostly I just use application code to, e.g. query the production/staging database and fix/cleanup/inspect some data. If I do need to do something that's even a little 'involved' (complicated), I'll usually create an ad-hoc 'issue script'. It's much easier to write code in my editor than in iex.

For ad-hoc scripts for [2], I have a dev/scripts sub-directory-tree in one project and mostly just name ad-hoc scripts based on the corresponding ticket number (from that project's ticket system), ex. 123.exs. Then, in iex, I can easily run that script:

iex> c "dev/scripts/123.exs"

One nice 'workflow' is to just define a single (top-level) module in the script file:

defmodule Ticket123 do

  ...

  def do_some_one_off_thing ...

end

Then, from iex:

iex> c "dev/scripts/123.exs"
[Ticket123]
iex> Ticket123.do_some_one_off_thing ...
Something something
...

For [3], both task aliases in your project's mix.exs and 'fully' custom Mix tasks are really nice.

Some example aliases from one project:

  • Run 'all' migrations – this project has regular (Ecto) database migrations AND some migrations that we want to run 'manually' (i.e. using the 'scripts mode' environments mentioned above). We use manual migrations for things like long-running database queries/commands, ex. adding or modifying an index on a big table.
  • Deploy to staging/production
  • test – this shadows the builtin Mix task to perform some custom local-database setup
  • todo – output search results for TODO comments in our (Elixir) code

Some example custom tasks from the same project:

  • Mix.Tasks.OneProject.BootstrapServer – some custom { configuration management / automated infrastructure deployment } code
  • Mix.Tasks.OneProject.DbSetup – pulls some configuration data from the Elixir app but mostly just passes that to a shell script
  • Mix.Tasks.OneProject.ImportTranslations – downloads translations and other internationalization data from a third-party
  • Mix.Tasks.OneProject.RollingDeploy – custom deployment that uses something like a 'blue-green' model, but with a load balancer instead of two entirely separate environments/instances

I haven't used entirely 'regular' Elixir script files much at all in any project – to be called via elixir some-script.exs. Probably the biggest reason not to, i.e. to just create a custom Mix task or an 'issue script', is that custom Mix tasks are nicely documented, i.e. via mix help and mix help some-custom-mix-task, and I really like working at a REPL (iex).


Elixir will compile the whole .ex file.

.exs files are compiled as well but are meant to be executed when invoked. So, most use cases of .exs files are to execute code immediately when called. Think of using .exs files for testing, migrating data and running scripts. Think of .ex files as being used for your application's main business logic.

Consider this example

.ex sample

sum.ex:

defmodule Sum do
  def add(a, b) do
    a + b
  end
end
$ iex sum.ex
...

Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Sum.add(1, 2)
3

.exs sample

sum.exs:

defmodule Sum do
  def add(a, b)  do
    a + b
  end
end

#within same file

IO.puts "The sum of 3 + 2 is: #{inspect Sum.add(3, 2)}"
$ elixir sum.exs
The sum of 3 + 2 is: 5

I check it in Elixir version 1.9.1 and both extensions .ex, .exs will be compiled with elixirc. Another words, we get bytecode (.beam file) in both cases.


.ex is for compiled code, .exs is for interpreted code.

ExUnit tests, for example, are in .exs files so that you don't have to recompile every time you make a change to your tests. If you're writing scripts or tests, use .exs files. Otherwise, just use .ex files and compile your code.

As far as pros/cons, interpretation will take longer to execute (as elixir has to parse, tokenize, etc.), but doesn't require compilation to run. That's pretty much it - if the flexibility of running scripts is more important than optimized execution time, use .exs. Most of the time, you'll use .ex.

Tags:

Elixir