On December 3rd, the Elixir core team announced the latest Elixir release, 1.13. In this post, we'll explore some of the more exciting changes and new features, and discuss their impact on the Elixir community.
All About Tooling
Elixir 1.13 comes packed with lots of goodies and improvements designed to make the day-to-day life of Elixir developers easier. With new additions like semantic code recompilation, better support for auto-complete in IEx, an API for custom code formatting, and more, quality of life for Elixir developers is going to greatly increase. Let's dive into some of the latest developments now.
Making IEx Better
Elixir 1.13 comes with a number of changes that enhance the IEx experience. The introduction of the new Code.Fragment
module greatly improves the autocomplete story for Elixir, making your work in IEx even easier. Code.Fragment
provides conveniences for analyzing fragments of textual code and returning information about those fragments. Practically speaking, this means that IEx now supports autocomplete of sigils, structs, and paths. Let's take a look at some examples.
Previously, attempting to autocomplete the ~
character didn't lead to anything too useful:
iex(1)> ~ !/1 !=/2 !==/2 %/2 %{}/1 &&/2 &/1 */2 ++/2 +/1 +/2 --/2 -/1 -/2 ..///3 ../2 ./2 //2 ::/2 </2 <<>>/1 <=/2 <>/2 =/2 ==/2 ===/2 =~/2 >/2 >=/2 @/1 ^/1 __CALLER__/0 __DIR__/0 __ENV__/0 __MODULE__/0 __STACKTRACE__/0 __aliases__/1 __block__/1 abs/1 alias!/1 alias/2 and/2 apply/2 apply/3 b/1 binary_part/3 binding/0 binding/1 bit_size/1 break!/1 break!/2 break!/3 break!/4 breaks/0 byte_size/1 c/1 c/2 case/2 cd/1 ceil/1 clear/0 cond/1 continue/0 def/1 def/2 defdelegate/2 defexception/1 defguard/1 defguardp/1 defimpl/2 defimpl/3 defmacro/1 defmacro/2 defmacrop/1 defmacrop/2 defmodule/2 defo # ... and the list goes on
Now, if you attempt the same autocomplete, you'll see this:
iex> ~ ~C (sigil_C) ~D (sigil_D) ~N (sigil_N) ~R (sigil_R) ~S (sigil_S) ~T (sigil_T) ~U (sigil_U) ~W (sigil_W) ~c (sigil_c) ~r (sigil_r) ~s (sigil_s) ~w (sigil_w)
A helpful list of sigil functions.
Similarly, in earlier Elixir versions, trying to autocomplete a struct would yield the following:
- First, assume you have a struct,
User
, defined like this:
defmodule User do defstruct [:name, :age] end
- Attempt IEx autocomplete by hitting
%Use<TAB>
:
iex> %Use %User.__struct__ __struct__/0 __struct__/1
Not exactly a helpful look at the struct. Let's see what the same operation yields in Elixir 1.13:
iex> %User{ name: age:
Here, we get a helpful look at all of the fields of our struct.
These autocomplete improvements don't just make IEx better. Your autocomplete experience in your text editor of choice will also improve. With additional changes like new compilation tracers and new Module
functions for retrieving module metadata, we can expect to see new code intelligence features come out that will improve your text editor experiences with Elixir even further.
Faster Elixir Code Recompilation
With Elixir's new semantic code recompilation, your code will be recompiled less frequently. This means a faster development life cycle, especially when it comes to working with large Elixir applications.
Prior to Elixir 1.13, your entire Elixir app would be recompiled whenever any of the mix.exs
, config/config.exs
, src/*
, or mix.lock
files changed. Now, thanks to semantic code compilation, your code will recompile only when you change the compilation options in the mix.exs
file or the configuration in the config/config.exs
file.
Code compilation has gotten smarter in other ways too. Not only will your application be recompiled less frequently, but certain changes will now trigger recompilation of smaller parts of your app, or no recompilation at all.
For example, now if the size and digest of a file stay the same, the file will not be recompiled. This means switching or rebasing git branches will trigger far fewer recompilations. Additionally, changing compile-time config files (like config/config.exs
) will only recompile any files that depend on the reconfigured files. Practically speaking, this means that, for example, bumping your Phoenix version in mix.exs
will only recompile lib/my_app_web
and not lib/my_app
.
Semantic compilation is accompanied by some improvements to the mix xref
task. This task gives you information about your files and how they depend on each other. Here's an abbreviated look at running mix xref
with the graph
option in a standard Phoenix app:
// ♥ mix xref graph lib/pento.ex lib/pento/accounts.ex ├── lib/pento/accounts/user.ex (export) ├── lib/pento/accounts/user_notifier.ex ├── lib/pento/accounts/user_token.ex (export) └── lib/pento/repo.ex
This tells us that all of the files listed under lib/pento/accounts/ex
depend on account.ex
at compilation time. The task already gives us some great tooling to identify that a compile-time dependency exists, but doesn't really provide any insight into why such a dependency exists. So, if you needed to track down the source of an inappropriate compile-time dependency when that source is non-obvious (if it comes from a macro, for example), this tool used to fall short. Until now!
Elixir 1.13 adds a new option to the mix xref
task - the trace
option. Now, you can run mix xref trace <file name>
and see a list of exactly where all of our compile time dependencies, exports, and runtime dependencies come from within that file, down to the exact line number. Let's check it out:
lib/pento/accounts.ex:26: call Pento.Repo.get_by/2 (runtime) lib/pento/accounts.ex:43: call Pento.Repo.get_by/2 (runtime) lib/pento/accounts.ex:44: call Pento.Accounts.User.valid_password?/2 (runtime) lib/pento/accounts.ex:61: call Pento.Repo.get!/2 (runtime) lib/pento/accounts.ex:78: struct Pento.Accounts.User (export) lib/pento/accounts.ex:79: call Pento.Accounts.User.registration_changeset/2 (runtime) lib/pento/accounts.ex:80: call Pento.Repo.insert/1 (runtime) lib/pento/accounts.ex:92: struct Pento.Accounts.User (export) lib/pento/accounts.ex:93: call Pento.Accounts.User.registration_changeset/3 (runtime) lib/pento/accounts.ex:108: call Pento.Accounts.User.email_changeset/2 (runtime) lib/pento/accounts.ex:126: call Pento.Accounts.User.email_changeset/2 (runtime) lib/pento/accounts.ex:127: call Pento.Accounts.User.validate_current_password/2 (runtime) lib/pento/accounts.ex:140: call Pento.Accounts.UserToken.verify_change_email_token_query/2 (runtime) lib/pento/accounts.ex:141: struct Pento.Accounts.UserToken (export) lib/pento/accounts.ex:141: call Pento.Repo.one/1 (runtime) # ... I'm truncating this output for brevity, but there's more!
This gives us a powerful tool for understanding the interdependencies in our code and eliminating them where necessary.
Better Error Messages in Elixir
Elixir 1.13 also brings us improved error messages for both the SyntaxError
and TokenMissing
errors. Such error messages now include code snippets that show you the exact source of your error. This is just one more feature to improve your development experience and make hunting down bugs even easier. Let's try it out.
Assuming I've added the following bad code to my project:
def add_nums(num1, num2) do num1 + *num2 end
I should see the following error when I try to do something like run iex -S mix
:
== Compilation error in file lib/pento/accounts.ex == ** (SyntaxError) lib/pento/accounts.ex:26:9: syntax error before: '*' | 26 | num1 + *num2 | ^ (elixir 1.13.0) lib/kernel/parallel_compiler.ex:346: anonymous fn/5 in Kernel.ParallelCompiler.spawn_workers/7
Extensible Formatting
The latest Elixir release extends the capabilities of the Elixir formatter so you can implement custom formatting for any embedded code.
Consider the following scenario. You have a LiveView app that renders lots of HEEx templates:
# lib/my_app_live/user_live.ex def render(assigns) ~H""" <h2> User Profile </h2> """ end
You can see that the code in our HEEx template is formatted improperly, but since its not actually invalid, HEEx won't complain about it. When we run mix format
, the code embed within the sigil_H
won't be formatted.
Now, though, Elixir provides an API through which we can implement and apply custom formatters for embedded code. Here's how it works.
You'll define a custom formatter plugin module along these lines:
defmodule MixHEExFormatter do @behaviour Mix.Tasks.Format def features(_opts) do [sigils: [:H], extensions: [".ex",".heex"]] end def format(contents, opts) do # logic that formats HEEx end end
Then, you'll add your plugin to your app in .formatter.exs
, like this:
[ plugins: [MixHEExFormatter], # Remember to update the inputs list to include the new extensions inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}", "lib/my_app_live/live/*.{ex,.heex}"] ]
With this new functionality, we can expect to see some upcoming development of custom formatters for common types of embedded code. The Zigler project already has an issue open for a custom ~Z
formatter, and I hope to see some development for a ~H
soon.
AST-Based Tooling
Another exciting new piece of functionality comes by way of two new functions added to the Code
module. Code.quoted_to_algebra/2
and `Code.string_to_quoted_with_comments/2 will enable you to fetch the Elixir AST with original comments and convert it into formatted code.
This capability will empower developers to create tools for custom formatting of Elixir source code.
Better Testing in Elixir
Elixir 1.13 ships with a few new ExUnit niceties for debugging tests. ExUnit will now show a hint when comparing two different but equivalent strings, making debugging minor discrepancies between strings in your ExUnit expectations much less of a headache.
ExUnit also provides two new functions:
ExUnit.CaptureIO.with_io/3
allows you to apply a function to captured IO outputExUnit.CaptureLog.with_log/2
invokes the given function and returns the result and a captured log.
Here's the new example from the docs:
{result, log} = with_log(fn -> Logger.error("log msg") 2 + 2 end) assert result == 4 assert log =~ "log msg"
With this, we can easily test the result of function invocations and validate assertions against any log statements made during those function invocations.
Lastly, mix supports a new option, --profile-require=time
- so that you can run mix test --profile-require=time
to debug test suite load times.
New Elixir Language Features
In addition to this impressive list of new tooling capabilities, Elixir 1.13 also ships with some exciting new pieces of Elixir functionality. We'll take a look at just of few of these changes here.
The Enum.slide/3
function takes in arguments of an enumerable, index or range of indices, and the insertion index. It "slides" the index or range of indices to the target insertion index, like this:
# Slide a single element Enum.slide([:a, :b, :c, :d, :e, :f, :g], 5, 1) # => [:a, :f, :b, :c, :d, :e, :g]
Here, we "slid" the element living at index 5, :f
, and inserted it instead at index 1.
The List.keyfind/4
function receives a list of tuples and returns the first tuple where the element at the position in the tuple matches the given key. For example:
List.keyfind([a: 1, b: 2], :a, 0) # => {:a, 1}
The Keyword.validate/2
function lets you validate a keyword list to ensure that it contains only the provided set of values.
You can now also filter a keyword list with the Keyword.filter/2
function and map over a keyword list with the Keyword.map/2
function.
Wrap Up
This was a quick overview of the highlights from Elixir 1.13, but there's lots more to see, and you can check out the full changelog. This latest Elixir release means that quality of life for Elixir developers is getting even better.
New autocomplete and associated features make writing Elixir code faster, semantic code recompilation means faster development cycles, and new testing conveniences and Elixir language capabilities give us even more tools to get the job done.
We're excited to see what the community builds on top of all of this functionality.
Happy coding!
P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!