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
现在,你总是用最初传递给 looper
的 ballsJsonMap
调用 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)
以上假设 step
和 looper
函数已经重构,因此 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.inspect
或IO.puts
或Logger.debug
等查看变量。
我正在研究一些使用 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
现在,你总是用最初传递给 looper
的 ballsJsonMap
调用 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)
以上假设 step
和 looper
函数已经重构,因此 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.inspect
或IO.puts
或Logger.debug
等查看变量。