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 1 to 10

adamu

adamu

I didn’t have room for this in my book

This topic is explored in more depth in my book

These two statements contradict each other, perhaps the footer on the blog is generic and doesn’t apply in this case?

I think behaviours also deserve a mention along with protocols. My experience is that it’s quite rare to write a protocol, compared to a behaviour.

For example, Eq could also be implemented using a behaviour:

defmodule Eq do
  @callback eq?(a :: struct, b :: map) :: boolean

  def eq?(%type{} = a, b) do
    type.eq?(a, b)
  end
end

defmodule Cat do
  defstruct [:name, :microchip]

  @behaviour Eq

  def eq?(%Cat{microchip: a}, %{microchip: b}), do: a == b
end
iex(1)> Eq.eq?(%Cat{microchip: "foo"}, %{microchip: "foo"})
true
iex(2)> Eq.eq?(%Cat{microchip: "foo"}, %{microchip: "bar"})
false
krasenyp

krasenyp

I really don’t know why there’s this behaviour fetish in the community. Protocols are for polymorphism. You use the protocol more or less directly. With behaviours you fill holes by providing callbacks. There is a framework, like the GenServer, which calls the module which implements the callbacks.

LostKobrakai

LostKobrakai

I’m not sure I see the point in a polymorphic equal? in the first place. One of the usecases for (dynamic) polymorphism is that the interface can exist independently from the implementations and implementations can independently maintained. Comparing a to b is not a problem well suited to that if any of a or b are of arbitrary type, because both types need to interact to be compared, hence they’re no longer independent.

Elixir actually kinda has a polymophic option in that domain: Enum.sort can receive a module to do the comparison between items in order to sort elements of a list. It’s not officially a behaviour, but uses the single @callback compare(a, b) :: :lt | :eq | :gt. The big difference on that one – it’s not polymorphism on a per type level. Each passed module needs to be able to deal with all elements it gets thrown at for comparison and you won’t be able to later bring in a new struct and make it be handled differently.

JKWA

JKWA OP

Author of Advanced Functional Programming with Elixir

Good point on the contradictory phrasing, I trimmed these kinds of language-specific details from the book after some early feedback. The footer is just a standard note pointing readers toward the book if they want more depth, though it does not always apply perfectly.

I agree behaviours are important, but I would not say they implement polymorphism. They enforce a contract, but you still have to choose which module to call. To me, polymorphism means a function changes behavior based on the type of input, which is the protocol.

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:

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.

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.

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

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

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.

Where Next? Top

Trending in Blog Posts Top

videsnelson
The code in this post is Erlang, but it all applies to Elixir binaries just the same: how appends, match contexts and sub-binaries behave...
New
charlie.foret
eMudhra’s leaked CA keys reached my Phoenix app through 2 trust stores, and my first redeploy patched neither. How I fixed the OS bundle ...
New
T0ha666
A deploy killed my OTP processes but not the containers running my AI agents. That mismatch kept a kanban card “executing” for eighteen h...
New
bartblast
My first real blog post is up! Got five minutes? Come break some stuff. I put five demos in it, and they’re meant to be played with. Kill...
New
T0ha666
I run a kanban of AI coding agents, and one afternoon the board showed three tasks running while the profile page on the same screen said...
New
danschultzer
I wrote a post on how to implement a feature flag system and A/B testing in an Elixir app with no need for third-party services or librar...
New
videsnelson
Following up on Making `dns_erlang` fast: binary tricks for the BEAM, there’s our second post in our series on making erldns, an authorit...
New

Other Trending Topics Top

dominicletz
Hey there, I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first. ...
New
raa-ziq
Hi everyone :waving_hand: I wanted to share something I’ve been building — NowBlind, a random chat app designed to help people discover ...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
kip
Localize is the next generation localisation library for Elixir. Think of it as ex_cldr version 3.0. The first version will be released ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews