Elixir - 更新嵌套地图/JSON;没有错误,但没有更新

Elixir - updating a nested map / JSON; no errors, but not updating

我正在研究一些使用 Elixir 语言的专有 API。我对后者很陌生,所以请多多包涵。

我仍然在思考整个不变的概念,这对我来说真的很难。

defmodule Main.Game do
    def doRequest("test_loop", _args) do
        ballsJsonMap = Sys.Tuning.value("balls")
        timestep = 1;
        looper(timestep, 10, ballsJsonMap)
        %{:results => :ok}
    end

    def looper(timestep, loopsleft, ballsJsonMap) do 
        case loopsleft do
            0 ->
                :ok
            x ->
                step(timestep, ballsJsonMap)
                looper(timestep, x - 1, ballsJsonMap)
        end
    end

    def step(timestep, ballsJsonMap) do
        Enum.map(ballsJsonMap, fn({ball, prop}) -> 
           ballMap = ballsJsonMap[ball]
           newX = (ballMap["x"] * timestep) + prop["x"]
           newY = (ballMap["y"] * timestep) + prop["y"]
           newZ = (ballMap["z"] * timestep) + prop["z"]
           Sys.Log.debug("X: #{newX}, Y: #{newY}, Z: #{newZ}")
           put_in(ballsJsonMap, [ball, "x"], newX)
           put_in(ballsJsonMap, [ball, "y"], newY)
           put_in(ballsJsonMap, [ball, "z"], newZ)
       end)
       Sys.Log.debug("#{inspect ballsJsonMap}")
    end
end 

doRequest 函数中的 Sys.Tuning.value("balls") 负责从 JSON 文件中加载数据,如下所示:

{
    "cue": {"x":2.0, "y":0.0, "z": 2.0},
    "ball_1": {"x":5.0, "y":0.0, "z": 5.0},
    "ball_2": {"x":10.0, "y":0.0, "z": 5.0},
}

但是 ballsJsonMap 的最终输出与任何函数 运行 之前的输出相同。我正在尝试做一些基本的球/池物理,理想情况下,X、Y、Z 值将针对 step 函数的每个 运行 进行修改(+= 或 -=),例如所以:

step(10):

{"cue": {"x":1.8, "y":0.0, "z": 1.8}, "ball_1" : {etc, etc}, }

step(9):

{"cue": {"x":1.65, "y":0.0, "z": 1.65}, "ball_1" : {etc, etc}, }

step(8):

{"cue": {"x":1.55, "y":0.0, "z": 1.55}, "ball_1" : {etc, etc}, }

等等,等等。

是的,在代码末尾,当您调用 inspect ballsJsonMap 时,您指的是作为参数传递给函数的 ballsJsonMap

可以这样想:

假设您有一些值 x,并想对其应用一个函数。语法可能如下所示:

f(x) = y

例如,假设您想要一个函数来计算数字的二次方。喜欢

pow = fn x -> x * x end

现在,如果你想使用它,你可以:

x = 2
pow.(x)
# => 4

这意味着 pow.(x) 会给你 4,正如预期的那样,但 x 仍然是 2。这就是不变性有意义的原因。因为 pow.(2) 永远是 4,而 2 永远是 2,这是应该的。您想要为 x 计算 pow 的事实并不意味着 x 应该成为 的结果,除非您想要重新分配 x 到那个。

当您在 ballsJsonMap 上调用 Enum.map 时,它会创建一个单独的东西,但由于您没有将它分配给不同的变量,或将 ballsJsonMap 重新分配给结果,当您调用 Sys.Log.debug 时它会丢失。如果你最后没有那个调用,你的函数将 return Enum.map 的结果,因为它是函数中计算的最后一个东西。

为了按照您的意愿记录转换后的版本,您需要将其存储在不同的变量中,或者在记录之前重新分配 ballsJsonMap 给它:

ballsJsonMap = Enum.map(ballsJsonMap, fn({ball, prop}) ->
  ...
end)
Sys.Log.debug("#{inspect ballsJsonMap}")

或者您可以简单地将 Enum.map 调用通过管道传递给要记录的函数:

Enum.map(ballsJsonMap, fn({ball, prop}) ->
  ...
end)
|> inspect()
|> Sys.Log.debug()

这就在您的 step 函数中。您需要检查您是否在其余代码中做同样的事情。例如,您可能需要将 step 的结果存储在 looper 函数中,并使用存储的更新版本递归调用 looper,因为 looper现在,你总是用最初传递给 looperballsJsonMap 调用 step,而 step 的结果没有被用于任何事情

Elixir 程序员(新手和老手)最常犯的错误之一是忘记在修改变量后重新分配它。在 Elixir 中,你永远不能做这样的事情:

my_list = ["b", "a", "c"]
sort(my_list)
IO.inspect(my_list) # no change!

总是必须在修改后捕获输出,例如

my_list = ["b", "a", "c"]
my_list = sort(my_list)
IO.inspect(my_list) # sorted!

这很微妙,但是一次重新分配会产生巨大的不同:当其他语言传递引用时突然出现“远距离的幽灵般的动作”,并且值突然改变,因为有人在某处做了一件事别的。 Elixir 中的变量 always 具有分配给它的值;它永远不会被神奇地间接修改

在你的情况下,这个概念在几个地方悄悄地出现在你身上。首先,要检查 step/2 函数的输出,您需要在检查之前捕获 Enum.map/2 操作的结果,因为 Enum.map returns修改后的值。考虑:

def step(timestep, ballsJsonMap) do
    result = Enum.map(ballsJsonMap, fn({ball, prop}) -> ... end)
    IO.inspect(result)
    result
end

请记住,隐含的 return 意味着最后 运行 是 returned 的内容。但是,与其改变函数的内部结构,不如检查 looper 函数中的值 up 可能更容易。

updatedBallsJsonMap = step(timestep, ballsJsonMap)
IO.inspect(updatedBallsJsonMap)
looper(timestep, x - 1, updatedBallsJsonMap)

或者,更惯用的做法是,编写您的函数,以便为要转换的变量保留第一个参数。这样你就可以使用方便的 |> 管道,并完全省略第一个参数,例如

ballsJsonMap
|> step(timestep)
|> IO.inspect()  # <-- remove this line when you're ready
|> looper(timestep, x-1)

以上假设 steplooper 函数已经重构,因此 ballsJsonMap 作为第一个参数接收。

如果您只需要更新给定的 ballsJsonMap,请尝试 Enum.reduce/3

balls_map = %{
      "cue" => %{"x" => 2.0, "y" => 0.0, "z" => 2.0},
      "ball_1" => %{"x" => 5.0, "y" => 0.0, "z" => 5.0},
      "ball_2" => %{"x" => 10.0, "y" => 0.0, "z" => 5.0}
    }

    a = 2

    updated_balls_map = Enum.reduce(balls_map, %{}, fn {key, %{"x" => x, "y" => y, "z" => z}}, acc ->
      Map.put(acc, key, %{"x" => x + a, "y" => y + 2, "z" => z + 2})
    end)
    
    IO.inspect(updated_balls_map)

请注意如何在函数子句中使用模式匹配来捕获现有值。

还有一些内务指导:

  • 惯用的 Elixir 使用 snake_case 作为变量
  • 使用IO.inspectIO.putsLogger.debug等查看变量。