Reputation: 4376
I'm working on some proprietary APIs that makes use of Elixir language. I'm very new to the latter, so please bear with me.
I'm still wrapping my head around the whole immutable concept, which makes things really hard for me.
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
The Sys.Tuning.value("balls")
in the doRequest
function is responsible for loading the data from a JSON file that looks like this:
{
"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},
}
But the final output of ballsJsonMap
is the same as it was before any functions were ran. I'm trying to do some basic ball / pool physics, and ideally, the X, Y, Z values would be modified (+= or -=) for every run of the step
function, like so:
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}, }
And so on, and so forth.
Upvotes: 1
Views: 389
Reputation: 9568
One of the most common mistakes made by Elixir programmers (new and experienced alike) is forgetting to re-assign a variable after modifying it. In Elixir, you can never do something like this:
my_list = ["b", "a", "c"]
sort(my_list)
IO.inspect(my_list) # no change!
You always have to capture the output after modification, e.g.
my_list = ["b", "a", "c"]
my_list = sort(my_list)
IO.inspect(my_list) # sorted!
It's subtle, but that one re-assignment makes a huge difference: you can never have that "spooky action at a distance" that pops up when other languages pass around references and suddenly values change because somebody did a thing somewhere else. A variable in Elixir always has the value it was assigned; it never gets magically modified indirectly.
In your case, this concept is sneaking up on you in a couple places. First, to inspect the output of your step/2
function, you would need to capture the result of the Enum.map/2
operation prior to inspecting it, because Enum.map
returns the modified value. Consider:
def step(timestep, ballsJsonMap) do
result = Enum.map(ballsJsonMap, fn({ball, prop}) -> ... end)
IO.inspect(result)
result
end
Remember that the implicit return means that the last thing run is what gets returned. Instead of altering the innards of your function however, it would probably be easier to inspect the value up in your looper
function.
updatedBallsJsonMap = step(timestep, ballsJsonMap)
IO.inspect(updatedBallsJsonMap)
looper(timestep, x - 1, updatedBallsJsonMap)
Or, even more idiomatically, write your functions so that the first argument is reserved for the variable that is being transformed. That way you can use the handy |>
pipe, and omit the 1st argument altogether, e.g.
ballsJsonMap
|> step(timestep)
|> IO.inspect() # <-- remove this line when you're ready
|> looper(timestep, x-1)
The above assumes that the step
and looper
functions have been refactored so that the ballsJsonMap
is received as the first argument.
If you just need to update the given ballsJsonMap
, give Enum.reduce/3
a try:
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)
Note how you can use pattern matching in the function clause to capture the existing values.
And just for a couple house-keeping pointers:
IO.inspect
or IO.puts
or Logger.debug
et al to view variables.Upvotes: 2
Reputation: 2212
Yes, at the end of your code, when you call inspect ballsJsonMap
, you're referring to the ballsJsonMap
that was passed as an argument to the function.
Think of it like this:
Imagine you have some value x, and want to apply a function to it. The syntax might look like:
f(x) = y
For instance, imagine you want a function to calculate the second power of a number. Like
pow = fn x -> x * x end
Now, if you want to use it, you can:
x = 2
pow.(x)
# => 4
This means that pow.(x)
will give you 4, as expected, but x
is still 2. That's why immutability makes sense. Because pow.(2)
will always be 4, but 2 will always be 2, as it should. The fact that you want to calculate pow
for x
doesn't mean x
should become the result of that unless you want to reassign x
to that.
When you're calling Enum.map
on your ballsJsonMap
, it's creating a separate thing, but since you're not assigning it to a different variable, or reassigning ballsJsonMap
to the result of that, it gets lost when you call Sys.Log.debug
. If you didn't have that call at the end, your function would return the result of Enum.map
, because it was the last thing that was computed in the function.
In order to log the transformed version, like you want, you would need to store it in a different variable, or reassign ballsJsonMap
to it before logging it:
ballsJsonMap = Enum.map(ballsJsonMap, fn({ball, prop}) ->
...
end)
Sys.Log.debug("#{inspect ballsJsonMap}")
Or you could simply pipe the Enum.map
call to the functions to log:
Enum.map(ballsJsonMap, fn({ball, prop}) ->
...
end)
|> inspect()
|> Sys.Log.debug()
And that's just in your step
function. You would need to check if you're doing the same thing in the rest of your code. For instance you would probably need to store the result of step
in your looper
function and make the recursive call to looper
with that stored, updated version, because the way looper
is right now, you're always calling step
with the ballsJsonMap
that was initially passed to looper
, and the result of step
isn't being used for anything
Upvotes: 1