Why `Ecto.Query` is usually `import`ed, instead of `alias`ed?

So in all elixir code I have read (not that many yet), it is always

import Ecto.Query

query = from(e in MyApp.Element)

that is used instead of

alias Ecto.Query

query = Query.from(e in MyApp.Element)

Is there any specific reason for that?

In general, what is a good practice between import and alias? It feels like import makes things less explicit, which reduces code comprehension. But then why such an exception?

Thanks

3 Likes

There isn’t a ton of difference when you’re just using from, although if you’re using |> where a lot the alias can feel verbose.

To your more general question though: Imports make sense when you want a DSL, and are particularly safe when that DSL is unlikely to be unambiguous. Aliases make sense for most everything else. To pick an example from my code base:

shipment
|> Model.SensorInstallation.in_shipment()
|> where(location: ^location)
|> join(:inner, [si], s in assoc(si, :sensor), as: :sensor)
|> where([sensor: s], s.service_token_id == ^token.id)
|> preload([sensor: s], sensor: s)
|> Repo.all()

If you only aliased query you’d have:

shipment
|> Model.SensorInstallation.in_shipment()
|> Query.where(location: ^location)
|> Query.join(:inner, [si], s in assoc(si, :sensor), as: :sensor)
|> Query.where([sensor: s], s.service_token_id == ^token.id)
|> Query.preload([sensor: s], sensor: s)
|> Repo.all()

To me, this impairs readability a fair bit because to see what’s actually going on you need to skip over the Query part of each line, cause it isn’t really telling you anything useful.

I do agree that import should be used sparingly. I did a quick grep through my code base and 99% of all imports are for either Ecto.Query or Ecto.Changeset. I think these end up being the most common ones to import because, in the context in which they are imported, ambiguity is non existent and there are frequently a TON of calls to functions in those modules all right next to each other, and without the import it would obscure what’s actually going on.

2 Likes

But alias, unlike import, wonĘĽt allow macro calls, one needs an explicit require as well to use from, which is a macro.

4 Likes

Ecto.Query.from/2 is a macro, therefore an alias alone is not enough, you need to additionally require it.

2 Likes

@augnustin That’s really good question!

Correct me if I’m wrong, but if we would follow that rule then the same could be applied to Repo in contexts like for example:

defmodule MyApp.Blog do
  alias MyApp.Blog.{Comment, Post}
  alias MyApp.Repo # or import MyApp.Repo

  # instead of:
  def comments do
    Repo.all(Comment)
  end

  def posts do
    Repo.all(Post)
  end
  # and so on …

  # we may write:
  def comments do
    all(Comment)
  end

  def posts do
    all(Post)
  end
  # and so on …
end

Now consider using ecto_shorts. Ok, we may import EctoShorts.Actions and that would of course work, but does it really improves readability? I don’t think so. The biggest problem is that without checking import most people would expect to use Ecto.Repo callbacks instead of EctoShorts.Actions API which may be confusing especially for bigger codebases.

Ok, I have used Ecto.Repo instead of modules you mentioned, so somebody may say that’s an other case … Is that really so? Do you think that Ecto.Changeset and Ecto.Query covers exactly 100% of all use cases and literally nobody would like to write let’s say custom MyApp.QueryBuilder with for example custom where? Wait … Doesn’t EctoShorts shows that’s not a new idea?

Ok, we may import Ecto.Query and alias MyApp.QueryBuilder, but for me both modules have similar purposes and using different rules for them looks inconsistent, so I can’t think where import Ecto.Query is good especially in cases we have modules which where written in similar purpose.

I would like also to quote some useful resources:

I understand the value of the identity function. But the cons of adding it to Kernel is that we partially take the name away from everyone else, unless they unimport it, and ultimately the implementation of identity/1 is not doing much to wararnt this loss.

Source: https://groups.google.com/g/elixir-lang-core/c/tB61BHYIH1s/m/XDd85m0pBgAJ

I don’t think my principle here would suggest aliasing Repo, for two reasons. First, it simply isn’t the case that there are large chains of |> Repo.x calls where x is obfuscated without importing Repo. Rather, there are usually a sequence of Ecto.Query calls followed by one call to Repo.

Secondly, the Repo calls are impure, and there’s a lot of value in seeing the Repo module on each callsite to make it clear when the impure call is happening.

1 Like

I agree that there are more calls to Ecto.Query, but it’s not enough good reason for me. Same could go for Enum, Flow and Stream which could have similar or even bigger number of calls. Some functions from those modules have also similar purposes like filtering (Enum.filter and Enum.reject vs Query.where) or ordering (Enum.sort_by vs Query.order_by).

If you prefer shorter syntax without module name you may just use different flavor:

Ecto queries come in two flavors: keyword-based and macro-based.

One may prefer alias everything and use macro-based flavor and other one could use alias only for few calls and for rest use keyword-based flavor. In such case import does not looks as well as before.

Just to make sure we’re on the same page, are you arguing that Ecto.Query should not be imported? Or merely that my particular arguments in favor of importing Ecto.Query are not good arguments?

I think a big reason Ecto.Query tends to get imported rather than aliased is the imported form (at least in my opinion) tends to read more like writing a query vs the aliased form reading more like transforming a query structure. Even though the latter is accurate, the former feels more immersive with the Ecto DSL.

I would agree, though, that imports aren’t the most explicit way to call another module’s functions. Personally I like to take advantage of the :only option when I use import. In my opinion this makes a function’s origin much clearer, especially when multiple modules are being imported.

import Ecto.Query, only: [from: 2]
5 Likes

Just to make sure we’re on the same page, are you arguing that Ecto.Query should not be imported?

Depends … if import does not improve performance or if it’s not giving really noticeable improve of readability then yes especially before Elixir version 1.11.0. Your first argument to not have unnecessary large chains of |> Query. would be much better if there would be no keyword-based flavor, but when it exists I don’t see a big difference between using import and alias + keyword-based syntax. Ecto.Changeset is a bit different story here.

Why I mention 1.11.0? In linked article there is an update:

UPDATE #2: Elixir v1.11+ will no longer consider imports as compile-time dependencies. Therefore converting imports to aliases is no longer strictly necessary for improving recompilation times. This article, however, can still be useful for those interested in converting imports to aliases for code readability reasons or for those willing to learn more about compilation tracers.

Source: https://dashbit.co/blog/rewriting-imports-to-aliases-with-compilation-tracers

I have not worked in this topic, so I can only be based on such articles, but it looks like that in previous Elixir versions many import calls like that causes longer recompilation times in many projects.

Or merely that my particular arguments in favor of importing Ecto.Query are not good arguments?

Ok, “not good” would be too much. I would say that having mentioned 2 flavors in mind your arguments about readability don’t convince me. For sure I’m not expert, so that’s just my own opinion. Personally I prefer alias even with macro-based flavor, but I would not say for example to not use keyword-based flavor etc.

@augnustin Looks like @brettbeatty hit a point! Now he reminded me that may be a case. So API with import as default was designed for people new in Elixir in order to minimize effort when learning and using ecto query API. Making it working like that by default in generated projects have much more sense. Personally I still prefer alias, but in project where people have strong SQL knowledge and low Elixir skills I may consider using this for others.

100% agree

For me the big difference between Flow, Stream, … and Ecto.Query is the fact that basically everything in the latter is a macro. From an performance perspective it doesn’t matter if you do require Ecto.Query or import Ecto.Query. Using macros will create a compile time dependency no matter what.

So which one is preferred should come down to preference of shorter function calls (local calls) vs. remote calls with an module/alias. I feel for this particular case the first is usually chosen not just for the shorter pipeline syntax, but also for plain from x in X, … instead of Query.from ….

If however we’re not talking about macros, but functions (they even tend to get more in Ecto.Query) then aliases are imo to be preferred (before 1.11 at least, as described before).

3 Likes