7rans

7rans

I implemented Access behavior for a struct today. Pseudo-code…

defmodule MyStruct do
  defstruct data: %{}
  @behaviour Access
   # ... implementation of Access behavior on `data` ...
end

Then I tried:

  a = %{x: 1, y: 2}
  s = %MyStruct{data: a}
  z = %MyStruct{ s | y: 3 }

And of course it did not work.

Sure would be awesome if it could be made to work somehow though.

Showing Posts 1 to 10

zachallaun

zachallaun

You generally shouldn’t need to implement Access. For instance, in your example, you can already use the put_in/etc. helpers for ergonomic updates:

a = %{x: 1, y: 2}
s = %MyStruct{data: a}

put_in(s.data[:y], 3)
# %MyStruct{data: %{x: 1, y: 3}}
cmo

cmo

Why would you want that to work?

7rans

7rans OP

It does seem odd at first glance, I realize. The reason has to do with changesets. I have a function that updates a structure (e.g. some fields are calculations based on other fields). Which is fine, but when working with a web form the data is stored as a changeset. To perform those same updates I either have to re-implement the function for a changeset (using get_field, etc) or I have to apply the changes, take the resulting structure and run it through my function, and then make a new changeset.

So it occurred to me that I could wrap the changeset in an special module using Access behavior and use bracket notation (instead of dot notation) in my original function and then my original function can take a struct or a changeset. Mostly it would work. But it’s not a perfect polymorphism. Some things won’t work like the update shorthand.

This is one area where I think OOP in general, and Ruby in particular, really shines. It’s quite easy there to create an interface wrapper that would allow something like this to work. As it stands in Elixir, I don’t think I have much choice but to implement my function twice, once for the structure and another for the changeset.

D4no0

D4no0

It is absolutely not clear what you want to do, can you show a extensive example that involves ecto changesets?

Or maybe the problem could be reformulated, it is often the case that you will try to do things the way you were used to in other languages.

7rans

7rans OP

Sure. Here is a very simplified bit of code to demonstrate.

defmodule Foo
  use    Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :a,  :integer,  default: 0
    field :b,  :integer,  default: 0
    # calculated field
    field :x,  :integer,  default: 0
  end

  def changeset(%__MODULE__{} = foo, params \\ %{}) do
    foo
    |> cast(params, [:a, :b, :x])
  end

  def calculate(%__MODULE__{} = foo) do
    %{ foo | x: foo[:a] + foo[:b] }
  end
end

So, when working with LiveView I create a changeset (Foo.changeset(%Foo{}) pass that to to_form and it gets assigned to the socket… standard stuff. When the form on the web page changes, say via def handle_event("change", %{"foo" => changes}, socket) I use the changes to get a changeset. Foo.changeset(%Foo{}, changes). Now I need to run it through calculate to update x and send it back to the webpage. But calculate takes a %Foo{} not a changeset of it.

So what to do? One way to to write another calculate function that can take a changeset of Foo. Something like…

  def calculate(%Ecto.Changeset{} = cs_foo) do
    a = get_field(cs_foo, :a)
    b = get_field(cs_foo, :b)
    cs_foo |>
    put_change(:x, a + b)
  end

The downside of course if lack of DRY – I am implementing the same functionality twice.

The alternative is to apply the changeset, call calculate and make a new changeset.

  cs_foo = Foo.changeset(%Foo{}, changes)
  case  apply_action(cs_foo, :update) do
    {:ok, data} ->
      new_foo = Foo.calculate(data)
      changeset = Foo.changeset(new_foo)
      {:noreply, socket |> assign(:form, to_form(changeset))}
    {:error, changeset} ->
      {:noreply, socket |> assign(:form, to_form(changeset))}
  end

This works, but it applies an update I am not necessarily ready to apply (certainly not to the database) and it seems a rather “long way round” just to get back to an updated changeset.

So anyhow, I hope that clarifies things. My thought was maybe I could wrap that changeset in an Access behavior so I can pass it to the same calculate function that the struct itself uses. That’s where the idea of this thread spawned.

(Note I actually was able to implement this Access behavior, at least in part, but as I point out it is an imperfect polymorphism). Here is the code thus far:

defmodule Ecto.Accessor do
  defstruct changeset: %{}

  @behaviour Access

  def fetch(accessor, key) do
    changeset = accessor.changeset
    case Ecto.Changeset.fetch_field(changeset, key) do
      {_from, value} -> {:ok, value}
      :error         -> :error
    end
  end

  def get_and_update(accessor, key, fun) when is_function(fun, 1) do
    changeset = accessor.changeset
    current = 
      case Ecto.Changeset.fetch_field(changeset, key) do
        {_from, value} -> value
        :error         -> nil
      end

    case fun.(current) do
      {get, update} -> 
        # Hmmm.... should we cast? If so, how?
        {get, Ecto.Changeset.put_change(changeset, key, update)}
      :pop  -> 
        # doesn't delete the field, just deletes any pending change to it's value
        {current, Ecto.Changeset.delete_change(changeset, key)}
      other ->
        raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
    end      
  end

  def pop(accessor, key, default \\ nil) do
    changeset = accessor.changeset
    case Ecto.Changeset.fetch_field(changeset, key) do
      {_from, value} -> 
        {value, Ecto.Changeset.delete_change(changeset, key)}
      :error ->
        {default, changeset}
    end
  end

end
LostKobrakai

LostKobrakai

Why make a new changeset though? You could change the changeset you already have to include the change for your freshly calculated x.

sodapopcan

sodapopcan

Why do you need both versions of the function? If calculate is changing part of %Foo{} that’s exactly what change-sets are for. They should be the sole interface for change of Ecto-backed structs.

D4no0

D4no0

What kind of functionality you are implementing twice? You literally have a function receiving a changeset that adds an additional change.

I really hope this is not a production project, if it is, please stop.

As other mentioned above, a changeset is literally a list of changes you apply to your data. You can achieve what you want easily by:

def changeset(%__MODULE__{} = foo, params \\ %{}) do
    foo
    # x is not casted as we do not receive it as an input parameter
    |> cast(params, [:a, :b])
    |> validate_required([:a, :b])
    |> calculate()
  end

defp calculate(changeset) do
    case changeset do
       %Ecto.Changeset{valid?: true} -> 
          a = get_field(changeset, :a)
          b = get_field(changeset, :b)
          put_change(changeset, :x, a + b)
       _other ->
         changeset
    end
  end

If you need different behaviors based on where you use the changeset (for example a lot of times you might have forms where full validation doesn’t make sense), you can literally define different functions that will generate and validate your changeset differently.

7rans

7rans OP

I really hope this is not a production project, if it is, please stop.

Not sure it’s all that bad. I’m just routing the changeset gets and puts I would otherwise use through an Access behavior. But as I said, it’s probably not quite polymorphic enough to really make it worth it.

You can achieve what you want easily by:

I actually tried putting calculate in the changeset function just as you suggest. Seemed like a great idea at the time, but I ran into an issue because in my actual case the calculate function also needs some external “calibration” parameters, which need to be passed in. I considered adding a third parameter to changeset() but I felt that was mucking up the typical interface for changeset, so it felt off to me. (Maybe I could use a special entry to params though, I didn’t try that.)

It still (could) lead to me implementing calculate twice – there shouldn’t be a need to create a changeset if I am just creating the struct programmatically. But I suppose I could create a changeset then too.

Funny thing is I had just started to think I should apply the changes and work with struct directly, but now these comments have me thinking the opposite.

I think that supports my overall point though… It would be nice if we didn’t have to implement it one way or the other – some way to pass in the struct or the changeset and the same code could work on it either way.

D4no0

D4no0

You are literally trying to design a class like code, that achieves nothing but introduces complexity to the reader without any reason.

Creating the struct programmatically will skip all validation, this defeats the whole propose of using ecto schemas in the first place.

As I mentioned above, changesets are extremely versatile, you can easily write something like this:

embedded_schema do
    field :a,  :integer,  default: 0
    field :b,  :integer,  default: 0
    # calculated field
    field :x,  :integer,  default: 0
    # external field
    field :magic, :integer
  end

def magic_changeset(%__MODULE__{} = foo, params \\ %{}) do
    foo
    |> cast(params, [:a, :b, :magic])
    |> validate_required([:a, :b, :magic])
    |> calculate()
  end

defp calculate(changeset) do
    case changeset do
       %Ecto.Changeset{valid?: true} -> 
          magic = get_field(changeset, :magic)
          put_change(changeset, :x, magic + 3)
       _other ->
         changeset
    end
  end

If you don’t want the field in the final struct, you can pass it as an argument to the calculate function (however, you lose the benefit of validation if that’s important for you).

Where Next? Top

Trending in Proposals: Ideas Top

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