Conditional assignment nils out my variable

I’m trying to parse out if a given application has PC, Mac or Linux support.

defp platforms(game) do
  game
  |> Floki.find("img")
  |> Enum.reduce([], fn img, platforms ->
    image_url = Floki.attribute(img, "src") |> Enum.at(0)

    platforms =
      if image_url =~ "inline_pc" do
        ["PC" | platforms] |> IO.inspect()
      end

    platforms =
      if image_url =~ "inline_mac" do
        ["Mac" | platforms] |> IO.inspect()
      end

    platforms =
      if image_url =~ "inline_linux" do
        ["Linux" | platforms] |> IO.inspect()
      end

    platforms
  end)
  |> Enum.uniq()
  |> IO.inspect()
end

I notice that in my if conditional, if it doesn’t pass it will nil out my platforms variable.

Something like this works fine, but it’s ugly as hell:

        if image_url =~ "inline_pc" do
          ["PC" | platforms]
        else
          platforms
        end

Wondering if there’s a better approach to accomplish my goal. Thank you!

Of course it “nils out” the variable, how it should work otherwise? Remember that Elixir do not have “variables” in non-FP programming languages sense, it has bindings. That is one, the other part is that Elixir if is a macro that falls back to else: nil if that branch is not set . So your:

if image_url =~ "inline_linux" do
  ["Linux" | platforms] |> IO.inspect()
end

In fact ends as:

if image_url =~ "inline_linux" do
  ["Linux" | platforms] |> IO.inspect()
else
  nil
end

So nothing strange there, instead what you need to do is:

if image_url =~ "inline_linux" do
  ["Linux" | platforms] |> IO.inspect()
else
  platforms
end

If you want better solution then you need give us example of what image_url can be.


Additionaly, your =~ calls can be replaced with String.contains?/2 which should end slightly faster as it do not need to compile regular expression. Also your Enum.reduce can be replaced with Enum.flat_map.

2 Likes
platforms = cond do
  image_url =~ "inline_pc" -> ["PC" | platforms] |> IO.inspect()
  image_url =~ "inline_mac" -> ["Mac" | platforms] |> IO.inspect()
  image_url =~ "inline_linux" -> ["Linux" | platforms] |> IO.inspect()
  true -> platforms
end
1 Like

This one has different semantics, as in the original code it will work for example on string "inline_pc inline_mac" differently from yours.

2 Likes

Yes, it will behave wrongly if image_url contains multiple answers in the string.

Wouldn’t this only go through one branch? A game can be in multiple platforms.

Yes it will, sorry I thought image_url would contain one platform only per reduce operation.

In this case as @hauleth mentionned, if without else will return nil in case the condition is not met.

1 Like
iex(1)> pairs = %{"inline_pc" => "PC", "inline_mac" => "Mac", "inline_linux" => "Linux"}
%{"inline_linux" => "Linux", "inline_mac" => "Mac", "inline_pc" => "PC"}
iex(2)> img_url = "inline_pc inline_linux"
"inline_pc inline_linux"
iex(3)> Enum.reduce(pairs,[], fn ({k,v},acc) -> if(String.contains?(img_url,k), do: [v|acc], else: acc) end)
["PC", "Linux"]
iex(4)>


Alternately MapSet is handy as well:

iex(1)> m = [{"inline_pc", "PC"}, {"inline_mac", "Mac"}, {"inline_linux", "Linux"}]
[{"inline_pc", "PC"}, {"inline_mac", "Mac"}, {"inline_linux", "Linux"}]
iex(2)> imgs = ["inline_pc", "inline_pc inline_linux", "inline_linux"]
["inline_pc", "inline_pc inline_linux", "inline_linux"]
iex(3)> h =
...(3)>   fn pairs ->
...(3)>     fn (img, set) ->
...(3)>       List.foldl(pairs, set, fn ({k,v},acc) -> if(String.contains?(img,k), do: MapSet.put(acc,v), else: acc) end)
...(3)>     end
...(3)>   end
#Function<6.128620087/1 in :erl_eval.expr/5>
iex(4)> f =
...(4)>   fn (list, g) ->
...(4)>     platforms = MapSet.new()
...(4)>     list
...(4)>     |> List.foldl(platforms, g)
...(4)>     |> MapSet.to_list()
...(4)>   end
#Function<12.128620087/2 in :erl_eval.expr/5>
iex(5)> f.(imgs, h.(m))
["Linux", "PC"]
iex(6)> 


Consistent ordering, no duplicates

iex(1)> m = [{"inline_linux", "Linux"}, {"inline_mac", "Mac"}, {"inline_pc", "PC"}]
[{"inline_linux", "Linux"}, {"inline_mac", "Mac"}, {"inline_pc", "PC"}]
iex(2)> imgs = ["inline_pc", "inline_pc inline_linux", "inline_linux"]
["inline_pc", "inline_pc inline_linux", "inline_linux"]
iex(3)> g =
...(3)>   fn list ->
...(3)>     fn ({k,v}, acc) ->
...(3)>       if(Enum.any?(list, fn img -> String.contains?(img,k) end), do: [v|acc], else: acc) 
...(3)>     end
...(3)>   end
#Function<6.128620087/1 in :erl_eval.expr/5>
iex(4)> List.foldl(m, [], g.(imgs))
["PC", "Linux"]
iex(5)> 

4 Likes

Are You sure You need MapSet for that case? because You reduce pairs… and there would be no risk of duplication.

The OP had Enum.uniq:

  • Without it ["inline_pc", "inline_pc inline_linux", "inline_linux"] would lead to 2 “PC” and 2 “Linux” values in the platforms list as you go through all three elements.

I suspect that a single game can have multiple images as they can vary between platforms but some games may have the same image for multiple platforms.

1 Like

Ok, I see now the Enum.uniq :slight_smile:

Thank you everyone I appreciate your help!

I learned something new today that the else if not set, just returns nil.

1 Like