tomr

tomr

Hey elixirists,

another of my questions regarding piping:

The following code works perfectly well for what I want to achieve, but I don’t like the reuse of the var - it looks like I want to use pipes. I have not been able to make it work as I need to access the piped value in between. How can I improve the code?

def show(conn, %{"id" => id}) do
    project = Clients.get_project!(id)
    project = Map.put_new(project, :budget, total_budget(project.budget_items))
    project = Map.put_new(project, :budget_used, budget_used(project.work_items))
    project = Map.put_new(project, :budget_remaining, project.budget - project.budget_used)

    changeset = Clients.change_budget_item(%BudgetItem{project_id: project.id})
    render(conn, "show.html", project: project, changeset: changeset)
  end

Thanks :slight_smile:

Showing Posts 1 to 10

code-shoily

code-shoily

Look up Map.merge/3. Probably something like:

project = Map.merge(project, %{
  budger: total_budget(project.budger_items),
  budget_used: budget_used(project.work_items),
  budget_remaining: project.budget - project.budget_used
}, fn _, existing, _ -> existing end)

Also maybe extract it out as a separate function (get_or_add_budgets ?) in Clients context or something?

ericgray

ericgray

Instead of reusing the variable you can take Map.put and wrap it into three separate functions. Each function
will update the struct. Not sure what your struct looks like so I’ll just use %Project{}

def put_budget(%Project{budget_items: budget_items} = project) do
  Map.put(project, :budget, total_budget(budget_items))
end

def put_budget_used(%Project{work_items: work_items} = project) do
  Map.put(project, :budget_used, budget_used(work_items))
end

def put_budget_remaining(%Project{budget: budget, budget_used: budget_used} = project) do
  budget_remaining = budget - budget_used
  Map.put(project, :budget_remaining, budget_remaining)
end

Now you can set up a pipeline for your struct.

def show(conn, %{"id" => id}) do
  project = Clients.get_project!(id)
  |> put_budget()
  |> put_budget_used()
  |> put_budget_remaining()

  changeset = Clients.change_budget_item(%BudgetItem{project_id: project.id})
  render(conn, "show.html", project: project, changeset: changeset)
end
axelson

axelson

Scenic Core Team

I agree that Map.merge would be good to use here, I just would like to point out that it will override any existing values (unlike Map.put_new). But I would probably restructure the code so that Map.merge would work well.

tomr

tomr OP

Thanks everyone, my current approach is now:

  def show(conn, %{"id" => id}) do
    project = Clients.get_project!(id)

    total   = total_budget(project.budget_items)
    used    = budget_used(project.work_items)

    project =
      project
      |> Map.put_new(:budget, total)
      |> Map.put_new(:budget_used, used)
      |> Map.put_new(:budget_remaining, total - used)

    changeset = Clients.change_budget_item(%BudgetItem{})
    render(conn, "show.html", project: project, changeset: changeset)
  end

I still quite like @ericgray s idea of splitting this up into separate functions. I think I would go this way.

srowley

srowley

I think here the function getting passed to Map.merge/3 makes sure there are no overrides.

Just judging from the code it seems odd to me that you wouldn’t want to override existing values (especially if project is a struct), but the OP says that works for them so I’ll take them at their word.

I also want to +1 the recommendation to put all of this in a function in a context module as opposed to in a controller, which is what appears to be happening.

axelson

axelson

Scenic Core Team

Ah, good point, you’re correct. I didn’t notice that it was Map.merge/3 being used instead of Map.merge/2.

srowley

srowley

For what it’s worth, I might do:

# In the Clients module
defp add_budget_data(project) do
  budget  = total_budget(project.budget_items)
  budget_used = budget_used(project.work_items)
  budget_remaining = total - used

  Map.merge(
    project, 
    %{budget: budget, budget_used: budget_used, budget_remaining: budget_remaining},
    fn _, existing, _ -> existing end
  )
end

def get_project_with_budget!(id) do
  id
  |> get_project!()
  |> add_budget_data()
end

# in the controller module
def show(conn, %{"id" => id}) do
  project = Clients.get_project_with_budget!(id)
  changeset = Clients.change_budget_item(%BudgetItem{project_id: project.id})
  render(conn, "show.html", project: project, changeset: changeset)
end
ericgray

ericgray

Map.merge/3 is nice succinct solution but I think he wishes to see how to use pipes to transform data. I’m sure he’ll revisit it once he gains more experience. It’s a little harder to understand what Map.merge does as opposed to Map.put

srowley

srowley

That’s fine. I think there is a practical reason not to create separate functions for each operation - you never want those functions to be called in isolation. You would never call just the function to update the budget and the budget used without also updating the budget remaining, because you would have values that don’t tie (i.e., budget - budget_used might not equal budget_remaining.)

So while normally I would say the degree to which you extract smaller functions from larger functions is ultimately a matter of personal style, in this case it is probably better not to make it possible to use any of these three update functions in isolation.

ericgray

ericgray

Good point you’d have to be careful not to call those functions in isolation. I’m not sure exactly what he’s trying to accomplish. Just gave an example of how to pipe it. Great input tho, something he should keep in mind.

Where Next? Top

Trending in Questions Top

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
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
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
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
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
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
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
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews