[Question] Support for __using__/3

60 views
Skip to first unread message

Yordis Prieto

unread,
Aug 8, 2021, 4:15:59 PM8/8/21
to elixir-lang-core
I am trying to do the following,

defmodule Command do
  defmacro __using__(opts \\ [], do: block) do
    unless Keyword.has_key?(opts, :aggregate_identifier) do
      raise ArgumentError, "Missing :aggregate_identifier key"
    end

    aggregate_identifier = Keyword.fetch!(opts, :aggregate_identifier)

    quote do
      use Ecto.Schema
      @primary_key { @aggregate_identifier_key, :binary_id, autogenerate: true }
      @derive Jason.Encoder
       embedded_schema unquote(block)
    end
  end
end


Just so I can do something like,

defmodule CreateRecurringTransfer do
  use Command, [aggregate_identifier: :id] do
    field(:dtstart, :naive_datetime)
    field(:dtend, :naive_datetime)
    embeds_one(:amount, PositiveAmount)
    embeds_one(:rrule, Rrule)
    embeds_one(:owned_by, CustomerIdentity)
    embeds_one(:from_account, TransferAccount)
    embeds_one(:to_account, TransferAccount)
  end
end

But I get `** (CompileError) iex:3: undefined function use/3` which I assume what I am trying to do is not supported.

I am curious to understand why it doesn't work (beside maybe not being supported), and what would be the problem by trying to support it

Adi

unread,
Aug 8, 2021, 7:54:35 PM8/8/21
to elixir-lang-core
I am not sure if supporting use/* is the right way forward here. The `Kernel.use/2` macro is reserved for invoking a module's `__using__/1` macro. To be honest, I don't like the fact that you're setting the module attributes and defining the `embedded_schema` in the same macro. You can easily just define the `@primary_key` in the `__using__/1` macro and explicitly define the `embedded_schema` in `CreateRecurringTransfer` itself, but that's my preference.

Either way, if you have defined a `__using__/3` macro, you can always invoke it explicitly. Although I would rename it to avoid any confusion for developers reading the code in the future and potentially thinking it has something to do with `Kernel.use/2`.

defmodule Command do
  defmacro aggregate_schema(opts \\ [], do: block) do

    unless Keyword.has_key?(opts, :aggregate_identifier) do
      raise ArgumentError, "Missing :aggregate_identifier key"
    end

    aggregate_identifier = Keyword.fetch!(opts, :aggregate_identifier)

    quote do
      use Ecto.Schema
      @primary_key { @aggregate_identifier_key, :binary_id, autogenerate: true }
      @derive Jason.Encoder
       embedded_schema unquote(block)
    end
  end
end

defmodule CreateRecurringTransfer do
  require Command
  Command.aggregate_schema [aggregate_identifier: :id] do

    field(:dtstart, :naive_datetime)
    field(:dtend, :naive_datetime)
    embeds_one(:amount, PositiveAmount)
    embeds_one(:rrule, Rrule)
    embeds_one(:owned_by, CustomerIdentity)
    embeds_one(:from_account, TransferAccount)
    embeds_one(:to_account, TransferAccount)
  end
end

Paul Schoenfelder

unread,
Aug 8, 2021, 8:59:03 PM8/8/21
to 'Justin Wood' via elixir-lang-core
It looks to me like the error is due to a stray comma: `use Command, [aggregate_identifier: :id] do` should be `use Command [aggregate_identifier: :id] do`, which should work as you’d expect.

As an aside, you might be interested in https://github.com/bitwalker/strukt, which looks like it has a lot of overlap with what you’re building (though what you are shooting for may be totally different, I just see some similarities)

Paul
--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Yordis Prieto

unread,
Aug 9, 2021, 12:35:26 AM8/9/21
to elixir-lang-core
Hey Paul,

I can't remove the comma from it thou, I get the following error, `use` macro expects 2 arguments as far as I can tell

```
** (ArgumentError) invalid arguments for use, expected a compile time atom or alias, got: Command[[aggregate_identifier: :id]]
    (elixir 1.11.2) lib/kernel.ex:5006: anonymous fn/3 in Kernel."MACRO-use"/3
    (elixir 1.11.2) lib/enum.ex:1399: Enum."-map/2-lists^map/1-0-"/2
    (elixir 1.11.2) expanding macro: Kernel.use/2
```

Yordis Prieto

unread,
Aug 9, 2021, 12:42:53 AM8/9/21
to elixir-lang-core
aditya7, I dont disagree with you.

There is always a balance between magic and verbosity. The reason I don't mind "magic" in Elixir is that everything is explicit and macros are expanded at compile time. So my intention is to make the code as simple as possible at the cost of hidden complexity that I am OK with it at the moment. Everything with balance, this is one of those cases where it is just a bag of data and few things, the easier the better.

In the worst case, I will end up doing a version of what you proposed, I am trying to avoid having to import a module and having to call yet another function at the moment.

Paul Schoenfelder

unread,
Aug 9, 2021, 12:58:14 AM8/9/21
to 'Justin Wood' via elixir-lang-core
Sorry, for some reason I completely overlooked that you were doing this with `use`. I would second the recommendation to avoid that approach. Since `use` is a special form, it’s likely you can’t do what you’re trying to do here anyway. The bigger issue is that it’s very unidiomatic - while it might save you one extra line of code versus an explicit macro, it’s very different from how most libraries expose this kind of functionality, and much less composable. 

Ultimately though, I think you’ll be limited by the fact that `use` is a special form (IIRC, I haven’t confirmed that you _can’t_ do what you’re trying to do, but I suspect that’s the thing tripping you up).

Yordis Prieto

unread,
Aug 9, 2021, 1:04:33 PM8/9/21
to elixir-lang-core

I hear you, I am curious to know why it can't be done. "Idiomatic" code comes from the capabilities of the language, I don't disagree with you, but we didn't have `with` at some point as well.

I notice that we do a lot of macros to initialize module attrs, or include other macros, and sometimes, we do it only to use one macro in some way (yes, I understand that I can use `require`)

Not sure,  I dont need it 1000%, but it is nice to have it honestly, there is more cohesion (sometimes) among why `use` and the intention.

P.S: I may move with what I have today, or do something around what aditya7 proposed
Reply all
Reply to author
Forward
0 new messages