How do I add functions to a base module

I have seen the __schema__ (guess it’s to be considered private) function in my schemas.
and I need introspection, in my database schema (to assist users when forming queries, but also because I need to parse them and translate them (see this topic)), so I need to access :associations and :fields.

I have the idea that these three function would be highly useful, and compact:


  def list_associations() do
    __schema__(:associations)
  end

  def list_fields() do
    __schema__(:fields) |>
      Enum.filter(fn(a) ->
        not(Atom.to_string(a) =~ ~r/(^|_)id$/)
      end)
  end

  def get_association(name) do
    __schema__(:association, name)
  end

I want to have them in each of my modules, but without copying the same three def in each and every module source file.

???

their presence allows me writing things like this (check the example structure):

Botany.Plant.list_associations()
[:location, :accession]
iex(12)> Botany.Plant.get_association(:accession).related.list_associations()
[:taxa, :verifications, :plants]
iex(13)> Botany.Plant.get_association(:accession).related.get_association(:taxa).related.list_associations()
[:children, :verifications, :accessions, :parent, :rank, :accepted, :synonyms]
iex(14)> 

actually, I would very much like to see this style adopted in Ecto, but I’m also fine with learning how to define my own Ecto.Schema module that does precisely the same as Ecto.Schema, but includes a few commodities of my liking.

You can use use or more explicit macro calls to inject any code in your module.

can you include a simple silly example of this? I have tried use and __using__, and don’t quite manage.

In general this would be better off if you put them in another module and took the schemas as arguments:

Botony.Plant |> MyApp.Schema.list_associations

In Elixir, this kind of composition where you pass values into functions and then further functions is more common than trying to treat the modules as objects and inherit functions in each.

3 Likes

I would like to treat modules as namespaces, which they are, as far as I understand.

Right, but my point is that you’re copying the same exact function into N different modules. This would be better as a singular functions in a single module that you pass arguments to:

Botany.Plant
|> Schema.get_association(:accession)
|> Schema.related
|> Schema.list_associations()

you mean that the several list_associations etc functions would be distinct functions? all with separate binary implementation? no, that’s not what I wish.

If you want them to be in every module that is a schema, then that’s what happens.

You can avoid actually typing it N times by using use and meta programming, but if you are having them as functions in the module, then it’s still duplicating the code. You can minimize this some with defdelegate. Regardless though, the best way to have functions that are available for a wide range of inputs is to just put them in a module and pass your inputs in as arguments. In this case your schema module names are the inputs, they aren’t really namespaces, they’re parameters as far as this functionality is concerned.

3 Likes

tried the consequences of your hint on the code, and still, it feels really clumsy, doing it this way. after all, we do that for the __schema__ function, I can just as well, I consider, jump on the same train and do the same for a couple more functions.

I only still do not understand how to do that. short of physically copying the functions in each module.

please consider I have one month experience with this language, so I do not yet know how to write my own __using__ macros.

OK, let me show you how to do it the way you want:

defmodule Botony.SchemaHelpers do
  defmacro __using__(_) do
    quote do

      def list_associations() do
        __schema__(:associations)
      end

      def list_fields() do
        __schema__(:fields) |>
          Enum.filter(fn(a) ->
            not(Atom.to_string(a) =~ ~r/(^|_)id$/)
          end)
      end

      def get_association(name) do
        __schema__(:association, name)
      end
    end
  end
end

Then in your schema you woud have:

defmodule Botony.Plant do
  # after any other `use`
  use Botony.SchemaHelpers
  # other stuff
end

:blush:
¡thank you!
it looks suspiciously similar to one of my attempts.
too bad I did not save and commit each and every attempt, because there must be a difference that allows your code to work, and prevented mine from doing the same.

The difference is that __schema__ is a reflection function. In order to reflect, you have to define it within the module itself. @benwilson512’s advice is correct, is better to encapsulate those functions in a module that you reuse. It will feel clumsy now, especially if you are coming from OO languages, but once you get more familiar with Elixir, the code injection everywhere will be the one feeling clumsy.

6 Likes

the Schema module you suggest, looks like this from my point of view:

defmodule Botany.Schema do
  def list_associations(module) do
    module.__schema__(:associations)
  end

  def list_fields(module) do
    module.__schema__(:fields) |>
      Enum.filter(fn(a) ->
        not(Atom.to_string(a) =~ ~r/(^|_)id$/)
      end)
  end

  def get_association(module, name) do
    module.__schema__(:association, name)
  end

  def related(value) do
    value.related
  end
end

it also needs the related function, which was a field in the schema structure.

and would be used as you say:

iex(29)> Botany.Plant |> 
...(29)> Botany.Tools.get_association(:accession) |> 
...(29)> Botany.Tools.related() |> 
...(29)> Botany.Tools.get_association(:taxa) |> 
...(29)> Botany.Tools.related() |> 
...(29)> Botany.Tools.get_association(:rank) |> 
...(29)> Botany.Tools.related()
Botany.Rank

I will keep it around, for when I will change my mind.

right, and that is what I would really want to achieve. that I write use Botany.BaseSchema, which would be a module expanding Ecto.Schema, so I do not need to use both.

is that possible?

can you expand on the meaning “it’s a reflection function”?

It is possible with the code Ben shared, you can add use Ecto.Schema inside the quote do block. But to be double clear: generating a bunch of functions like this into modules is not considered best practice in the Elixir community. We limit ourselves to reflection functions (functions that return metadata) or for callbacks from behaviours.

2 Likes