I tried to implement this as a small exercise. I am not sure if it is perfectly idiomatic, but I did my best. Feedback is welcome, of course.
I put most of the logic into a plain (“pure”) module, so it’s easily testable and used Agent
only for the necessary (stateful) stuff.
I am not sure if it is desired behavior, but in your solution, if I try to increment the count of a non-present item, it will do nothing. In my solution, it will add it into the cart and set its count to 1. Also, in your solution, you’re not able to decrement the count from 1 to 0. I would assume that decrementing count from 1 would set it to 0 or remove it from the cart. But those are business logic details, that are easily adjustable.
Also, in my opinion, having (doc)tests for a seemingly simple algorithm like this is always helpful, not only to refactor it with confidence but also to serve as documentation. I don’t want to be captain obvious here, but since you asked for an idiomatic solution, I put it there.
defmodule Carts do
@doc """
## Examples:
# Incrementing item count in non-existing cart
# will do nothing and return the original carts
iex> Carts.increment(%{}, "cart_id", "item_id")
%{}
# Incrementing count of non-existing item
# will add it to a cart and set its count to 1
iex> Carts.increment(%{"cart_id" => %{}}, "cart_id", "item_id")
%{"cart_id" => %{"item_id" => 1}}
# Standard scenario
iex> Carts.increment(%{"cart_id" => %{"item_id" => 3}}, "cart_id", "item_id")
%{"cart_id" => %{"item_id" => 4}}
"""
def increment(carts, cart_id, item_id)
when is_map(carts) and is_binary(cart_id) and is_binary(item_id) do
case carts[cart_id] do
nil ->
carts
_cart ->
update_in(carts, [cart_id, item_id], fn
nil -> 1
count -> count + 1
end)
end
end
@doc """
## Examples:
# Decrementing item count in non-existing cart
# will do nothing and return the original carts
iex> Carts.decrement(%{}, "cart_id", "item_id")
%{}
# Decrementing count of non-existing item
# will do nothing and return the original carts
iex> Carts.decrement(%{"cart_id" => %{}}, "cart_id", "item_id")
%{"cart_id" => %{}}
# Decrementing count of existing item with count 0
# will do nothing and return the original carts
iex> Carts.decrement(%{"cart_id" => %{"item_id" => 0}}, "cart_id", "item_id")
%{"cart_id" => %{"item_id" => 0}}
# Standard scenario
iex> Carts.decrement(%{"cart_id" => %{"item_id" => 3}}, "cart_id", "item_id")
%{"cart_id" => %{"item_id" => 2}}
"""
def decrement(carts, cart_id, item_id)
when is_map(carts) and is_binary(cart_id) and is_binary(item_id) do
case carts[cart_id][item_id] do
nil -> carts
0 -> carts
_count -> update_in(carts, [cart_id, item_id], &(&1 - 1))
end
end
end
defmodule CartsAgent do
def increment(cart_id, item_id) do
Agent.update(__MODULE__, fn carts ->
Carts.increment(carts, cart_id, item_id)
end)
end
def decrement(cart_id, item_id) do
Agent.update(__MODULE__, fn carts ->
Carts.decrement(carts, cart_id, item_id)
end)
end
end