Fallback to nearest prior imported library module and not necessarily Kernel when overriding an operator?

I may be wrong here, but I really don’t see a proper way to override an operator in one’s own library while allowing for a fallback to whatever library module the user imported before it that also overrides the same operator (or any other function or a macro), e.g.:

use OtherLibrary # imports its `*` override for maps
use MyLibrary # imports its `*`override for lists

2 * %{ a: 1}
# => MyLibrary passes it to the whatever prior library overriding `*` i.e. OtherLibrary and OtherLibrary does its  job
2 * [ a: 1]
# => MyLibrary does its job
2 * 2
# => 4 (MyLibrary passes it to OtherLibrary which passes it to Kernel)

The idea is to fetch the last prior import by relying on Macro.Env.lookup_import/2, and it works, but I have another problem. All I can do with the import prior to mine in MyLibrary.__using__/1 is import unquote( prior_import), except: [ *: 2] and that’s not exactly what I need. The reason is I cannot afford to assume that the caller is ok with my library importing all other macros and functions from the OtherLibrary module (otherwise unknown to me), and I don’t know of any other way to un-import just this particular function or macro.

Any ideas?

How about Kernel.defoverridable/1?

Never applied it in a case like this. This is not an “inheritance” case. You think it will work?

Nope, I can’t get it to work. Besides, even if it did work, it would require other libraries to define it too.

It would be easier if you would give something from you like an example Elixir script or at least tell us what error you have.

I can’t get it to work.

does not tell us anything.

Ok. For starts, here’s the sketch of the original that I explained above that works, but imports all functions and macros from a prior library (the operator is - but it’s still the same point).

defmodule Helpers do
  def prev_import( caller_env, fun_name, arity, to_reject) do
    Macro.Env.lookup_import( caller_env, { fun_name, arity})
    |> Enum.reject( & &1 in to_reject)
    |> List.last()
    |> then( & &1 && elem( &1, 1))
  end
end

defmodule OtherLibrary do
  defmacro __using__( _) do
    prev_import = Helpers.prev_import( __CALLER__, :-, 1, macro: OtherLibrary)

    quote do
      import unquote( prev_import), except: [ -: 1]
      import OtherLibrary, only: [ -: 1]
    end
  end

  defmacro -value do
    IO.inspect( value, label: "OtherLibrary")
    OtherLibrary.doit( value, __CALLER__)
  end

  def doit( { :&, _, _}, _caller_env) do
    IO.puts( "OtherLibrary caught it!")
  end

  def doit( other, _caller_env) do
    quote do
      -unquote( other)
    end
  end
end

defmodule MyLibrary do
  defmacro __using__( _) do
    prev_import = Helpers.prev_import( __CALLER__, :-, 1, macro: MyLibrary)

    if prev_import && __CALLER__.module do
      Module.put_attribute( __CALLER__.module, :prev_import, prev_import)
    end

    quote do
      import unquote( prev_import), except: [ -: 1] # note: this is a problem for it imports all else even if unwanted
      import MyLibrary, only: [ -: 1]
    end
  end

  defmacro -value do
    IO.inspect( value, label: "MyLibrary")
    MyLibrary.doit( value, Module.get_attribute( __CALLER__.module, :prev_import))
  end

  def doit( { :%{}, _, _}, _) do
    IO.puts( "MyLibrary caught it!")
  end

  def doit( other, prev_import) do
    IO.puts( "MyLibrary passing it on to prev..")
    IO.inspect( prev_import)

    if prev_import do
      quote do
        unquote( prev_import).-( unquote( other))
      end
    else
      quote do
        -unquote( other)
      end
    end
  end
end

defmodule LibraryUser do
  use OtherLibrary
  use MyLibrary

  def run_map() do
    -%{ a: 1}
  end

  def run_capture() do
    -&bla
  end

  def run_other() do
    -2
  end
end
  • edit: removed obsolete comments from code

Regardless of the defoverridable/1, if I remove the lines with import unquote( prev_import) .. it will complain about ambiguous call of the - operator. If I don’t remove the lines, I am importing all the functions and macros.

I mean as a general rule, I would simply not try to do this at the module level. If a library wants to override an operator that should be lexically scoped inside one function at a time.

3 Likes

There is no good solution for this. Look that if you do import … except … then what happens with all previous imports?

For example:

defmodule Example do
  import Kernel. except: [+: 2]
  use OtherLibrary
  use MyLibrary

  # …
end

Firstly I would go for: Defining custom operators. and then I would register an (accumulate?) attribute :prev_import and within use Helper I would get said attribute and define a custom function/macro depending on value of such attribute.

Alternative both libraries could put an attribute and add @before_compile which would work as same as use Helper.

Yes, and there’s also another solution and that is not to stop at just taking the prev_import but then also (while still in compile time) take all of the module’s macros and functions and exclude them all unless already imported, but it’s already getting cumbersome.

My point being:

  1. There’s a scarcity of custom operators, unary in particular.
  2. It wouldn’t hurt if import had an additional option (say :not) to un-import some but not import all other functions/macros.

Just sayin’

Your observation is valid if the library is a very specific one. But if you’re developing something of a very generic purpose that can be applied all over your code base, then evading this problem will hardly do it. Still, even if done on a function by function basis, I am simply reluctant of importing all of third party module’s functions/macros just because I need to exclude one.

What unary operator could be added? So far I can see only $ free. The problem is that Elixir core does use most of ASCII characters and there is no much place here.

That would not happen. Firstly I did not saw anyone else needing this and because of it Elixir core team would most probably not accept it. Also at least for me it looks like you are trying to overcomplicate the problem and you should search for an easier solution to do same/similar thing.

The specifics matter here a lot though honestly. Your example is about operator overloading which really ought to be done very sparingly as it is not zero cost. Do you have other concrete examples where this is an issue? Are these your libraries or external ones?

  1. $, ~ and all special forms being overridable operators (can’t tell if it is easily or anyhow doable though). Also, being able to define different arity for non-unary operators to become ones. PS. I am aware that ~ is used for sigils, but nevertheless.

  2. It’s their right to decide whatever they want. I expressed my opinion and I think it’s valid.

Besides, do note that I am using macros and not functions there. It’s the only way to take precedence to re-interpret the otherwise special forms.

I agree that it comes at an additional cost unless there’s a way to quote the fallback to the original operator (which was my intention, btw).

In my example above the “OtherLibrary” is an external one that I don’t have an influence over. It’s a hypothetical library. My intention was (as the title suggests) to make my library (that overrides a unary operator) resilient and flexible to situations where the user is also using another such library that I do not know of. That’s all to it.

I’m less-opposed to custom operators than most, but IMO if you’ve got different overrides for the same operator overriding each other you may want to consider a different design.

Setting the specifics of operators aside, it sounds like what you want is something where you can import heads for one function from multiple modules:

# HYPOTHETiCAL: THIS CODE DOES NOT WORK - SEE NOTES BELOW

defmodule ImplOne do
  def some_fun(x) when is_list(x), do: IO.puts("list!")
end

defmodule ImplTwo do
  def some_fun(x) when is_map(x), do: IO.puts("map!")
end

defmodule Demo do
  import ImplOne
  import ImplTwo

  def do_stuff do
    some_fun([1,2,3])
    some_fun(%{a: 1})
  end
end

This currently complains:

** (CompileError) iex:11: function some_fun/1 imported from both ImplTwo and ImplOne, call is ambiguous
    (elixir 1.14.0) src/elixir_dispatch.erl:149: :elixir_dispatch.expand_import/7
    (elixir 1.14.0) src/elixir_dispatch.erl:119: :elixir_dispatch.dispatch_import/6
    (elixir 1.14.0) src/elixir_expand.erl:567: :elixir_expand.expand_block/5
    (elixir 1.14.0) src/elixir_expand.erl:40: :elixir_expand.expand/3
    (elixir 1.14.0) src/elixir_clauses.erl:37: :elixir_clauses.def/3
    (elixir 1.14.0) src/elixir_def.erl:197: :elixir_def."-store_definition/10-lc$^0/1-0-"/3
    iex:6: (file)

TBH this style of importing reminds me of the worst parts of OO, where you end up playing “your method is in another castle” through twelve classes all named Base.


If you’re doing something like this codebase-wide, I’d recommend combining all the relevant plumbing into one module that gets used consistently. That module may not be pretty, but it will be centralized - the thing you want to avoid is having your codebase littered with slightly-different sets of operators etc in different modules.

2 Likes

I agree with you completely, but that’s not the point here. I explained my concerns here:

The question was what unary operator could be added - overriding is not adding. :smiley: That’s important to say as you only repeat after me one character $, so in fact you confirm there is not much to add here and definitely not many things.

You need to understand that any overrides and multiple meaning of same operators is more confusing than helping especially when we are talking about % as every new developer would think about maps in first place. Also SpecialForms naming is not random. If you really want to write for example your own for special form then you would need to go outside Elixir.

But then those would no longer be operators. :sweat_smile:

When starting with programming and Elixir I have also expressed many things and found myself that many of them does not makes sense at all only because I did not understand something.

Let’s go back to your question … If you really want to go this way then consider splitting your macros.

defmodule Example do
  use First
  use Second
  import First, only: [-: 1]
  # or
  # import Second, only: [-: 1]
end

This is simplest way without going around. The developer could decide which function/macro should be imported. Also it’s highly recommended to prefer import in favor of use when everything you do is to import functions/macros.

1 Like

I meant functionally ‘added’ not added in literal sense.

Personally, I had no intention of overriding for. The special forms that I was referring to are: ^, &, ., :, =.

Don’t agree with that. For instance ** can just as well be interpreted as a unary operator meaning same as x ** 2, to name one. Not to mention (Godforbid) the ++ and -- as used in C/C++. Not operators?

Me too. First in 1988 with C, then in 1998 with Java, then …, and then in 2019 with Elixir. Must say though that for Javascript at least, I still find many of my old remarks valid :slight_smile:

Yes, this is a valid approach, but I don’t like it (the “batteries not included” style). But that’s just me. Turns out I’ll be most likely writing that “cumbersome” (yet compile-time so it doesn’t count as much) piece of code that collects all not previously imported functions and macros only to exclude them along with the one for my operator.

I don’t get “functionally added”. They exists as a part of language. They are named “special” for purpose.

Special forms are the basic building blocks of Elixir, and therefore cannot be overridden by the developer.

It’s like you want to replace foundations of house and expect that nothing bad would happen. Special forms are not even macros like the ones you are writing. They are expanded by compiler and this is a completely different topic.

wait, wait

Non-unary operators like a = b have always 2 arity and you wrote about different arity. How do you see 3 or 4-arity operator?

The problem is that you would need to describe that weird behaviour to all developers using you library. The inspecting is only in examples. Most probably you would not log them and that would be extremely confusing.

The good practice is to write a code and be able to describe what it does even if you stop touching it for let’s say 6 months. In your case the developer would need to remind which macros are overriding operator and in which order. Now think that you decide to change behaviour of your library and this time you want to take operator from first macro. If so would happen nobody maybe except maintainers would follow the whole process.

Hiding everything is not always the best way and in some cases it may be seen as a bad practice.