基于另一个嵌套的Map迭代更新Map

时间:2017-06-04 22:32:53

标签: elixir

Elixir的不变性真的让我大吃一惊,使语言难以使用。我需要迭代嵌套映射并简单地根据迭代更新一些计数,但是Enum.reduce只会让我感到困难。说我有:

defmodule Predictor do

  def past_matches() do
    [
      team1: %{team2: %{f: 0, a: 1},  team3: %{f: 1, a: 3}},
      team2: %{team1: %{f: 3, a: 0},  team3: %{f: 2, a: 0}},                    
      team3: %{team1: %{f: 1, a: 0},  team2: %{f: 0, a: 1}},
    ]
  end


  def init_indexes(matches) do
    local = Enum.reduce matches, %{}, fn({team, _scores}, acc) ->
      Map.put acc, team, %{f: 0, a: 0, n_games: 0}
    end

    Enum.each matches, fn({team, scores}) ->
      Enum.each scores, fn({vteam, %{f: ff, a: aa}}) ->
        %{f: fi, a: ai, n_games: ni} = local[team]
        put_in(local[team], %{f: fi+ff, a: ai+aa, n_games: ni+1})
      end
    end

    local
  end

  def run() do
    local = past_matches() |> init_indexes()
  end
end

我需要local加上fan_games

local = %{
    team1: %{f: 1, a: 4, n_games: 2}
    ...
}

run()结束时,地图local显示全部为0且没有更新值。

1 个答案:

答案 0 :(得分:3)

我可以看到一些让你绊倒的事情:

  • Enum.each/2会将一个函数应用于列表中的每个项目,但它不会以任何方式累积结果或修改原始列表。我上次使用Enum.each时无法记住 - 它有它的位置,但它很少见。

  • 这意味着您正在调用local[team]的地方实际上并未更新任何值,因为该输出未传递给其他任何内容。它基本上只是将这些变化发送到以太网中。

  • 解决方案:管道!我保证|>管道操作员会改变你的生活。如果你来自OO背景,那么一开始有点令人难以置信,但坚持下去并且我保证你永远不会想要回去。思考的转变帮助我解决了这个问题,首先是尽可能少地使用匿名函数。它帮助我适应了不变性的概念,因为它迫使我思考每个函数实际需要的值,以及如何将这些值传递给每个函数。

在这里,我尝试使用更加以管道为中心的方法重写您的模块 - 希望它有所帮助。它在IEx中运行时会产生预期的结果。如果您有任何问题,我很乐意澄清任何事情。

defmodule Predictor do

  @past_matches [
    team1: %{
      team2: %{f: 0, a: 1},
      team3: %{f: 1, a: 3}
    },
    team2: %{
      team1: %{f: 3, a: 0},
      team3: %{f: 2, a: 0}
    },
    team3: %{
      team1: %{f: 1, a: 0},
      team2: %{f: 0, a: 1}
    }
  ]

  # see note 1    
  @baseline %{f: 0, a: 0, n_games: 0}

  def run(past_matches \\ @past_matches) do
    past_matches
    |> Enum.map(&build_histories/1)
    |> Enum.into(%{}) 
    # see note 2
  end

  def build_histories({team, scores}) do
    history = Enum.reduce(scores, @baseline, &build_history/2)
    {team, history}
  end

  def build_history({_vteam, vresults}, acc) do
    # see note 3
    %{acc | f: acc.f + vresults.f,
            a: acc.a + vresults.a,
            n_games: acc.n_games + 1}
  end
end

(1) since the baseline is the same for every team, you can 
    set it as a module attribute -- basically like setting a global 
    (immutable) variable that you can use as a starting point for a new 
    value. Another option would be to create a %BaseLine{} struct that 
    has default values.

(2) you could also use `Enum.reduce/2` here instead, but this does 
    effectively the same thing -- the output of the `Enum.map/1` 
    call is a list of {atom, _val} which is interpreted as a Keyword 
    list; calling `Enum.into(%{}) turns a Keyword list into a map 
    (and vice versa with `Enum.into([])`).

(3) NB:  %{map | updated_key: updated_val} only works on maps or 
    structs where the key to be updated already exists -- it'll throw 
    an error if the key isn't found on the original map.