Writing idiomatic Elixir code

Trying to write good elixir code.

Input:

data = %{"a" => 0, "b" => 2, "c" => 0, "evilkey" => 666}

for specific keys i need to write some specific output if condition is true

Ruby version:

out = []
out << "text1" if data["a"] > 0
out << "text2" if data["b"] > 0
out << "text3" if data["c"] > 0
out

Ugly elixir version:

out = []
out = if data["a"] > 0 do
  out ++ ["text1"]
else
  out
end
...
...
out

Good elixir version: ???

Any thoughts here?

2 Likes

A more functional apporach

output_texts = %{
  "a" => "text1",
  "b" => "text2",
  "c" => "text3",
}

out = data
      |> Enum.filter(fn {_, v} -> v > 0 end)
      |> Enum.filter(fn {k, _} -> Enum.member(Map.keys(output_texts), k) end)
      |> Enum.map(fn {k, _} -> output_texts[k] end)
      |> Enum.join()
2 Likes

You can do that by using if as an expression and assembling list of values finally concatenating them together:

IO.iodata_to_binary([
  if(data["a"] > 0, do: "text1", else: ""),
  if(data["b"] > 0, do: "text2", else: ""),
  if(data["c"] > 0, do: "text3", else: "")
])

You could even extract the if to a separate function, if the pattern if very repeating:

defp if_positive(value, text), do: if(value > 0, do: text, else: "")

IO.iodata_to_binary([
  if_positive(data["a"], "text1"),
  if_positive(data["b"], "text2"),
  if_positive(data["c"], "text3")
])
5 Likes

Thanks!

The solution with filters and map look about right.

Re: IO.iodata_to_binary thing - i can’t just put things together this way, but overall it looks good, just need to add |> Enum.filter(fn(x) -> !is_nil(x) end), i.e.

[
  (if data["a"] > 0, do: "text1"),
  (if data["b"] > 0, do: "text2"),
  (if data["c"] > 0, do: "text3")
] |> Enum.filter(fn(x) -> !is_nil(x) end)
2 Likes
data
|> Enum.filter(&match?({_, v} when v > 0, &1))
|> Enum.map(fn 
  {"a", _} -> "text1"
  {"b", _} -> "text2"
  {"c", _} -> "text3"
end)
3 Likes

Also because nobody has mentioned this yet, the “ugly elixir version” is not actually an elixir version. It would not work at all because out is immutable.

2 Likes

It will, there are re-assignment for out:

out = if ...
  out ++ ...
else
  out
end

so every consequent out is different variable

2 Likes

We do not have any guarantee of ordering in a map. So in theory the begs pour of this snippet is non deterministic, while in the OPs version we do have an explicit ordering of the keys and their transformation/concatenation into the result.

2 Likes

I know people love |> but you don’t have to use it:

:maps.fold(fn (k,v,acc) when v > 0 -> acc ++ [output_texts[k]]
              (_,_,acc) -> acc
           end, [], data)

There is probably a function equivalent to fold in Enum but it is called something else and I could not immediately see it, hence using the one in :maps.

2 Likes

Yeah, in my use-case ordering guarantees are required so maps don’t work (but for someone else it could be better approach). also there are other non-related keys as well (which should be filtered out)

plain [..., ..., ...] |> remove nils works just fine - ordering granaries, short and easy to read code

1 Like

It’s called reduce and is a left fold AFAIR

Ah, classic name and not from the functional world. On a map there is no left or right.

I’m aware that we have no left or right in a map but we do have more data structures which in fact do have. And for some operations it is important to know if we are folding from the left or the right.

1 Like

Yes, for many structures you definitely need to know whether you are folding left or right. That is one reason why calling them foldl and foldr is better than just reduce. The :maps module just has fold as there is no defined ordering.

1 Like