Elixir - 循环并添加到地图

Posted

技术标签:

【中文标题】Elixir - 循环并添加到地图【英文标题】:Elixir - Looping through and adding to map 【发布时间】:2015-07-07 14:01:15 【问题描述】:

我正在用我在 C# 中构建的一些代码在 Elixir 中重建一些东西。

它被很好地破解了,但运行良好(虽然不是在 Linux 上,因此重新构建)。

基本上它所做的是检查一些 RSS 提要并查看是否有任何新内容。这是代码:

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)

我想知道如何在 Elixir 中有效地进行枚举。另外,我在“历史”中添加东西的过程似乎是反函数式编程。

显然第一步是声明我的 URL 列表,但除此之外,枚举的想法让我很头疼。有人可以帮我吗?谢谢。

【问题讨论】:

【参考方案1】:

这是一个很好的挑战,解决它肯定会让您对函数式编程有所了解。

函数式语言中此类问题的解决方案通常是reduce(通常称为fold)。我将从一个简短的答案开始(而不是直接翻译),但随时要求跟进。

以下方法通常不适用于函数式编程语言:

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

最后的地图仍然是空的,因为我们不能改变数据结构。每次调用Map.put(map, x, x),它都会返回一张新地图。因此,我们需要在每次枚举后显式检索新地图。

我们可以在 Elixir 中使用 reduce 来实现这一点:

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

Reduce 会将前一个函数的结果作为下一项的累加器发出。运行上述代码后,变量map 将变为%1 =&gt; 1, 2 =&gt; 2, 3 =&gt; 3

出于这些原因,我们很少在枚举中使用each。相反,我们使用the Enum module 中的函数,这些函数支持广泛的操作,最终在没有其他选择时回退到reduce

编辑:回答问题并进行更直接的代码翻译,您可以这样做来检查和更新地图:

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

其实这里用set会更好,所以我们重构一下,在过程中使用set:

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

【讨论】:

好的,太棒了,谢谢你的帮助。但是,我仍然不知道如何使用这种方法检查项目是否是新的(不在地图中)?它经常发生,我宁愿不使用数据库之类的东西,尽管如果我不得不这样做的话。不过,这很有意义,谢谢。 结合 Enum 模块中的两个函数如何:成员?和过滤。这样你就可以编译不是现有列表成员的所有项目的列表,然后做一些事情。 您将如何添加到过滤器中?每当它找到一个独特的帖子时,它就会将它添加到列表中(不再是唯一的)并使用它执行一个功能。函数可以调用自己吗? 我已编辑问题以获得更完整的答案! 非常感谢@José Valim【参考方案2】:

这也可能有帮助:

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"

输出:

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

我只是在学习 Elixir,所以上述内容无疑不是最理想的,但希望能提供一些信息。

【讨论】:

【参考方案3】:

我也是 Elixir 的新手,但这里有一个使用模式匹配和递归的可爱而简单的解决方案。

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 

并仅使用您要映射的列表和一个空映射来调用该函数

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

【讨论】:

以上是关于Elixir - 循环并添加到地图的主要内容,如果未能解决你的问题,请参考以下文章

为啥元组在 Elixir 中不可枚举?

Elixir:模式匹配对元组和映射的工作方式不同

dialyzer 无法识别 elixir 函数并出现错误:0:unknown_function

如何管道为循环elixir

在 Laravel Elixir 升级后使用严格添加到每个文件 - 损坏的脚本

Elixir/Erlang:变量列表与许多其他列表合并时出错