Update shorthand for Access behavor

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.

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}}

Why would you want that to work?

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.

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.

1 Like

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

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

1 Like

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.

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.

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.

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).

1 Like

The way to achieve that is to have two function heads, one that matches on the struct and one that matches on the changeset. You can have the calculate logic in a separate private function and just extract the fields and put the result into whatever structure in each of the function heads.

And where are you going to get the other values for the calculation, if they’re not being passed as args? Passing required arguments to functions is not an antipattern :wink:

3 Likes

As @D4no0 said, it really sounds like you are trying to create class-like code. The problem with the code you showed us is that it just doubles down on why you think you need access. The real question is: “Why do you need a function that works on both in the first place?” The reason people are questioning you is because there shouldn’t be any reason to need to manually update a key of a schema’s struct, all changes should be done through changesets.

No such thing! Changeset constructors can look however you need to them to. The fact that cast_assoc will automagically look for a changeset/2 function is just a convenience. I won’t name names (@dimitarvp :sweat_smile:) but some people think this in-of-itself is a mistake and the :with option should always be used. Which is just a lot of words to try and re-enforce my point.

2 Likes

This and put_assoc will haunt me to the end of the days. Nowadays I avoid them altogether for my mental health :sob: .

I used to feel this way! I definitely came around to them. I usually just use put_assoc for tenant-like relationships, or really just anything I don’t want the user to have control over changing, otherwise I just cast the ids directly. And I came to love cast_assoc for many types of nested relationships, but not all! But ya, we don’t have to get into this in this thread :sweat_smile:

2 Likes

I think one thing is missing from this discussion and that‘s the notion of violating DRY by having ways to calculate the computed value from various sources of data. Instead of trying to avoid repetition by limiting the number of inputs (only struct or only changeset) you can also keep both inputs, but delegate the business logic in question to a shared helper.

def with_computed(%Ecto.Changeset{} = cs) do
  a = get_field(cs, :a)
  b = get_field(cs, :b)
  put_change(cs, :x, compute_x(a, b))
end

def with_computed(%Struct{} = struct) do
  %{struct | x: compute_x(struct.a, struct.b)}
end

defp compute_x(a, b), do: a + b
3 Likes

To be fair I believe that I what @cmo was saying.

1 Like

Ah yeah, seems like I missed that one.

1 Like

You are overthinking this, like a lot. As others have pointed out, have a private function that does the calculation and then only work with a Changeset that calls it. You don’t want the calculation being directly applied to the struct when you have Ecto in the picture; the Changeset is the perfect way of, a-hem, accumulating sets of changes before putting them in the DB.

Not sure where your analysis paralysis comes from but it’s likely that you are used to doing this in a very different way and trying to shoehorn it in Elixir. It can be a struggle to reshape some neural pathways, admittedly, but you should give our way a chance.

1 Like

Just to drive the point harder, I would say even if you aren’t going to persist you still want to change through a changeset!

@7rans, think of changesets as ActiveRecord’s validations along with its instance methods+callbacks (validations just being another form of callback, of course). What you’re proposing is roughly the equivalent of calling model_instance.instance_variable_set from a controller. It’s not an exact mental mapping but not too far off! So just as you have a public API for for assigning attributes in AR, changesets are Ecto’s version of that.

1 Like