MMore

MMore

How to distribute function clauses over several files

Hello Hello,

I have a bunch of function clauses (means one function, but different clauses to match for different input parameters). This function is basically an API handler.

def handle_event_type(event_type = "car_started", _params) do
  # do sth.   
end

def handle_event_type(event_type = "car_stopped", _params) do
  # do sth. else 
end

I wanna structure and organise a little bit for better readability according to the different business use cases. So my idea was to put the different function clauses in one file per use case and import those files back into my central API handler file to consolidate everything again.

But I get the following error:

== Compilation error in file lib/project/controllers/api_controller.ex ==
** (CompileError) lib/project/controllers/api_controller.ex:90: imported Project.ApiHandler.handle_event_type/2 conflicts with local function
(elixir 1.11.4) src/elixir_locals.erl:94: :elixir_locals.“-ensure_no_import_conflict/3-lc$^0/1-0-”/2
(elixir 1.11.4) src/elixir_locals.erl:95: anonymous fn/3 in :elixir_locals.ensure_no_import_conflict/3
(stdlib 3.14.1) erl_eval.erl:680: :erl_eval.do_apply/6
(elixir 1.11.4) lib/kernel/parallel_compiler.ex:314: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

Any other idea to organise everything better?

Marked As Solved

Qqwy

Qqwy

TypeCheck Core Team

The error you get, is because when importing multiple modules that define a function with the same name,
the result is not “a single function” with the clauses combined.
Instead, the result is two functions, and ambiguity between them.

Multiple function clauses

def foo(1), do: "bar"
def foo(2), do: "baz"
def foo(_other), do: "qux"

are syntactic sugar for:

def foo(val) do
  case val do
    1 -> "bar"
    2 -> "baz"
    _other -> "qux"
  end
end

As soon as a module was compiled, all function clauses end up as case-statements, and the function is “finished”.
The only way to combine function clauses from multiple modules into the same function, is by exporting them at the AST (parsed-source-code) level (i.e. using macros), and combine these AST snippets.
However, this is almost always a bad idea as it will no longer be obvious where a particular event is handled. Instead, you’ll have to crawl through all files that contain some of the clauses. As such, you haven’t really gained anything by splitting the file this way: There is no separation of concerns between files.


But there is an alternative:

A relatively common approach I’ve seen used and used myself in the past, is to have the event_type be something “splittable” (either a list, or by e.g. pattern match on the front of the event type). For instance, by using / or : as “separator”, you might have events like customer:car:started, customer:car:stopped, billing:overdue, etc.
This then allows you to essentially create some event routing logic, spread out over multiple modules:

defmodule MainEventHandler do
  def handle_event(event_type, params) do
    dispatchable_event_type = String.split(event_type, ":")
    dispatch_event(dispatchable_event_type, params)
  end
  
  def dispatch_event(["customer" | rest], params), do: Customer.EventHandler.dispatch_event(rest, params)
  def dispatch_event(["billing" | rest], params), do: Billing.EventHandler.dispatch_event(rest, params)
  # etc.
end

defmodule Customer.EventHandler do
   def dispatch_event(["car" | rest], params), do: Customer.Car.EventHandler.dispatch_event(rest, params)
   # ... and add others here.
end

defmodule Customer.Car.EventHandler do
  def dispatch_event("started", params) do
    # ...
  end

  def dispatch_event("stopped", params) do
    # ...
  end
  # ... etc.
end

defmodule Billing.EventHandler do
  def dispatch_event(["overdue"], params) do
    # ...
  end
  # ... etc.
end

Also Liked

dimitarvp

dimitarvp

Generally: just don’t, you can’t achieve it the way you aimed at for the reasons @Qqwy explained excellently.

But if you really want to have some sort of routing, you might be better off just making one central router module which just delegates to a number of other modules.

Protocols I would advise against; you can make do without them for sure.

Finally, you can use behaviours and runtime hackery to dispatch to a compliant one but IMO that’s an overkill as well.

IMO just go for a router module that dispatches function calls to other modules. Standardize those modules somehow. Easiest way would be to just clump them up in one directory but if you really want to be able to find them at runtime, you can also make them implement a behaviour.

Where Next?

Popular in Questions Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

Other popular topics Top

sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
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
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
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement