fireproofsocks

fireproofsocks

This is more of a philosophical question hoping for some best-practices or practical suggestions to help write better code.

I’m working with behaviours and callbacks and it provides a sensible way to organize code into different implementations, e.g. modules that implement vendor-specific business rules.

Sometimes I want multiple implementations to refer to a shared function because it does something onerous or complicated. This makes sense from an OO perspective where you have an abstract base class and then children classes: the children classes can call methods on the parent classes, so they don’t have to re-write code.

In a functional language, however, I feel like it is a potential smell when the implementation calls shared functions. I would prefer that the implementations are NOT dependent on anything else: it would be cleaner if those modules were completely isolated.

What are ways to share/reuse functions that perform complex tasks?

  • Option 1: each implementation lists a dependency
  • Option 2: each implementation returns a more complex result and then the dispatching module can interpret the results and perform the more complex operations there.
  • Others?

Showing Posts 1 to 10

OvermindDL1

OvermindDL1

The whole ‘parent/child’ thing implies an inheritance hierarchy, although you can model that in functional languages it is usually poor form to do so, rather composition is what should be done.

Hmm, this is rather open ended, I’m not sure what “implementation” is in this context or “lists a dependency” is?

Unsure on implementation again, but dispatching can be done in quite a variety of ways from witnesses to protocols to far far more. It really depends on ‘what’ is being done.

It might be good to give precise examples so they can be converted into a more functional style that would be a good comparison. :slight_smile:

imetallica

imetallica

You can have an behaviour with a “standard” implementation, much in the same line on how GenServer, Behaviour, GenStage, works.

As an example: elixir/lib/elixir/lib/gen_server.ex at main · elixir-lang/elixir · GitHub

dgmcguire

dgmcguire

Sometimes I want multiple implementations to refer to a shared function because it does something onerous or complicated.

Simply calling that function to do work in the context of each module you want to use it would satisfy the need for re-use. Are you asking how you should organize shared functions? Because I’m also interested in how people choose to organize shared functions.

Personally I’m fine just having a Utils module or similar that encapsulates the often reused functions. Often reused functions to me implies a generic function and as such it seems fine to organize generic functions under a generic namespace.

fireproofsocks

fireproofsocks OP

To provide an example, consider the following:

  • An OrderHandling module that defines a @callback named do_something().
  • VendorOne module implements the do_something() callback.
  • VendorTwo module implements the do_something() callback.

Both VendorOne and VendorTwo modules do vendor-specific stuff, but they both wish to call a function named complex_operation(). So the question (and the reason for this post) is “where to put the complex_operation() function?”

Option 1: rewrite the complex_operation() function in each vendor’s module. This keeps VendorOne and VendorTwo highly modular but it violates DRY (don’t repeat yourself).

Option 2: put the complex_operation() function somewhere that both VendorOne and VendorTwo can access it, e.g. inside a Utils module. This follows DRY, but now VendorOne and VendorTwo have a “dependency” on the Utils module.

Now imagine that the VendorOne and VendorTwo modules were written and deployed as separate applications inside an umbrella app. If we want to develop these vendor apps separately (outside of the umbrella app), how can they share the complex_operation() code?

imetallica

imetallica

You may want something like this:

defmodule Shared.OrderHandling do
  @callback do_something(foo, bar)

  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      @behaviour Shared.OrderHandling

      def do_something(foo, bar), do: ... your default implementation here
      def complex_operation, do: ... your implementation here ...
      def i_can_also_override(bar), do: ... your implementation ...

      defoverridable i_can_also_override: 1, do_something: 2
    end
  end
end

Then in your AppOne application:

defmodule AppOne.VendorOne do
  use Shared.OrderHandling

  # Now you can override the implementation of i_can_also_override and do_something
  def do_something(foo, bar), do: ...
end

Same for VendorTwo.

peerreynders

peerreynders

As written one could make the argument that complex_operation() should be part of OrderHandling (you started out with subclasses using functionality from the abstract base class) and that the callback should be respecified as do_something(complex_operation_fun) so that the callback module can use the functionality provided by the behaviour module - in which case do_something simply becomes a higher order function.

OvermindDL1

OvermindDL1

All what I’d personally do, thus:

I’d put complex_operation/0 in the OrderHandling module or a specialty module depending on the transformations it should do.

No point, it doesn’t do anything vendor specific thus should go into a singular module. If you really really want it callable from those modules then just defdelegate it in.

I’d probably put it on OrderHandling itself if it does something complex related to Order Handling, otherwise in some other more specially named module, Utils is way too generically named so not that. No clue what’s bad about a dependency on another module as modules are just namespaced static functions (to use a C++/Java’ism).

Then just have the library that supplies their behaviours also supply that function. :slight_smile:
A dependency is only loaded once regardless of the amount of things depending on it, like object linkages in C++.

Eh I would not recommend that unless you really really want it implemented entirely internally, which is almost certainly poor form and harms maintainability.

fireproofsocks

fireproofsocks OP

Thanks for the response. Yes: I have tended to put things like the complex_operation() in the “parent”-ish class (OrderHandling in this case) for exactly the reason you articulated: because it is NOT specific to a vendor.

I guess what has been bothering me is that the VendorOne and VendorTwo implementations have a “dependency” of sorts on the OrderHandling module because each vendor implementation will specify @behaviour OrderHandling.

It may sound crazy, but I’m wondering if I should NOT explicitly list that @behaviour. In the case where I want multiple vendor integrations to exist as separate and independent apps, I don’t want them to have to know or care about the OrderHandling module. I could totally write and test the vendor specific code without ever concerning myself with the OrderHandling module.

How crazy would it be to loosely couple the dynamic dispatching in OrderHandling to the vendor-specific modules? I.e. do that WITHOUT a behaviour or @callbacks. Sure, everything would fall apart if your VendorThree module didn’t implement specific functions, but it could be developed and tested without ever referencing the OrderHandling module. The vast majority of the time, I’d say it’s a bad idea to forgo the benefits of a contact, but if there is a pressing need to isolate implementations into their own applications, then it might make sense to skip it.

This scenario reminds me of how Go implements interfaces implicitly (not explicitly). In Go, as long as your class contains the proper methods, it will be considered as a viable implementation of an interface. In this one use case, I can see a distinct benefit to that approach.

LostKobrakai

LostKobrakai

If you have a dependency – your vendor modules should be usable as callback module for OrderHandling – why are you trying so hard to hide that? What do you get by your vendor modules not knowing about OrderHandling if they need to implement it’s callback to work in the first place? If that’s not the intention then have VendorOne be a separate module all together and only have VendorOneOrderHandling be the callback module. It could even just delegate to VendorOne where possible and you’d have OrderHandling and VendorOne fully separated.

OvermindDL1

OvermindDL1

Hmm, then how else would you specify the API that they should follow? Otherwise I guess you can pray-and-hope that they follow identical interfaces so you can call them the same? :-/

But why wouldn’t you want to reference the OrderHandling module?

But again, why skip it depending on OrderHandling?

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews