How to create functions working for multiple models?

Coming from OO languages, the functional approach of Elixir is still a bit difficult for me. I’m now facing the following problem that I don’t know how to solve:

My application consists of multiple models (db tables) that have a couple of fields in common. For example, they have a field “order” that is used to have a custom ordering of the records in a list.

I have implemented two functions in model A two move down or up the position of the record in the list by swapping the value of the field order with the previous or next record in the list.

What is the best approach to “copy” those two functions to the other models as well? In an OO language I’d create a superclass and implement the functions there.

Coming from OO world, inheritance isn’t the only solution to this problem. If you think about separating your data structures (models) and your functions the solution might be easier to see.

If you have functions which operate on multiple models, perhaps you can move those functions to a separate module rather than keeping them inside the module that defines your data structure?

That was my idea as well. Here’s a snippet to illustrate where I’m struggling:

In the above mentioned function for the model “Menu”, I have the following code to swap the value of the field “order” of two records:
      |> Multi.update(:first_rec_temp, Menu.changeset(first_menu, %{order: 0}))  # needed to avoid unique index constraint
      |> Multi.update(:second_rec, Menu.changeset(second_menu, %{order: first_order}))
      |> Multi.update(:first_rec, Menu.changeset(first_menu, %{order: second_order}))
      |> Repo.transaction()

To make this work for different models, I would need to call the “changeset” function for a model passed as a parameter to that function. But how can I then call it?

Maybe you can “inject” your module?

@spec swap_orders_multi(Multi.y, tuple, tuple, module) :: Multi.t
def swap_orders_multi(multi, {first_menu, first_order}, {second_menu, second_order}, schema \\ Menu) do
  |> Multi.update(:first_rec_temp, schema.changeset(first_menu, %{order: 0}))  # needed to avoid unique index constraint
  |> Multi.update(:second_rec, schema.changeset(second_menu, %{order: first_order}))
  |> Multi.update(:first_rec, schema.changeset(first_menu, %{order: second_order}))

Or pass a closure calling the appropriate changeset function?

@spec swap_orders_multi(Multi.t, tuple, tuple, ((struct, map) -> Ecto.Changeset.t)) :: Multi.t
def swap_orders_multi(multi, {first_menu, first_order}, {second_menu, second_order}, changesetter) do
  |> Multi.update(:first_rec_temp, changesetter.(first_menu, %{order: 0}))  # needed to avoid unique index constraint
  |> Multi.update(:second_rec, changesetter.(second_menu, %{order: first_order}))
  |> Multi.update(:first_rec, changesetter.(first_menu, %{order: second_order}))

# usage
swap_orders_multi(multi, {first_menu, first_order}, {second_menu, second_order}, &Menu.changeset/2)

Off topic …

I think you can swap rows with a case statement in a single query.

1 Like

I suspect you’re wanting to achieve some kind of duck typing here?

If so, you might want to read up on Protocols - this is Elixir’s way of achieving polymorphism.
You would define a protocol which has the interface, for example Orderable.change_order or something. Then you simply define the implementation for that protocol in each of your models. Thats where you’d call the changeset for each Model respectively.

And then finally, within your pipeline, you just call the Orderable.change_order passing in the model you are operating on as the first argument.

I’m aware that I’m explaining this really badly - but if you replace the word Protocol with the word Interface, it might make sense to your OO mind.

@globalkeith: I’ve read about protocols before, but I don’t see the benefit for this use case as my goal is to avoid copying the code for the record swap. As you write, I’d have to write the implementation in each of the models.

@idi527: Passing the closure seems what would suit my needs best. But after thinking about your “off topic” comment using plain SQL for swapping the records, the easiest thing to do here is to just pass the table name as a parameter. Thanks for this hint!