如何在Elixir中将映射键从字符串转换为原子


77

什么是转换的方式%{"foo" => "bar"},以%{foo: "bar"}在仙丹?


2
警告:[ String.to_atom/1]动态创建原子,并且原子不会被垃圾收集。因此,字符串不应是不受信任的值,例如从套接字接收到的输入或在Web请求期间输入的信息。考虑改用to_existing_atom / 1。hexdocs.pm/elixir/String.html#to_atom/1
vaer-k

Answers:


92

使用理解

iex(1)> string_key_map = %{"foo" => "bar", "hello" => "world"}
%{"foo" => "bar", "hello" => "world"}

iex(2)> for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), val}
%{foo: "bar", hello: "world"}

1
如何将其存储到一些变量中!
Rizwan Patel

2
如何在下面将其转换为原子串[%{“ foo” =>“ bar”,“ hello” =>“ world”},%{“ foo” =>“ baromater”,“ hello” =>“ nope”} ]
Rizwan Patel

2
这是递归辅助函数的函数定义: def keys_to_atoms(string_key_map) when is_map(string_key_map) do for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), keys_to_atoms(val)} end def keys_to_atoms(value), do: value
Olshansk

9
警告!您不应该在不受信任的用户输入上调用此函数,因为原子不会被垃圾回收,并且可能会导致您遇到BEAM允许的原子数限制:hexdocs.pm/elixir/String.html#to_atom/1
Jason Axelson

1
请注意,这不会转换深度/递归
sn3p

52

我认为最简单的方法是使用Map.new

%{"a" => 1, "b" => 2} 
|> Map.new(fn {k, v} -> {String.to_atom(k), v} end)      

=> %{a: 1, b: 2}

我喜欢这个。因为如果您使用管道,for循环会变得很困难。
Kaushik Evani

请注意,这不会转换深度/递归
sn3p

23

您可以结合使用Enum.reduce / 3String.to_atom / 1

%{"foo" => "bar"}
|> Enum.reduce(%{}, fn ({key, val}, acc) -> Map.put(acc, String.to_atom(key), val) end)

但是,您应该警惕转换为基于用户输入的原子,因为它们不会被垃圾回收,否则会导致内存泄漏。看到这个问题

如果原子已经存在,则可以使用String.to_existing_atom / 1防止出现这种情况。


是的,我知道内存泄漏,同样的事情在2.2之前是在ruby中,但是还是谢谢你
NoDisplayName 2015年

10
是的,在Ruby中是相同的东西,但是Elixir不是Ruby,通常在Elixir中不建议使用这种模式。我能想到它让我考虑的首要意义上的唯一情况是,当将数据加载到结构(然后有更安全的方式来做到这一点)
何塞·Valim

11
@JoséValim将数据加载到结构中的更安全方法是什么?
Letseatlunch '16

7

要以@emaillenin的答案为基础,可以检查键是否已经是原子,以避免ArgumentErrorString.to_atom在得到键已经是原子的情况下引发该键。

for {key, val} <- string_key_map, into: %{} do
  cond do
    is_atom(key) -> {key, val}
    true -> {String.to_atom(key), val}
  end
end

5

有一个用于此的库,https://hex.pm/packages/morphix。它还具有嵌入式密钥的递归功能。

大多数工作都在此功能中完成:

defp atomog (map) do
    atomkeys = fn({k, v}, acc) ->
      Map.put_new(acc, atomize_binary(k), v)
    end
    Enum.reduce(map, %{}, atomkeys)
  end

  defp atomize_binary(value) do 
    if is_binary(value), do: String.to_atom(value), else: value
  end

递归调用。阅读@Galzer的答案后,我可能会将其转换为String.to_existing_atom很快使用。


递归如何?atomog(%{"a" => %{"b" => 2}})返回%{a: %{"b" => 2}}
sn3p

atomog函数被称为递归函数的一部分,它本身不是递归的。有关更多详细信息,请检查morphix代码本身。
philosodad

4

这是模块形式的@emaillenin答案的版本:

defmodule App.Utils do

  # Implementation based on: http://stackoverflow.com/a/31990445/175830
  def map_keys_to_atoms(map) do
    for {key, val} <- map, into: %{}, do: {String.to_atom(key), val}
  end

  def map_keys_to_strings(map) do
    for {key, val} <- map, into: %{}, do: {Atom.to_string(key), val}
  end

end

4

首先,@ Olshansk的回答对我来说就像一个魅力。谢谢你

接下来,由于@Olshansk提供的初始实现缺少对地图列表的支持,因此下面是我的代码片段对此进行了扩展。

  def keys_to_atoms(string_key_map) when is_map(string_key_map) do
    for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), keys_to_atoms(val)}
  end

  def keys_to_atoms(string_key_list) when is_list(string_key_list) do
    string_key_list
    |> Enum.map(&keys_to_atoms/1)
  end

  def keys_to_atoms(value), do: value

这是我使用的示例,然后将其传递给上述函数后输出- keys_to_atoms(attrs)

# Input
%{
  "school" => "School of Athens",
  "students" => [
    %{
      "name" => "Plato",
      "subjects" => [%{"name" => "Politics"}, %{"name" => "Virtues"}]
    },
    %{
      "name" => "Aristotle",
      "subjects" => [%{"name" => "Virtues"}, %{"name" => "Metaphysics"}]
    }
  ]
}

# Output
%{
  school: "School of Athens",
  students: [
    %{name: "Plato", subjects: [%{name: "Politics"}, %{name: "Virtues"}]},
    %{name: "Aristotle", subjects: [%{name: "Virtues"}, %{name: "Metaphysics"}]}
  ]
}

对此的解释非常简单。第一种方法是为类型映射的输入而调用的所有内容的心脏。for循环会破坏键值对中的属性,并返回键的原子表示形式。接下来,在返回值的同时,又有三种可能性。

  1. 该值是另一张地图。
  2. 该值是地图列表。
  3. 该值不是以上所有值,它是原始值。

因此,这一次,当keys_to_atoms在分配值的同时调用该方法时,它可能会根据输入的类型来调用这三种方法之一。这些方法以相似的顺序组织在代码段中。

希望这可以帮助。干杯!


2
defmodule Service.MiscScripts do

@doc """
Changes String Map to Map of Atoms e.g. %{"c"=> "d", "x" => %{"yy" => "zz"}} to
        %{c: "d", x: %{yy: "zz"}}, i.e changes even the nested maps.
"""

def  convert_to_atom_map(map), do: to_atom_map(map)

defp to_atom_map(map) when is_map(map), do: Map.new(map, fn {k,v} -> {String.to_atom(k),to_atom_map(v)} end)     
defp to_atom_map(v), do: v

end

1
完美的是,您正在处理嵌套地图(又名递归)
sn3p


1

这是我用来递归(1)将映射键设置为蛇形格式和(2)将其转换为原子的方法。请记住,切勿将未列入白名单的用户数据转换为原子,因为它们不会被垃圾收集。

defp snake_case_map(map) when is_map(map) do
  Enum.reduce(map, %{}, fn {key, value}, result ->
    Map.put(result, String.to_atom(Macro.underscore(key)), snake_case_map(value))
  end)
end
defp snake_case_map(list) when is_list(list), do: Enum.map(list, &snake_case_map/1)
defp snake_case_map(value), do: value

1

下面的代码片段将嵌套的json-like映射的键转换为现有原子:

iex(2)> keys_to_atoms(%{"a" => %{"b" => [%{"c" => "d"}]}})

%{a: %{b: [%{c: "d"}]}}
  def keys_to_atoms(json) when is_map(json) do
    Map.new(json, &reduce_keys_to_atoms/1)
  end

  def reduce_keys_to_atoms({key, val}) when is_map(val), do: {String.to_existing_atom(key), keys_to_atoms(val)}
  def reduce_keys_to_atoms({key, val}) when is_list(val), do: {String.to_existing_atom(key), Enum.map(val, &keys_to_atoms(&1))}
  def reduce_keys_to_atoms({key, val}), do: {String.to_existing_atom(key), val}

0

当您在另一张地图中有一张地图时

def keys_to_atom(map) do
 Map.new(
  map,
  fn {k, v} ->
    v2 = cond do
      is_map(v) -> keys_to_atom(v)
      v in [[nil], nil] -> nil
      is_list(v) -> Enum.map(v, fn o -> keys_to_atom(o) end)
      true -> v
    end
    {String.to_atom("#{k}"), v2}
  end
 )
end

样品:

my_map = %{"a" => "1", "b" => [%{"b1" => "1"}], "c" => %{"d" => "4"}}

结果

%{a: "1", b: [%{b1: "1"}], c: %{d: "4"}}

注意:当您使用“ b” => [1,2,3]时,is_list将失败,因此在这种情况下,您可以注释/删除此行:

# is_list(v) -> Enum.map(v, fn o -> keys_to_atom(o) end)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.