JKWA

JKWA

Author of Advanced Functional Programming with Elixir

I didn’t have room for this in my book, Advanced Functional Programming with Elixir , but I still thought it was worth sharing on my blog.

Showing Posts 14 to 5

JKWA

JKWA OP

Author of Advanced Functional Programming with Elixir

We’re probably just interpreting the term “polymorphism” a bit differently. I’m using the Strachey definition, where the runtime selects behavior based on the type of the input. Under that (admittedly narrow) lens, behaviours don’t quite fit, since the caller selects the module explicitly. But in the end, it’s probably just semantics.

christhekeele

christhekeele

I would say protocols are for datatype polymorphism, often to get similar behaviour from different input data. Behaviours are for functional polymorphism, often to get different implementation details from similar contracts. Both can solve similar problems but have their own idiomatic wood grains to cut with or against.

Both involve functions accepting dynamic input that changes how the program executes: just as a function that accepts dynamic data may invoke a protocol to get different results, a function that accepts a dynamic module may invoke a behaviour to get different results.

Generally, unless we are doing supervision stuff/adapter work, our applications want to act similarly on different data more than we want them to act differently on similar interfaces, which is more of a library concern. Additionally, the type system and dialyzer work well enough with dynamic data types but dynamic modules is currently more of a black box for editor experiences. So I’d say the average application has more reason to be concerned with protocols than behaviours in practice, despite both mechanisms being open for extension in some sense to application developers.

But I would disagree with the claim that behaviours are not a mechanism for polymorphism; just a mechanism with different and less-common aims. I think my first paragraph there is a half-remembered direct quote from José, I’ll see if I can find the source. Some keynote maybe?

sodapopcan

sodapopcan

I do find protocols make a lot more sense for extending modules you don’t own. Of course this isn’t always the case with Elixir’s Enum being a prime example. Of course when you own the module you can just add all the function heads yourself and do essentially the same thing as a protocol, but I do find protocols pretty clean. It’s also quite a nice bonus that you get MyProtocol.t() for free :slightly_smiling_face:

dimitarvp

dimitarvp

Well behaviors are decoupling too, and are a touch more explicit. And they too are a contract.

That being said, these days I’m not as fussy about such things. If somebody from my team strongly prefers protocols then I’m forced to admit that, as a person without a strong opinion on the matter, there’s no reason for me to resist it.

sodapopcan

sodapopcan

Thanks for your reply!

Ruby’s “concern” concept is no more than a wrapper around mixins/traits, so it’s not really a Ruby specific thing but a general OO one. I guess I’ve never felt super confident of how to tackle things in Ecto that I would have used mixins/traits for in OO. Protocols always seemed the best fit. It essentially creates the classic “basic” difference between method and function calls:

resource.create_note(attrs)

and

create_note(resource, attrs)

Yes that’s really all it is. My thing is actually really an even more general abstraction around dynamically resolving an Ecto association. We have a bunch of these things and it ends up with a bunch of copy/paste code or being implemented slightly differently, so to me learning a tiny bit of reflection once is better than the inevitable inconsistencies that come about. YMMV, of course.

JKWA

JKWA OP

Author of Advanced Functional Programming with Elixir

Oh, interesting. You’re seeing magic where I’m seeing decoupling. I suppose it comes down to perspective.

dimitarvp

dimitarvp

That’s basically why I don’t want to use protocols and I am resisting them in work places. To me they are kind of implicit. A behaviour is an explicit function argument. I am tired of magic.

JKWA

JKWA OP

Author of Advanced Functional Programming with Elixir

I admit, I’m having a bit of trouble giving helpful advice on this one.

Your goal, as I understand it, is to reduce Ecto boilerplate, and you’re also, in a sense, extending Ecto’s macro system. Within your project, I don’t see a problem, except that it introduces a bit of indirection that you’ll need to make sure your colleagues understand.

From the perspective of protocols, what hung me up is that I was looking for the polymorphism problem you were solving, but in the end, I think you’re using the protocol more as a contract.

Is this generalizable? I see some tight coupling to your domain, so my answer would be probably not, but I might be misunderstanding.

Is there another way to solve it? Sure. But you’re approaching it from a Ruby context, and I don’t have enough Ruby experience to say whether there’s a better alternative through that lens.

JKWA

JKWA OP

Author of Advanced Functional Programming with Elixir

Yes, we’re both solving the same problem: defining a default comparison for a type.

Here’s how I might implement the homogeneous sort:


def sort(list, ord \\ Funx.Ord) when is_list(list) do

Enum.sort(list, Ord.Utils.comparator(ord))

end

This lets the caller sort a list using the domain’s default ordering or pass in a different Ord when needed. It supports cases where the same type needs multiple comparison strategies. It also allows Ord composition, making it easy to build more complex ordering logic.

sodapopcan

sodapopcan

I’ve found myself deriving protocols a lot to sort of mimick Rails’ “concerns” (but in a functional way) and I’m wondering if it is the optimal way.

This is a simple example that for associating a note with any other record. I do add a direct fkey to the notes table for everything it can be assoicated with (avoiding the noteable_type and noteable_id pattern from other frameworks) but otherwise this mainly DRYs up having to mention these anywhere else (pattern matching to dispatch and typespecs mainly).

Here’s a stripped-down example:

defprotocol MyApp.Notes.Noteable do
  def assoc_key(schema)

  @impl true
  defmacro __deriving__(module, opts) do
    quote location: :keep do
      defimpl MyApp.Notes.Noteable, for: unquote(module) do
        def assoc_key(schema) do
          get_assoc_meta(MyApp.Notes.Note, schema).owner_key
        end

        # Returns an Ecto Relation struct
        defp get_assoc_meta(note, %noteable{}) do
          :associations
          |> note.__schema__()
          |> Enum.map(&note.__schema__(:association, &1))
          |> Enum.find(&(&1.related == noteable))
        end
      end
    end
  end
end
defmodule MyApp.Notes.Note do
  use MyApp.Schema

  schema "notes" do
    belongs_to :order, MyApp.Orders.Order

    field :body, :string
  end

  def create_changeset(noteable, attrs) do
    assoc_field = Noteable.assoc_field(noteable)

    %__MODULE__{}
    |> changeset(attrs)
    |> put_assoc(assoc_field, noteable)
  end

  # ...
end
defmodule MyApp.Orders.Order do
  use MyApp.Schema

  @derive MyApp.Notes.Noteable

  # ...
end

I wanted to key the sample small but there is also a assoc_key on the protocol which allow finding a note so we do:

order = Orders.get!(1)
notes = Notes.list_notes(order)

Again, I’ve been wondering about the general utility of this. We have a few of these things that all operate similarly so the abstraction feels worth it, but I’ve been wondering if I’m overlooking a simpler way (that is, outside of not using any abstraction at all).

Another I’ve seen stuff like this done in the past would be using macros to add list_notes to all contexts that have notes. I’ve found this can cause some nasty compile time deps and still requires setting up the schema stuff.

Anyway, I hope this is on topic :grin:

Where Next? Top

Trending in Blog Posts Top

bartblast
Hey folks, I just published a post about Hologram’s funding and where the project goes next - the short version: Curiosum as Main Spons...
New
pckrishnadas88
Hey everyone! :waving_hand: I’ve published Part 7 of the Building Distributed Systems in Elixir series, where we build core distributed ...
New
zorn
An educational side project in Elixir, Phoenix, and Tauri. I share what I learned while wiring Automerge into the BEAM, including how I s...
New
mudasobwa
So, instead of wasting my afternoon arguing with anonymous handles on X, I turned to my trusty, soulless assistant and said: “Listen, ple...
New
nathanl
Process labels are useful for visualization and debugging. Here’s why you should use them.
New
abreujp
New article: Elixir Project Structure — From mix new to a Growing Codebase I’ve published a new article in my Elixir learning series on d...
New
mudasobwa
Somewhere, right now, a senior engineer is on the verge of a nervous breakdown because his company will not let him switch from Claude to...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
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
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
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
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
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews