Accessing list element with put_in for lists of structs

Let’s say there is a list of structs as a field of a parent struct. Due to the nested nature of the list of maps in the struct, I have been using put_in to insert a new value into the child struct. However, I find myself having to get the index of the child struct in the list and then using that index to access the child struct in the nested list. Is there a better way of doing this? I have searched online and haven’t found much useful info.

In my use case (shown below), the parent struct is a game and the list contains player structs. I am trying to update the player in the list to be the opposite of their current mute boolean.

defmodule Game do
  defstruct id: nil, name: nil, password: nil, connected_users: [], messages: []
end

defmodule Player do
  defstruct id: nil, muted?: false
end

def mute(%{connected_players: connected_players} = game, player) do
    index = Enum.find(connected_players, &(player == &1.id)
    game
    |> put_in([:connected_players, Access.at!(index), :muted?], not player.muted?)
  end

Hi @collectivelysuperior, welcome to the forum!

I think you can do try something like this:

def mute(%{connected_players: connected_players} = game, %{id: player_id}) do
  players = Enum.map(connected_players, fn 
    {id: ^player_id, muted?: false} = player -> %{player | muted?: true}
    player -> player
end

%{game | connected_players: players}
3 Likes

I think finding a player by id is quite frequent. For example, when a player joins the game, you also want to make sure you won’t add him/her twice if already in the room.

So I often end up with having the connected_players list as a map, whose keys are ids. Would that be easier for you?

%Game{connected_players: %{"foo" => %Player{id: "foo", ...}}}
1 Like

If you want to keep using put_in, you could do it like this (haven’t tested it, but should be fine):

def mute(%Game{} = game, %Player{id: id} = player) do
  put_in(game, [
     Access.key(:connected_players), 
     Access.filter(&match?(%{id: ^id}, &1)), 
     Access.key(:muted?)
  ], not player.muted?)
end
1 Like

Three different ideas but all very valid. Thank you all!! I like qhwa’s idea of having the connected players a map, which would make accessing the players easier… If I stick with the list, I think the solution by @thiagomajesk is elegant and maintainable, if I ever need to change other fields in the player struct as well. (I didn’t know you could do multiple pattern matches in an anonymous function, cool!!)