zack_falcon
zack_falcon

Reputation: 4376

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

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

Answers (2)

Everett
Everett

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:

  • idiomatic Elixir uses snake_case for variables
  • Use IO.inspect or IO.puts or Logger.debug et al to view variables.

Upvotes: 2

sbacarob
sbacarob

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 looperis 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

Related Questions