神刀安全网

What's coming in Elixir 1.3

I recently gave a talk about Elixir 1.3 in Tokyo, and spoke about the changes, new features, improvements and all the awesome stuff coming in Elixir 1.3. I decided to write this as a blog post, with a little more details, and some links for those who want to check in more details.

Here is a short table of contents:

The list is not complete and the order is quite biased, but I think the main changes are here. Of course, you can always check the full changelog .

Deprecation of imperative assignment

I would argue that this is one of the most important changes in 1.3, and will make a lot of projects emit warnings, which is why I brought this up first.

As a lot of you probably already know, from 1.2, conditional variable declaration has been deprecated.

This means that instead of writing something like this

if condition do   n = 1 else   n = 2 end IO.puts(n) 

which will make the compiler explain us that

the variable “n” is unsafe

we should write

n = if condition do       1     else       2     end IO.puts(n) 

I think that this warning is not too surprising, and has a solution that works in every case. For people coming from a Ruby background, Rubocop gave us the same warning by default.

However, in 1.2, we could write the following,

def imperative_duplicate(str, n, opts // []) do   if opts[:double] do     n = n * 2   end   String.duplicate(str, n) end 

which is actually quite common.

This will be deprecated from 1.3. This is a little more constraining, and does not have a solution that work for every cases, as the previous warning did. For the above code, we could for example, add a helper as follow:

def imperative_duplicate(str, n, opts // []) do   n = double_if_true(n, opts[:double])   String.duplicate(str, n) end  defp double_if_true(n, true), do: n * 2 defp double_if_true(n, false), do: n 

or simply rewrite like this:

def imperative_duplicate(str, n, opts // []) do   n = if opts[:double], do: n * 2, else: n   String.duplicate(str, n) end 

Both are a little more verbose, so I would like to discuss why this change has been introduced, and why I think it is a good thing.

Currently, we have basically two scopes rule depending on the special form we use: with , for and try get their own scope, and do not leak, while case , cond and receive do not get their own scope.

So when comparing

x = 1 with :ok <- :ok do   x = 2 end IO.puts(x) 

and

x = 1 case :ok do   :ok -> x = 2 end IO.puts(x) 

someone new to Elixir would probably expect both code to output the same thing, but the first case outputs 1 while the second outputs 2 . Although the scoping rules are well documented, I think that having all special forms getting their own scope will make thinks simpler, and this is where we are heading with the deprecation of imperative assignment. Therefore, hopefully in Elixir 2.0

x = 1 case :ok do   :ok -> x = 2 end IO.puts(x) 

will output 1 and not 2 .

Other than simplicity, another advantage mentioned by Jose in the mailing list is making things easier to refactor. Here is the example he gave.

For example, imagine this code:

path =   if (file = opts[:file]) && (line = opts[:line]) do     relative = Path.relative_to_cwd(file)     message  = Exception.format_file_line(relative, line) <> " " <> message     relative   end # ... {path, message} 

Now imagine we want to move the “if block” to its own function to clean up the implementation:

path = with_file_and_line(message, opts) {path, message} defp with_file_and_line(message, opts) do   if (file = opts[:file]) && (line = opts[:line]) do     relative = Path.relative_to_cwd(file)     message  = Exception.format_file_line(relative, line) <> " " <> message     relative   end end 

The refactored version is broken because the “if block” was actually returning two values, the relative path AND the new message. The new message was returned implicitly via the scope which now is broken. In this particular case we get a warning but we may not always.

To sum it up, we will get simpler and more consistent scoping rules at the cost of a little verbosity in some cases. This is in my opinion a very reasonable tradeoff.

For more details, you can take a look at this thread in the mailing list .

with on steroids

In Elixir 1.2, the new special form with was introduced, mainly to make working with nested case easier. The relevant thread from the ML can be found here .

There was however two drawbacks that have been adressed in 1.3.

The first one was when doing some processing on the error case. Here is more or less the example that have been given in the issue tracker for this proposal :

case create_organization(params) do   {:ok, organization} ->     case create_address(organization, params) do       {:ok, address} ->         %{organization | address: address}       {:error, changeset} -> {:error, changeset |> error_string}     end   {:error, changeset} -> {:error, changeset |> error_string} end 

It is easy enough to rewrite the success case with with , but the error case is a bit more tedious as we need to do some processing on the error value. As of 1.2, this would probably be rewritten in the following way

with {:ok, organization} <- create_organization(params),      {:ok, address}      <- create_address(organization, params) do   {:ok, %{organization | address: address}} end |> case do      {:ok, organization} -> {:ok, organization}      {:error, changeset} -> {:error, changeset |> error_string}    end 

which is a little cleaner but still does not feel very pretty. This is where with / else comes in. The else part allows to match on error cases, so the above example can be rewritten as follows.

with {:ok, organization} <- create_organization(params),      {:ok, address}      <- create_address(organization, params) do   {:ok, %{organization | address: address}} else   {:error, changeset} -> {:error, changeset |> error_string} end 

This looks much better, and as we can pattern match on error cases, it is also very easy to change the behavior depending of which clause has fail. If none of the clause in the else part matches, a WithClauseError will be raised, which is very close to what we already have for try / else .

Another drawback when using with was the impossibility of using guards. There was therefore no easy way to rewrite the following code using with .

case HTTPoison.get(url) do   {:ok, %Response{status_code: code, body: body}} when code in 200..299 ->     case Poison.decode(body) do       {:ok, data} -> data       _           -> :error     end   _ -> :error end 

With Elixir 1.3, we will be able to use guards in with , so the above code can be rewritten as follow.

with {:ok, %Response{status_code: code, body: body}}         when code in 200..299 <- HTTPoison.get(url),      {:ok, data} <- Poison.decode(body) do   data else   _ -> :error end 

You can check the ticket in the issue tracker for more info.

Calendar datatypes

Another important change in Elixir 1.3 is the addition of date related datatypes in the standard libraries. Until Elixir 1.2, we had two very good libraries for working with dates and times:

Thanks a lot to their authors for the great work. The biggest drawback, however, of not having these types in the standard library is the interoperability between libraries. If a library A uses timex while another library B uses calendar, it takes some work to make everything work smoothly together.

From Elixir 1.3, types to work will time and dates will be introduced in the standard library, and the libraries will therefore be using these types, which will make them interoperable for most common cases.

The types which will be introduced are:

  • Calendar
  • Date
  • Time
  • NaiveDateTime
  • DateTime

Calendar will contain typespecs for datetime related types, NaiveDateTime is a datetime without a timezone, and the other three types are what we could expect from the name. An ISO8601 calendar implementation, with probably parse and format should also be added to the standard library. However, timezone support and other calendar implementations will not be added to the standard library so we will continue to rely on libraries for these.

You can check the relevant PR for more information.

escript installation related tasks

As most of you probably already know escript are a very convenient way to distribute executables that can be run with only Erlang installed on the machine.

A CLI tool can be compiled by simply adding

escript: [main_module: MyApp.CLI] 

to mix.exs project, as long as MyApp.CLI exports a main/1 . It is then only a matter of running mix escript.build to get a packaged executable.

This is perfect to distribute with attachments in GitHub releases for example.

With Elixir 1.3, we now have a way to install escript s from an URL, which makes installing such binaries really trivial.

For example, to distribute an Elixir CLI tool, adding the following install instructions in the README will now be sufficient.

mix escript.install https://example.com/my-escript 

There are also the mix escript to list the currently installed escript s, and mix escript.uninstall to uninstall an escript .

Although this may not seem such an important change, I think that having a unified way of installing and managing binaries is very helpful.

You can find more information in the relevant PR .

ExUnit diff

Until 1.2, when a test case failed, we had the expected output and the actual output, but no easy way to compare them. Elixir 1.3 introduces a very nice and easy to understand diff, which will probably save us a lot of time especially when working with large data structures or strings.

An image is worth a thousand words, so here we go:

Until Elixir 1.2

What&#x27;s coming in Elixir 1.3

From Elixir 1.3

What&#x27;s coming in Elixir 1.3

As you can see, a clean diff line has been added with a nice formatted diff.

For this addition, String.myers_difference/2 has been added to the String module.

iex(1)> String.myers_difference("foobar", "fopbar") [eq: "fo", del: "o", ins: "p", eq: "bar"] 

This improvement is divided in multiple PRs, but the largest part, including the diff algorithm implementation has been merged in this PR .

make compiler addition

Erlang, and by extension Elixir support Native Implemented Functions (NIFs), which is great for CPU intensive task, especially when immutability can make things slow.

A very good example usage of NIFs is the comeonin package, which provides the bcrypt and blowfish algorithms.

However, before Elixir 1.3, it was a little tedious to distribute a library containing NIFs, and for example comeonin had to write its own compiler to compile the NIFs.

As running make is a pretty common use case, Elixir 1.3 now integrates a make compiler, which can be used by simply adding :make under the :compilers key of the mix project. It should also allow to customize the targets to run and the Makefile to use, but the feature has not been merged yet as I am writing this.

Changes in defdelegate

There has been a few changes in defdelegate . Let’s start by the “bad” news. Using defdelegate with lists, and the :append_first option have been deprecated.

The following calls will therefore both receive a deprecation warning.

defdelegate [reverse(list), max(list)], to: :lists # warning: passing a list to Kernel.defdelegate/2 is deprecated, please define each delegate separately  defdelegate map(list, callback), to: :lists, append_first: true # warning: Kernel.defdelegate/2 :append_first option is deprecated 

The reason for deprecating usage with lists is

You want each defdelegate to be on its own line so you can control module attributes like @doc

and the reason behind the deprecation of :append_first is that Elixir 1.3 will support optional parameters in defdelegate .

Until Elixir 1.2, given the following module

defmodule Hound.Helpers.Session do   def start_session(opts // []) do     # ...   end end 

if we wanted to use a delegate we had to write

def Hound do   defdelegate start_session,       to: Hound.Helpers.Session   defdelegate start_session(opts), to: Hound.Helpers.Session end 

to support both the case where opts uses its default value and the case where we pass opts .

From Elixir 1.3, we will now be able to write

def Hound do   defdelegate start_session(opts // []), to: Hound.Helpers.Session end 

which will allow the delegate to have the same signature as the delegated function. For more info, you can check proposal for the optional parameters and the proposal the deprecations .

Process.sleep addition

Until Elixir 1.2, there was no Process.sleep but we could very easily achieve the same this by using :timer.sleep . I believe the main reason Process.sleep/1 has been added to Elixir was to better document the fact that it is usually not a good idea to use it, but I could not find any releant information about why it was added in the mailing list.

Here are a few patterns where we should NOT use Process.sleep .

Waiting for a task to finish

We have Task.async and Task.await which is very clean when we want to do something asynchronously and wait for the result at some point.

do not

Task.start(fn -> do_something() end)  # wait until it's done Process.sleep(10_000) 

do

task = Task.async(fn -> do_something() end) Task.await(task, 10_000) 

We can also achieve something similar using a receive construct, which is actually what Task.await is using under the hood.

parent = self() {:ok, pid} = Task.start fn ->   res = do_something()   send parent, {:result, res} end receive do   {:result, _res} -> :we_are_done after   10_000 -> :timeout # we can use a timeout end 

Checking the state of a process

Another thing that should not be done using Process.sleep is waiting for a process to finish.

do not

{:ok, pid} = Task.start(fn -> do_something() end)  Process.sleep(10_000) # monitoring Process.alive?(pid) 

but do

{:ok, pid} = Task.start(fn -> do_something() end)  ref = Process.monitor(pid) receive do   {:DOWN, ^ref, _, _, _} -> :task_is_down after   10_000 -> :timeout # optional timeout end 

When to use Process.sleep ?

So, is there any case where it is relevant to use Process.sleep ? Clearly it would not be here if there were not. One of the case I use quite often is to wait before a retry, for example:

def execute(retries // 0) def execute(0), do: do_execute() def execute(n) do   case do_execute() do     {:ok, result} -> result     {:error, _err} ->       Process.sleep(5_000) # wait 5 secs before retrying       execute(n - 1)   end end  def do_execute do   # do something that could fail end 

For more info, you can take a look at the relevant issue and the commit introducing Process.sleep/1 .

Support of OTP optionalcallback

Erlang/OTP 18 has introduced the optional_callbacks attribute . Here is the description in the OTP changelog.

When implementing user-defined behaviours it is now possible to specify optional callback functions.

It is used to define a default implementation in a behaviour module, so that the modules implementing the behaviour can only keep the strict minimum.

This was already possible in Elixir, by using defoverridable/1 .

Here is a short example.

defmodule MyBehaviour do   @callback required_callback :: any   @callback optional_callback :: any    defmacro __using__(_opts) do     quote do       @behaviour MyBehaviour        def optional_callback do         IO.puts("I am the default implementation")       end        defoverridable [optional_callback: 0]     end   end end  defmodule ImplementingBehaviour do   use MyBehaviour    def required_callback do     IO.puts("I am the only one needed")   end    # but I can override `optional_callback` if I want end 

I therefore suppose the introduction of optionalcallback is to stay close to the Erlang/OTP implementation, which should I think make things simpler when working with Erlang behaviour directly.

The work on this one is still in progres, but you can check the relevant PR for more information.

New mix tasks

Elixir 1.3 also brings very neat mix tasks to get information about a project.

Until 1.2, we could use mix deps to show a project dependencies. This gave us an output like this:

* connection 1.0.2 (Hex package) (mix)   locked at 1.0.2 (connection)   ok * fs 0.9.1 (Hex package) (rebar)   locked at 0.9.2 (fs)   ok * gettext 0.11.0 (Hex package) (mix)   locked at 0.11.0 (gettext)   ok * ranch 1.2.1 (Hex package) (rebar)   locked at 1.2.1 (ranch)   ok * poolboy 1.5.1 (Hex package) (rebar)   locked at 1.5.1 (poolboy)   ok .... 

which is enough to know what is installed, but does not tell us anything on why is package X or Y required.

Elixir 1.3 introduces mix deps.tree , which gives us a nice dependency tree:

foobar ├── gettext ~> 0.9 (Hex package) ├── cowboy ~> 1.0 (Hex package) │   ├── cowlib ~> 1.0.0 (Hex package) │   └── ranch ~> 1.0 (Hex package) ├── phoenix_html ~> 2.4 (Hex package) │   └── plug ~> 0.13 or ~> 1.0 (Hex package) │       └── cowboy ~> 1.0 (Hex package) ├── phoenix ~> 1.1.4 (Hex package) │   ├── poison ~> 1.5 or ~> 2.0 (Hex package) │   ├── plug ~> 1.0 (Hex package) │   │   └── cowboy ~> 1.0 (Hex package) │   └── cowboy ~> 1.0 (Hex package) ........ 

The other addition in 1.3 is app.tree . Most applications will start other applications, and it can be handy to know who starts what.

At runtime, the :observer module can be used to get info about this, but the app.tree can now provide a quick overview without even having to run the app.

foobar ├── elixir ├── phoenix │   ├── elixir │   ├── plug │   │   ├── elixir │   │   ├── crypto │   │   └── logger │   │       └── elixir │   ├── poison │   │   └── elixir │   ├── logger │   │   └── elixir │   └── eex │       └── elixir ...... 

You can take a look at the deps.tree commit and the app.tree PR for more information.

Wrapping up

A lot of great stuff is coming in Elixir 1.3. I personally cannot wait to use the new features of with .

There are also a few things that got deprecated and will emit warnings, so I suggest you try to compile your project against Elixir master branch from times to times, this will probably save you some time later.

I am not sure if there is already a planned date for the release, but at the time I am writing this, there are only two issues opened in the 1.3 milestone , and a few PR that will probably be merged before we get to 1.3, so let’s hope we have a RC soon!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » What's coming in Elixir 1.3

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址