Як перетворити ключі карти зі рядків в атоми в Еліксирі


79

Який спосіб перетворити %{"foo" => "bar"}в %{foo: "bar"}в еліксиру?


2
Попередження: [ String.to_atom/1] створює атоми динамічно, і атоми не збирають сміття. Отже, рядок не повинен бути ненадійним значенням, таким як вхідні дані, отримані від сокета, або під час веб-запиту. Подумайте про використання to_existing_atom / 1. hexdocs.pm/elixir/String.html#to_atom/1
vaer-k

Відповіді:


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
як зберігати його в якійсь змінній!
Різван Патель 02

2
як перетворити нижче в рядки атомів [% {"foo" => "bar", "hello" => "world"},% {"foo" => "baromater", "hello" => "nope"} ]
Різван Патель

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
Ольшанськ,

9
Увага! Ви не повинні викликати це при введенні ненадійного користувача, оскільки atom не є зібраним сміттям, і це може призвести до того, що ви натрапите на обмежену
Jason Аксельсон

1
Тільки зауважте, що це не перетворює глибокий / рекурсивний
sn3p

54

Я думаю, що найпростіший спосіб зробити це - використовувати Map.new:

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

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

Мені подобається це. Тому що якщо ви трубите трубопровід, це стає важко з циклом for.
Каушик Евані,

Тільки зверніть увагу, що це не перетворює глибокий / рекурсивний
sn3p

23

Ви можете використовувати комбінацію Enum.reduce / 3 і String.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, але все одно дякую
NoDisplayName

10
Так, у Ruby це було те саме, але Elixir - це не Ruby, і в цілому ця модель вкрай не рекомендується в Elixir. Єдиний випадок, який, на мою думку, має сенс - це завантаження даних у структури (і тоді є більш безпечні способи це зробити)
Хосе Валім,

12
@ JoséValim, який безпечніший спосіб завантаження даних у структури?
Letseatlunch

7

Щоб спиратися на відповідь @ emaillenin, ви можете перевірити, чи є ключі вже атомами, щоб уникнути того, ArgumentErrorщо піднімається String.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

функція атомогу називається частиною рекурсивної функції, вона сама не є рекурсивною. Детальніше перевірте сам код morphix.
philosodad 20.03.20

5

Фрагмент нижче перетворює ключі вкладеної json-подібної карти в існуючі атоми:

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}

1
На мій погляд, цей написаний найкращий.
Хо-Шен Сяо

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
m = %{"key" => "value", "another_key" => "another_value"}
k = Map.keys(m)|> Enum.map(&(String.to_atom(&1)))
v = Map.values(m)
result = Enum.zip(k, v) |> Enum.into(%{})

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

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"}}

Примітка: is_list не зможе, коли у вас є "b" => [1,2,3], тому ви можете прокоментувати / видалити цей рядок, якщо це так:

# is_list(v) -> Enum.map(v, fn o -> keys_to_atom(o) end)

0

Мені дуже сподобалася відповідь Романа Бедічевського ... але мені потрібно було щось, що досконало розімкнуть ключі глибоко вкладених файлів yaml. Ось що я придумав:

   @doc """
   Safe version, will only atomize to an existing key
   """
   def atomize_keys(map) when is_map(map), do: Map.new(map, &atomize_keys/1)
   def atomize_keys(list) when is_list(list), do: Enum.map(list, &atomize_keys/1)
   def atomize_keys({key, val}) when is_binary(key),
     do: atomize_keys({String.to_existing_atom(key), val})
   def atomize_keys({key, val}), do: {key, atomize_keys(val)}
   def atomize_keys(term), do: term

   @doc """
   Unsafe version, will atomize all string keys
   """
   def unsafe_atomize_keys(map) when is_map(map), do: Map.new(map, &unsafe_atomize_keys/1)
   def unsafe_atomize_keys(list) when is_list(list), do: Enum.map(list, &unsafe_atomize_keys/1)
   def unsafe_atomize_keys({key, val}) when is_binary(key),
     do: unsafe_atomize_keys({String.to_atom(key), val})
   def unsafe_atomize_keys({key, val}), do: {key, unsafe_atomize_keys(val)}
   def unsafe_atomize_keys(term), do: term

Головне обмеження полягає в тому, що якщо ви подаєте його кортежем {key, value}, а ключ є двійковим файлом, він атомізує його. Це те, що ви хочете для списків ключових слів, але це, мабуть, чийсь крайній випадок. У будь-якому випадку файли YAML та JSON не мають поняття кортежу, тому для обробки цих файлів це не матиме значення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.