Midwire
Midwire

Reputation: 1100

Elixir - modifying values external to an anonymous function

First, I am absolutely sure that I'm going about this the wrong way as I'm still learning Elixir coming from Ruby...

I get a list of search results from youtube and am trying to extract the video with the most views.

# html is the contents of the search results page
metas = html |> Floki.find(".yt-lockup-meta-info > li")

counter = -1
index = -1
high_views = 0

Enum.each(metas, fn(li) ->
  counter = counter + 1
  text = Floki.text(li)
  case String.split(text, " ") do
    [count, "views"] ->
      views = String.to_integer(String.replace(count, ",", ""))
      IO.puts(">>> #{counter} - #{to_string(views)} views")
      if views > high_views do
        high_views = views
        index = counter
      end
    [age, time_measurement, "ago"] ->
      nil
  end
end)

metas is a list of li tuples, like this:

[{"li", [], ["2 years ago"]}, {"li", [], ["5,669,783 views"]},
 {"li", [], ["9 years ago"]}, {"li", [], ["17,136,804 views"]},
 ...
 {"li", [], ["1 year ago"]}, {"li", [], ["15,217 views"]},
 {"li", [], ["8 years ago"]}, {"li", [], ["909,053 views"]}]

This won't work because the anonymous function passed to Enum.each has its own scope and doesn't set the values for index and high_views.

Is there a way to pass values from the outside scope into the anonymous function? Or maybe a better question is, how should I go about doing this?

I intended to get it working and then refactor the code but I'm stuck. Thanks for any help.

Upvotes: 2

Views: 1537

Answers (2)

Martin Svalin
Martin Svalin

Reputation: 2267

Elixir is immutable. The function is a closure, so the outside variables are visible in there, but you can't mutate them. You can only re-bind them, but that re-binding stays in the inner, anonymous function scope.

But the tools for what you're trying to do are all in the Enum module.

You're essentially looking for the index with the maximum views. Let's look through the Enum functions. Enum.max_by/2 looks promising. It takes an enumerable and a function that returns a value we want to max. I'll pair it up with Enum.with_index/1, that takes a list, and wraps each element in a tuple with that element's index.

metas
|> Enum.with_index
|> Enum.max_by(fn {li, index} ->
  text = Floki.text(li)
  case String.split(text) do # (splits on whitespace by default)
    [count, "views"] ->
      views = count |> String.replace(",", "") |> String.to_integer
      IO.puts ">>> #{index} - #{views} views"
      views
    _ -> -1
  end
end)

The main difference to your implementation is that the inner function returns a value based on its arguments, instead of trying to mutate outside state.

I collapsed the "do nothing" case to a simple catch-all _, and return -1 on the assumption that youtube videos don't have negative view counts. A straight translation of your example would return zero here (the initial value of your high_views). That's probably safe, too.

Upvotes: 8

Midwire
Midwire

Reputation: 1100

As I surmised, I was indeed going about it all wrong. Here's how I ended up making this work:

defp extract_song_url_from_youtube_response(html = _) do
  sorted = html
  |> Floki.find(".yt-lockup-content")
  |> Enum.sort(fn(item1, item2) -> view_count(item1) > view_count(item2) end)

  [_, id] = Enum.at(sorted, 0)
  |> Floki.find("h3 > a")
  |> Floki.attribute("href")
  |> Enum.find(fn(x) -> x =~ "/watch" end)
  |> String.split("=")
  "https://www.youtube.com/embed/" <> id
end

defp view_count(item) do
  meta = item |> Floki.find(".yt-lockup-meta-info > li")
  views = case Enum.at(meta, 1) do
    {"li", _, viewlist} ->
      parts = String.split(Enum.at(viewlist, 0), " ")
      String.to_integer(String.replace(Enum.at(parts, 0), ",", ""))
    nil ->
      # most likely a playlist
      0
  end
end

So, instead of trying to modify variables from outside the scope of the anonymous function, I stepped back up in the HTML hierarchy and sort on each <div> result based on the number of views the video has received.

Elixir is just amazing, once I can wrap my head around it and stop trying to force things to be like Ruby.

Upvotes: 1

Related Questions