A good way to define a lot of similar functions

I am writing a library and trying to figure out what would be a good way to define a lot of similar functions. So for example if I have code like this, but I have to do this thing basically for hundreds of functions. Just have to figure out what variables are being passed and pass them on to the main function based off of that.

def first(base, x, y) do
  main(base, "first", [x, y])  
end

def first(base, list) when is_list(list) do
  main(base, "first", list)
end

def first(list) when is_list(list) do
  main(Base.new(), "first", list)
end

def first(x, y) do
  case Base.is_base(x) do
    false -> main(Base.new(), "first", [x, y])
    true -> main(x, "first", [y])
  end
end

def second(base, x, y) do
  main(base, "second", [x, y])
end

def second(x, y) do
  case Base.is_base(x) do
    false -> main(Base.new(), "second", [x, y])
    true -> main(x, "second", [y])
  end
end

Only idea how to simplify this was to generate functions using a macro, but that way I would lose IDE completions and usability for anyone who would implement this. So I am thinking that is not the best design for library? Is there a way to keep the explicitness while also automating this process somehow? other thing than macro I could think of would be actually generating code.

If you have many similar functions, I would rethink the whole API.
In your example, instead of creating first and second (and third? etc), I would consider the function nth with integer as a parameter. Or anything else as a parameter, like atoms :first, :second, etc.

1 Like

This is how get, put, post etc are defined in Phoenix.Router:

2 Likes

Thank you! I guess this kind of validates my original idea of automatically generating functions. :thinking: I am just not that big fan that the source code is not that explicit.

If you have many similar functions, I would rethink the whole API.
In your example, instead of creating first and second (and third ? etc), I would consider the function nth with integer as a parameter. Or anything else as a parameter, like atoms :first , :second , etc.

Issue here unfortunately is that it is a wrapper for another api, so I want to keep the original form, but I guess in this case even more automatically generating functions would be okay, as the one who implements this would use original API documentation.

Why not just go the simple route and make the arguments be a named keyword list instead?

def main(opts) do
  base = opts[:base] || Base.new()
  name = opts[:name] || raise SomeMissingArgumentException(...)
  list = opts[:list] || raise SomeMissingArgumentException(...)
  ...
end

And if you really want first/second helper functions then just make them like:

def first(opts), do: main([name: "first"] ++ opts)
def second(opts), do: main([name: "second"] ++ opts)

And if you are going to have a LOT of those helpers (really don’t do this unless there are a lot):

for name <- [:first, :second, :whatever, :others, :here] do
  def unquote(name)(opts), do: main([name: unquote(to_string(name))] ++ opts)
end

The main above could just even be a bouncer method call to the other main ‘main’ call in whatever library too.

1 Like

Why not just go the simple route and make the arguments by a named keyword list instead?

Thank you for reply! I was thinking that, but that would break the rules of original API that I am writing wrapper for as I want to keep the same form.

And if you are going to have a LOT of those helpers (really don’t do this unless there are a lot):

for name <- [:first, :second, :whatever, :others, :here] do
  def unquote(name)(opts), do: main([name: unquote(to_string(name))] ++ opts)
end

Yeah, there are a lot of these 100+. So I was thinking doing something like this:

helper first({base, :optional, :base}, {x, :required, :list}, {y, :optional, :single})

Or just write out all function heads and generate bodies for them. This way if someone were to open the code it would be easier to understand what arguments each function would take.

1 Like

I’m curious about what you’re trying to accomplish, as some of the restrictions you describe needing to operate within feel a little fishy to me.

With no knowledge of your domain–why wrap the API if you are not changing how it is accessed? If you’re making no practical improvements to its structure or offering more-idiomatic paraphrasing of input arguments, why wouldn’t users just access it as-is?

This further re-enforces my suspicion that you might not be doing enough unique work in these functions to merit the layer of indirection–surely they should be consulting the documentation of your API wrapper if they saw any benefit in using it in the first place, if they must parse and interpret the original documentation they might as well make the calls themselves too.

If you are worried about discoverablity and readability of your library’s functions, remember that every function will appear in your modules exactly as if you’d written them out by hand, and the Phoenix example above even shows you how you might go about describing those functions such that your exdoc generated documentation can elaborate on their arguments.

Those docs should be able to clairfy any questions about using your library itself, and the code only needs to be readable to those updating or extending the surface area of the API that you have chosen to wrap. If that has to be done very often, the API you are wrapping might be too unstable to be worth wrapping.

In either case I think you’re on the right track pursing the Phoenix model here: if you encode the information the functions are generated from in a simple data structure that you enumerate, unless radically new functionality is needed contributors only need to touch to the data structure, not the code.

The best way to keep the code simple is to pass each item you are enumerating into a plain old function, rather than doing the work in the inner loop, so that it can be understood and tested separately from the surrounding metaprogramming.


If you are still struggling with the readability of either the data structure, code, metaprogramming, or documentation in your end result, post more details here! I’ve grappled with this before a few times and might be able to offer more constructive guidance on what’s ailing you with a specific example of what feels ugly to you.

1 Like