Can a macro generate functions iteratively from list of atoms?

I have a use case that may require generating functions on the fly for lists of atoms. Considering a list such as

[:item_a, :item_b, :item_c]

Would it be possible to generate a macro that constructs functions as the ones below using a macro?

def fn_item_a(params) do
...
end

def fn_item_b(params) do
...
end

def fn_item_c(params) do
...
end

Quick test from the shell:

iex(1)> defmodule F do
...(1)> [:item_a, :item_b, :item_c]
...(1)> |> Enum.map(fn atom ->
...(1)> def unquote(:"fn_#{atom}")(param) do
...(1)> {:ok, param}
...(1)> end
...(1)> end)
...(1)> end
{:module, F,
 <<70, 79, 82, 49, 0, 0, 5, 200, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 160,
   0, 0, 0, 17, 8, 69, 108, 105, 120, 105, 114, 46, 70, 8, 95, 95, 105, 110,
   102, 111, 95, 95, 10, 97, 116, 116, 114, ...>>,
 [fn_item_a: 1, fn_item_b: 1, fn_item_c: 1]}
iex(2)> F.fn_item_a("value")
{:ok, "value"}

2 Likes

Thanks!

Notice that the code above is not a macro. It does make use of unquote fragments.

I often like to use for comprehensions in cases like this. Same outcome, just an alternative approach:

defmodule F do
  for name <- [:item_a, :item_b, :item_c] do
    def unquote(name)(param), do: {:ok, param}
  end
end
5 Likes

Yes, you can but please please do not do this. I am working on a codebase where this is done here and thee and it is the worst. Every time I come across a part of the code where this was done I scream bloody murder and wish I could go back in time and slay my coworkers grandfather to paradox him out of this timeline (I otherwise really like him though).

Tl;Dr it makes debugging annoying.

1 Like

Annoying because the stack trace on a function clause error is hard to track down, or something else?

A lot of my libs have functions generated from data (CLDR stuff) and I use this approach extensively without any particular debugging issue with that one exception (function clause error when there are a lot of clauses). I wrote a custom exception formatter for testing to help with that though and it hasn’t been an issue since.

Hence curious about your observations on the issues of debug-ability.

3 Likes

Could you give an example of the data, generator and the generated functions?

I think this is only feasible if the data is somehow standardized and correct, and the thing you want to generate from it is clear, trivial, error-free and final (never changes).

1 Like

Two primary examples I work with:

The data is standardised and clear but the data sets are also non-trivial and never final (bi-annual revisions). One example module is Unicode.Block.

5 Likes

Doing this would’nt be too useful (and fun) if the data wouldn’t change. I meant the schema.
I think its important that you can really rely on the macro-code and thats only possible if that code is simple enough and doesn’t change too often.

like this code for example, very clear and understandable. Would’nt be frightened to use this.

Thanks for sharing this. I’m thinking about implementing sth like this for some (thousands) data-types defined in XML. Seeing this approach being used in an important lib gives me hope it could be feasible.

<DatapointTypes>
<DatapointType Id="DPT-1" Number="1" Name="1.xxx" Text="1-bit" SizeInBit="1" PDT="PDT-50">
 <DatapointSubtypes>
    <DatapointSubtype Id="DPST-1-1" Number="1" Name="DPT_Switch" Text="switch" Default="true">
       <Format>
	  <Bit Id="DPST-1-1_F-1" Cleared="Off" Set="On" />
       </Format>
    </DatapointSubtype>
    ...
 </DatapointSubtypes>
</DatapointType>
...
<DatapointType Id="DPT-11" Number="11" Name="11.xxx" Text="date" SizeInBit="24" PDT="PDT-6">
 <DatapointSubtypes>
    <DatapointSubtype Id="DPST-11-1" Number="1" Name="DPT_Date" Text="date">
       <Format>
	  <Reserved Width="3" />
	  <UnsignedInteger Id="DPST-11-1_F-1" Width="5" MinInclusive="1" MaxInclusive="31" Unit="Day of month" />
	  <Reserved Width="4" />
	  <UnsignedInteger Id="DPST-11-1_F-2" Width="4" MinInclusive="1" MaxInclusive="12" Unit="Month" />
	  <Reserved Width="1" />
	  <UnsignedInteger Id="DPST-11-1_F-3" Width="7" MinInclusive="0" MaxInclusive="99" Unit="Year" />
       </Format>
    </DatapointSubtype>
 </DatapointSubtypes>
</DatapointType>
...
<DatapointType Id="DPT-232" Number="232" Name="232.xxx" Text="3-byte colour RGB" SizeInBit="24" PDT="PDT-19">
 <DatapointSubtypes>
    <DatapointSubtype Id="DPST-232-600" Number="600" Name="DPT_Colour_RGB" Text="RGB value 3x(0..255)">
       <Format>
	  <UnsignedInteger Id="DPST-232-600_F-1" Width="8" Name="R" />
	  <UnsignedInteger Id="DPST-232-600_F-2" Width="8" Name="G" />
	  <UnsignedInteger Id="DPST-232-600_F-3" Width="8" Name="B" />
       </Format>
    </DatapointSubtype>
 </DatapointSubtypes>
</DatapointType>
...
</DatapointTypes>
1 Like

More than happy to be a sounding board if you go down that path and want to talk over ideas or roadblocks.

4 Likes

Thank you all for the thorough and detailed answers to this question. After reading them, looking at the CLDR libraries and getting back to the developers who will be using my code, I found out that

  • The schema from which the functions stem is quite stable
  • The determination of which attributes to query is not finalized at this point, since it must connect to various types of real-time data whose schemas are also under design
  • All exposed functions must be documented for that particular reason
  • The functions which I intended to generate are precisely those that need to be exposed

Hence, I ended up going the macro/unquote route, while keeping in mind how to do when adequate.

2 Likes