cjbottaro

cjbottaro

Getting a list of "tagged" modules

Easiest to ask this in code…

defmodule Foo do
  use Tagger
  tag(:red)
end

defmodule Bar do
  use Tagger
  tag(:blue)
end

Then at runtime I want to be able to say:

Tagger.all_tagged # %{ Foo => :red, Bar => :blue }

How to define Tagger to do this? I know how to use module attributes and macros to create methods on Foo and Bar to keep track of what they are tagged with, but how to aggregate that info into another module?

Thanks for the help.

Marked As Solved

voltone

voltone

I would probably use module attributes to store the tag in the compiled module:

defmodule Tagger do

  defmacro __using__([]) do
    quote do
      Module.register_attribute(__MODULE__, :tag, persist: true)
    end
  end

end

defmodule Foo do
  use Tagger
  @tag :red
end 

You can then inspect the attributes using module_info/1:

iex(1)> Foo.module_info(:attributes)[:tag]
[:red]

Given a list of modules you can then simply map, filter, aggregate, etc. using Enum.

As for the list of modules, you can use {:ok, modules} = :application.get_key(:my_app, :modules) to get all modules for your OTP app. Or, if you want to scan for modules more broadly, you can look at iex’s autocomplete for inspiration:
https://github.com/elixir-lang/elixir/blob/master/lib/iex/lib/iex/autocomplete.ex#L247

Also Liked

OvermindDL1

OvermindDL1

It works for any code that is indeed loaded. It is considered loaded when some function on the module is called, until that point it is not loaded (unless the erlang compiler option in the boot forces loads everything in the one file). Your modules will appear once you call something on them.

An application is loaded by the boot loader, but again that gets back into manual plugin definitions instead of dynamic detection. ^.^

voltone

voltone

If you look closely you’ll see that the IEx code only relies on :code.all_loaded() alone when :code.get_mode() returns anything other than :interactive.

When you start your application as part of a release, the :embedded mode is used, and all modules included in the release a loaded right after the VM has started. On the other hand, when you run iex on your Mix project, the code server uses :interactive mode in which modules are located and loaded on-demand, as @OvermindDL1 described.

As for application loading, iex -S mix loads your main application and all its dependencies (actually, it first loads the dependencies and then the main app). Some dependencies may explicitly load/start additional applications as they initialize.

RomanKotov

RomanKotov

Elxir Protocols have an interesting reflection feature. It looks like SomeProtocol.__protocol__(:impls) and can return a {:consolidated, modules}. It shows all modules, that implement the protocol. You can use this to tag modules, like:

defprotocol TagExample.Tagged do
  @spec tags(t()) :: [atom()]
  def tags(data)
end

defmodule TagExample.Tag do
  alias TagExample.Tagged

  defmacro __using__(_opts) do
    quote do
      Module.register_attribute(
        __MODULE__,
        :tag,
        persist: true,
        accumulate: true
      )

      defimpl TagExample.Tagged do
        def tags(_) do
          :attributes
          |> @for.__info__()
          |> Keyword.get(:tag, [])
        end
      end
    end
  end

  defimpl Tagged, for: Atom do
    def tags(module) do
      :attributes
      |> module.__info__()
      |> Keyword.get(:tag, [])
    end
  end

  @spec tagged_modules() :: [module()]
  def tagged_modules do
    modules =
      case Tagged.__protocol__(:impls) do
        {:consolidated, modules} ->
          modules

        _ ->
          Protocol.extract_impls(
            Tagged,
            :code.lib_dir()
          )
      end

    for module <- modules,
        module.__info__(:attributes)
        |> Keyword.has_key?(:tag),
        do: module
  end

  @spec modules_with_tag(tag :: atom()) :: [module()]
  def modules_with_tag(tag) do
    for module <- tagged_modules(),
        module
        |> Tagged.tags()
        |> Enum.member?(tag),
        do: module
  end
end

defmodule TagExample.TaggedModule1 do
  use TagExample.Tag

  @tag :tag1
  @tag :tag2
end

defmodule TagExample.TaggedModule2 do
  use TagExample.Tag

  @tag :tag2
  @tag :tag3
end

# iex> TagExample.Tag.tagged_modules()
# [TagExample.TaggedModule2, TagExample.TaggedModule1]

# iex> TagExample.Tag.modules_with_tag(:tag2)
# [TagExample.TaggedModule2]

# iex> TagExample.Tag.modules_with_tag(:nonexisting_tag)
# []

This example uses module attributes, but you can store tags somewhere else (possibly in methods).

I have also created a blog post with edge cases for this implementation.

Where Next?

Popular in Questions Top

9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Lily
In templates/appointment/index.html.eex: &lt;%= for appointment &lt;- @appointments do %&gt; &lt;tr&gt; &lt;td&gt;&lt;%= appoi...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New

We're in Beta

About us Mission Statement