Accessing/updating Struct error during Enum.map

I have a struct define as follows

defmodule Employee do
 defstruct [:fname, :lname, :id, salary: 0, job: "none"]

then I made a list, consisting of Employee structs and added one in there.
At some point I want to search the list for employee based on id number, this part also I’ve gotten to work.
But then I got a function promote, where I pick an employee based on id and then change job: field. This is where im running into problem. I want function return updated list of employees so i do Enum.map in the end and try to update job in there. Somethings wrong and Im not sure how to accomplish this?
in the following code

def promote(list) do
        empId = IO.gets("Enter employee ID to promote: ")
        empIdC = String.trim(empId)
        getEmp = Enum.filter(list, fn x -> x.id == String.to_integer(empIdC) end) |> List.first()
        IO.inspect(getEmp)
        if getEmp != nil do
            cond do
                getEmp.job == "none" -> Enum.map(list, fn x -> if x.id == String.to_integer(empIdC) do x.job = "coder" end end)
            end
        end
    end

When I inspect getEmp I see it has correct Struct inside

%Employee{fname: "m", id: 1, job: "none", lname: "a", salary: 0}

job is “none”, I want to promote it to “coder” and return new list

Error im getting with this code

** (CompileError) kt6.exs:71: cannot invoke remote function x.job/0 inside a match
    (stdlib 3.8) lists.erl:1354: :lists.mapfoldl/3
    (stdlib 3.8) lists.erl:1355: :lists.mapfoldl/3
    (elixir 1.10.4) expanding macro: Kernel.if/2

This is not valid elixir. If you want to update the map use %{x | job: "coder"}. Also you should be aware that if without an else will return nil, so your function will return nil if no employee was found.

But more generally this is not really ideomatic in the big picture as well. If you select employees by id then it makes more sense to store them in a map with the employee id as a key, which makes selecting employees quite a bit simpler.

def promote(map) do
  emp_id = 
    "Enter employee ID to promote: "
    |> IO.gets() 
    |> String.trim()
    |> String.to_integer()

  if Map.has_key?(map, emp_id) do
    Map.update!(map, emp_id, fn current -> %{current | job: "coder"} end)
  else
    map
  end
end
2 Likes

Thank you! I got a better understanding now.

Since originally this function was using list here is a solution for such use case:

defmodule Example do
  def promote(list) when is_list(list) do
    "Enter employee ID to promote: "
    |> IO.gets() 
    |> String.trim()
    |> String.to_integer()
    |> promote(list)
  end

  defp promote(id, [%{id: id} = head | tail]), do: [%{head | job: "coder"} | tail]
  defp promote(id, [head | tail]), do: [head | promote(id, tail)]
end

From iex shell:

iex> defmodule Employee do
...>   defstruct [:fname, :lname, :id, salary: 0, job: "none"]
...> end
{:module, Employee,
 <<70, 79, 82, 49, 0, 0, 7, 8, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 197, 0,
   0, 0, 19, 15, 69, 108, 105, 120, 105, 114, 46, 69, 109, 112, 108, 111, 121,
   101, 101, 8, 95, 95, 105, 110, 102, 111, ...>>,
 %Employee{fname: nil, id: nil, job: "none", lname: nil, salary: 0}}

iex> defmodule Example do
...>   def promote(list) when is_list(list) do
...>     "Enter employee ID to promote: "
...>     |> IO.gets() 
...>     |> String.trim()
...>     |> String.to_integer()
...>     |> promote(list)
...>   end
...> 
...>   defp promote(id, [%{id: id} = head | tail]), do: [%{head | job: "coder"} | tail]
...>   defp promote(id, [head | tail]), do: [head | promote(id, tail)]
...> end
{:module, Example,
 <<70, 79, 82, 49, 0, 0, 6, 200, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 200,
   0, 0, 0, 21, 14, 69, 108, 105, 120, 105, 114, 46, 69, 120, 97, 109, 112, 108,
   101, 8, 95, 95, 105, 110, 102, 111, 95, ...>>, {:promote, 2}}

iex> list = [%Employee{id: 1}, %Employee{id: 2}, %Employee{id: 3}]                     
[
  %Employee{fname: nil, id: 1, job: "none", lname: nil, salary: 0},
  %Employee{fname: nil, id: 2, job: "none", lname: nil, salary: 0},
  %Employee{fname: nil, id: 3, job: "none", lname: nil, salary: 0}
]

iex> Example.promote(list) 
Enter employee ID to promote: 2
[
  %Employee{fname: nil, id: 1, job: "none", lname: nil, salary: 0},
  %Employee{fname: nil, id: 2, job: "coder", lname: nil, salary: 0},
  %Employee{fname: nil, id: 3, job: "none", lname: nil, salary: 0}
]

In case you want to pattern match on non unique field (multiple updates) you need to change first promote/2 function clause by calling promote/2 function on tail, for example:

defp promote(job, [%{job: job} = head | tail]) do
  [%{head | job: "coder"} | promote(job, tail)]
end

Helpful resource: