web-dev-qa-db-fra.com

Elixir - Boucle et ajout à la carte

Je reconstruis quelque chose dans Elixir à partir d'un code que j'ai construit en C #.

Il a été assez piraté ensemble, mais fonctionne parfaitement (bien que pas sur Linux, donc reconstruire).

Essentiellement, cela a consisté à vérifier certains flux RSS et à voir s'il y avait du nouveau contenu. Voici le code:

Map historic (URL as key, post title as value).
List<string> blogfeeds
while true
for each blog in blogfeeds
   List<RssPost> posts = getposts(blog)
   for each post in posts
        if post.url is not in historic
           dothing(post)
           historic.add(post)

Je me demande comment je peux faire le dénombrement efficacement dans Elixir. De plus, il semble que mon processus même d'ajout de choses à "historique" est une programmation anti-fonctionnelle.

Évidemment, la première étape consistait à déclarer ma liste d'URL, mais au-delà, l'idée d'énumération me dérange la tête. Est-ce que quelqu'un peut m'aider? Merci.

35
William Dunne

C'est un beau défi à relever et le résoudre vous donnera certainement un aperçu de la programmation fonctionnelle.

La solution à ces problèmes dans les langages fonctionnels est généralement reduce (souvent appelée fold). Je vais commencer par une réponse courte (et non une traduction directe) mais n'hésitez pas à demander un suivi.

L'approche ci-dessous ne fonctionne généralement pas dans les langages de programmation fonctionnels:

map = %{}
Enum.each [1, 2, 3], fn x ->
  Map.put(map, x, x)
end
map

La carte à la fin sera toujours vide car nous ne pouvons pas muter les structures de données. Chaque fois que vous appelez Map.put(map, x, x), il renverra une nouvelle carte. Nous devons donc récupérer explicitement la nouvelle carte après chaque énumération.

Nous pouvons y parvenir dans Elixir en utilisant réduire:

map = Enum.reduce [1, 2, 3], %{}, fn x, acc ->
  Map.put(acc, x, x)
end

Réduire émettra le résultat de la fonction précédente comme accumulateur pour l'élément suivant. Après avoir exécuté le code ci-dessus, la variable map sera %{1 => 1, 2 => 2, 3 => 3}.

Pour ces raisons, nous utilisons rarement each sur l'énumération. Au lieu de cela, nous utilisons les fonctions dans le module Enum , qui prennent en charge un large éventail d'opérations, pour finalement revenir à reduce lorsqu'il n'y a pas d'autre option.

EDIT: pour répondre aux questions et passer par une traduction plus directe du code, voici ce que vous pouvez faire pour vérifier et mettre à jour la carte au fur et à mesure:

Enum.reduce blogs, %{}, fn blog, history ->
  posts = get_posts(blog)
  Enum.reduce posts, history, fn post, history ->
    if Map.has_key?(history, post.url) do
      # Return the history unchanged
      history
    else
      do_thing(post)
      Map.put(history, post.url, true)
    end
  end
end

En fait, un ensemble serait mieux ici, alors refactorisons ceci et utilisons un ensemble dans le processus:

def traverse_blogs(blogs) do
  Enum.reduce blogs, HashSet.new, &traverse_blog/2
end

def traverse_blog(blog, history) do
  Enum.reduce get_posts(blog), history, &traverse_post/2
end

def traverse_post(post, history) do
  if post.url in history do
    # Return the history unchanged
    history
  else
    do_thing(post)
    HashSet.put(history, post.url)
  end
end
86
José Valim

Cela pourrait aussi aider:

count_animals_in_area = fn (area, acc) ->
  acc = case Map.has_key?(area, "duck") do
          true ->
            Map.put(acc, "ducks", (acc["ducks"] + area["duck"]))
          false ->
            acc
        end

  acc = case Map.has_key?(area, "goose") do
          true ->
            Map.put(acc, "geese", (acc["geese"] + area["goose"]))
          false ->
            acc
        end

  acc = case Map.has_key?(area, "cat") do
          true -> 
            Map.put(acc, "cats", (acc["cats"] + area["cat"]))
          false ->
            acc
        end

  acc
end

count_animals_in_areas = fn(areas) ->
  acc = %{ "ducks" => 0,
           "geese" => 0,
           "cats" => 0 }
  IO.inspect Enum.reduce areas, acc, count_animals_in_area
end

t1 = [ %{"duck" => 3, "goose" => 4, "cat" => 1},
       %{"duck" => 7, "goose" => 2},
       %{"goose" => 12}]

IO.puts "JEA: begin"
count_animals_in_areas.(t1)
IO.puts "JEA: end"

Sortie:

iex(31)> c "count_animals.exs"
JEA: begin
%{"cats" => 1, "ducks" => 10, "geese" => 18}
JEA: end
[]

J'apprends juste Elixir donc ce qui précède est sans aucun doute sous-optimal, mais, espérons-le, légèrement informatif.

0
James Anderson

Je suis également nouveau sur Elixir, mais voici une solution mignonne et simple qui utilise la correspondance de motifs et la récursivité.

defmodule YourModule do
   def reduce_list([], reduced) do reduced end

   def reduce_list([first | rest ], reduced) do
      # Do what you need to do here and call the function again 
      # with remaining list items and updated map.
      reduce_list(rest, Map.put(reduced, first, "Done"))
   end
end 

Et appelez la fonction avec juste la liste que vous souhaitez mapper et une carte vide

> YourModule.reduce_list(["one", "two", "three"], %{})
  %{"one" => "Done", "three" => "Done", "two" => "Done"}
0
Zemuldo