简体   繁体   中英

How can I refactor two similar functions to reduce the code duplication in Elixir?

I have the following two functions:

  defp select_previous_scheduled_price(scheduled_prices, date) do
    if length(scheduled_prices) > 1 do
      before_prices = Enum.filter(scheduled_prices, &starts_before(&1, date))

      if !Enum.empty?(before_prices) do
        hd(before_prices)
      else
        nil
      end
    else
      nil
    end
  end

  defp select_next_scheduled_price(scheduled_prices, date) do
    if length(scheduled_prices) >= 1 do
      after_prices = Enum.filter(scheduled_prices, &starts_after(&1, date))

      if !Enum.empty?(after_prices) do
        hd(after_prices)
      else
        nil
      end
    else
      nil
    end
  end

There are two differences: 1. The operators on the second line (ie, > vs >= ); and 2. The function invoked for filtering on the third line3 (ie, &starts_before/2 vs &starts_after/2 )

As the differences are operators instead of functions and functions to which both local and parameterized values must be applied, I'm not entirely clear whether or how this might be factored out.

In other words, I'd like to make the solution like this (only actually functional, which this won't be):

  defp select_previous_scheduled_price(scheduled_prices, date) do
    select_scheduled_price(scheduled_prices, date, >, &starts_before/2)
  end

  defp select_next_scheduled_price(scheduled_prices, date) do
    select_scheduled_price(scheduled_prices, date, >=, &starts_after/2)
  end

  defp select_scheduled_price(scheduled_prices, date, meets_length_criteria, filter_criteria) do
    if meets_length_criteria(scheduled_prices, 1) do
      qualified_prices = Enum.filter(scheduled_prices, &filter_criteria(&1, date))

      if !Enum.empty?(qualified_prices) do
        hd(qualified_prices)
      else
        nil
      end
    else
      nil
    end
  end

Any ideas how to make this work?

Thanks!

List.first/1 instead of hd/1 would eliminate the necessity of the nested if in the first place. Then I'd split code into smaller functions to clarify the intent.

defp if_prices(:before, prices, date),
  do: {length(prices) > 1, &starts_before(&1, date)}
defp if_prices(:after, prices, date),
  do: {length(prices) >= 1, &starts_after(&1, date)}

defp select_previous_scheduled_price(scheduled_prices, date),
  do: select_scheduled_price(:before, scheduled_prices, date)
defp select_next_scheduled_price(scheduled_prices, date),
  do: select_scheduled_price(:after, scheduled_prices, date)

defp select_scheduled_price(direction, prices, date) do
  case if_prices(direction, prices, date) do
    {false, _} -> nil
    {_, fun} -> 
      prices
      |> Enum.filter(fun)
      |> List.first()
  end
end

Here's a version very similar to what you attempted. It compiles (but I've not ran it with real data).

defp select_previous_scheduled_price(scheduled_prices, date) do
  select_scheduled_price(scheduled_prices, date, &>/2, &starts_before/2)
end

defp select_next_scheduled_price(scheduled_prices, date) do
  select_scheduled_price(scheduled_prices, date, &>=/2, &starts_after/2)
end

defp select_scheduled_price(scheduled_prices, date, meets_length_criteria, filter_criteria) do
  if meets_length_criteria.(scheduled_prices, 1) do
    scheduled_prices
    |> Enum.filter(&filter_criteria.(&1, date))
    |> List.first()
  else
    nil
  end
end

Notable changes:

  1. To pass binary operators you can use & and /2 : >= becomes &>=/2 .
  2. To call these functions you need to use a . .
  3. I've made the if logic more idiomatic.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM