What's proper elixir way to update an **ordered** list by key

Hi all

I am playing with a weather forecast LiveView (just a hobby project to learn elixir on something that looks realistic). The list of days I want to display is certainly ordered.

Fetching weather takes a HTTP call which can fail, so I display a list of day weather rectangles with just “Loading…” and update them asynchronously: when weather fetch result arrives, I update the weather info rectangle for the day.

def handle_event("fetch-weather-click", _, %{assigns: %{target_date: target_date}} = socket) do
  weather_elem1 = %{
      state: :loading,
      date: target_date,
      day_weather: nil  #real weather info will be sitting here
  }

  weatherRow = [
    weather_elem1
    ## and more-more-more elements here for the whole week
  ]
  socket = assign(socket,
     :weatherRow, weatherRow
  )

  send(self(), {:fetch_weather, weather_elem1.date})
# and some more similar sends for the other days in the list
    {:noreply, socket}
end

def handle_info({:fetch_weather, date}, socket) do
  # MyWeatherApi initiates an HTTP call and returns a parsed result
  {:ok, dayWeather} = MyWeatherApi.get_weather(date)
  weather_elem  = %{
   state: :known,
   date: date,
      day_weather: dayWeather
   }

  # now I need to iterate socket.weatherRow, find element with the date API provided
  # replace it's state (or whole element via List.replace_at) and assign it back
  
  socket = assign(socket,
    :weatherRow, weatherRow
  )
  {:noreply, socket}
end

Notice how I need to iterate the list to find an element to be modified, then I need to modify element or replace_at and return a fresh row to the socket. That is all doable and would probably be even natural e.g. in Java arrays, but given immutable lists in Elixir that sounds like a lot of data recreation and slow access by index.

What would be an elixir way for such “find in a list and update”?
Or am I approaching the whole thing wrong?
Maybe there are some other structures for it for such “ordered maps” (to access by key, but maintain an order when rendering)?

Seems that you need a LiveComponent per date, with a preload callback.

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html

Here you go:

defmodule Example do
  def generate_row(date \\ Date.utc_today()) do
    Enum.map(-3..3, &%{date: Date.add(date, &1), state: :loading})
  end

  def sample(row, date \\ Date.utc_today(), func \\ &%{&1 | state: :known})
  # if no items match finish job
  def sample([], _date, _func), do: []
  # if match apply func on element and keep rest of row as is
  def sample([%{date: date} = element | row], date, func), do: [func.(element) | row]
  # if no match keep element as is and call same function for rest of row
  def sample([element | row], date, func), do: [element | sample(row, date, func)]
end

iex> generated_row = Example.generate_row()
iex> Example.sample(generated_row)

In this example we are stopping iterating over list as soon as we find element. There is no need to find and work with index.

Also you can consider storing a map with date as key and the rest of weather_elem1 as value. Then all you need to do is to call Map.update!/3 passing a date and func to change anything you want to in said value.

Helpful resource:

  1. Date.add/2
  2. Date.utc_today/0
  3. Enum.map/2
1 Like

Wow, I didn’t realize you can match between the arguments as you match the date

def sample([%{date: date} = element | row], date, func)

Thank you, @Eiji !