Skip to content

Conversation

josevalim
Copy link
Member

@josevalim josevalim commented Oct 18, 2019

Before this PR, sorting dates, decimals, and other structs was
somewhat awkward:

Enum.sort(dates, &(Date.compare(&1, &2) != :lt)) # ascending
Enum.sort(dates, &(Date.compare(&1, &2) != :gt)) # descending

Sorting a user by dates, decimals, and other structs was equally
verbose:

Enum.sort_by(users, &(&1.birthday), &(Date.compare(&1, &2) != :lt)) # ascending
Enum.sort_by(users, &(&1.birthday), &(Date.compare(&1, &2) != :gt)) # descending

Even a general sort in reverse required passing &>=/2 as an argument,
which is not clear in intent:

Enum.sort(list, &>=/2) # descending

This PR addresses this by adding two features:

  1. Sorting funtions now accept a module as sorter. The compare/2
    function of said modules are then used for sorting.

  2. sort_reverse* functions were added to complement the
    functionality above

All of the examples above can be written as:

Enum.sort(dates, Date) # ascending
Enum.sort_reverse(dates, Date) # descending
Enum.sort_by(users, &(&1.birthday), Date) # ascending
Enum.sort_reverse_by(users, &(&1.birthday), Date) # descending
Enum.sort_reverse(list) # descending

The reason why we picked sort_reverse instead of sort_descending
is because a sorting funtion can still be given to sort_reverse, which
means a descending function will return an ascending result (i.e. the
reverse).

Before this PR, sorting dates, decimals, and other structs was
somewhat awkward:

    Enum.sort(dates, &(Date.compare(&1, &2) != :lt)) # ascending
    Enum.sort(dates, &(Date.compare(&1, &2) != :gt)) # descending

Sorting a user by dates, decimals, and other structs was equally
verbose:

    Enum.sort_by(users, &(&1.birthday), &(Date.compare(&1, &2) != :lt)) # ascending
    Enum.sort_by(users, &(&1.birthday), &(Date.compare(&1, &2) != :gt)) # descending

Even a general sort in reverse required passing &>=/2 as an argument,
which is not clear in intent:

    Enum.sort(list, &>=/2) # descending

This PR addresses this by adding two changes:

  1. Sorting funtions now accept a module as sorter. The compare/2
     function of said modules are then used for sorting.

  2. `sort_reverse*` functions were added to complement the
     functionality above

All of the examples above can be written as:

    Enum.sort(dates, Date) # ascending
    Enum.sort_reverse(dates, Date) # descending
    Enum.sort_by(users, &(&1.birthday), Date) # ascending
    Enum.sort_reverse_by(users, &(&1.birthday), Date) # descending
    Enum.sort_reverse(list) # descending

The reason why we picked `sort_reverse` instead of `sort_descending`
because a sorting funtion can still be given to `sort_reverse`, which
means a descending function will return an ascending result (i.e. the
reverse).
@josevalim
Copy link
Member Author

The doctests are enough to provide full overage but I will also add some specific unit tests to the new functionality in the next hour or so. In any case, this is good for feedback.

@Qqwy
Copy link
Contributor

Qqwy commented Oct 18, 2019

Very interesting! I like this proposal; I think it is much clearer and cleaner than the earlier ideas that were floating around. 👍

@wojtekmach
Copy link
Member

@ericmj @josevalim there's a hiccup with Decimal in that we have:

iex> Decimal.cmp(1, 2) # :lt | :eq | :gt
:lt
iex> Decimal.compare(1, 2) # Decimal<-1> | Decimal<0> | Decimal<1>
#Decimal<-1>

Given compare/2 returning :lt | :eq | :gt is much more popular option, we could solve this issue by reversing the behaviour of functions in Decimal v2.0. (It's a separate decimal discussion but in v2.0 we could also require more recent Elixir version and thus drop some workarounds)

@josevalim
Copy link
Member Author

@wojtekmach my suggestion is to ship a Decimal version ASAP that deprecates the current use of compare.

@Eiji7
Copy link
Contributor

Eiji7 commented Oct 18, 2019

@josevalim Personally I do not like adding extra functions Enum.sort_reverse and Enum.sort_reverse_by. However I'm definitely 👍 for general idea.

Maybe it would be better if we would consider adding opts argument instead of creating more and more functions now and in future.

For example:

Enum.sort(list, Date, reverse: true)
Enum.sort_by(list, &(&1.birthday), Date, reverse: true)
# instead of
Enum.sort_reverse(list, Date)
Enum.sort_reverse_by(list, &(&1.birthday), Date)

Maybe further in 2.x.y version it could even look:

Enum.sort(list, reverse: true, sorter: Date)
Enum.sort_by(list, &(&1.birthday), reverse: true, sorter: Date)
# instead of
Enum.sort_reverse(list, Date)
Enum.sort_reverse_by(list, &(&1.birthday), Date)

Of course this version is not backwards compatible, but in my opinion it looks just better.

opts is much easier in some cases of building generic software. When receiving for example %{"order" => "asc"} parameters I would need to use apply/3 (or extra pattern matching) instead of simply passing boolean value into opts keyword.

defmodule Example do
  def sample(params) do
    list = get_list(params)
    order = params["order"] || "asc"
    Enum.sort_by(list, &(&1.birthday), Date, reverse: order != "asc")
  end

  # …
end

# instead of

defmodule Example do
  def sample(params) do
    list = get_list(params)
    order = params["order"] || "asc"
    name = if order == "asc", do: :sort_by, else: :sort_reverse_by
    apply(Enum, name, [list, &(&1.birthday), Date, reverse: order != "asc"])
  end

  # or
  def sample(params) do
    order = params["order"] || "asc"
    params |> get_list() |> do_sample(order)
  end

  defp do_sample(list, "asc"), do: Enum.sort_by(list, &(&1.birthday), Date)
  defp do_sample(list, "desc"), do: Enum.sort_reverse_by(list, &(&1.birthday), Date)

  # …
end

@josevalim
Copy link
Member Author

I see the point regarding options but nothing in Enum accepts options and composition of Enum functions is done by concatenating names, so I don't want to introduce a new precedent.

@devonestes
Copy link
Contributor

What about formalizing this as a Protocol? Something akin to Ruby’s Comperable? Then we can skip the option all together by implementing a new protocol, where Any falls back to something that returns the same values as Date.compare/2?

Also, I take it the reason for sort_reverse being implemented is performance? If it’s not faster, why add these functions instead of having the user sort and then reverse their list?

@josevalim
Copy link
Member Author

josevalim commented Oct 19, 2019 via email

@devonestes
Copy link
Contributor

Oh, yeah. And I guess if we change Enum.sort/1 to automatically use the implementation of that behaviour that would be a breaking change, so I guess that’s not an option right now, either (even though it’s most likely the behaviour that newcomers to the language would expect).

@josevalim
Copy link
Member Author

josevalim commented Oct 19, 2019 via email

@josevalim
Copy link
Member Author

Closing in favor of #9432.

@josevalim josevalim closed this Oct 20, 2019
@josevalim josevalim deleted the jv-enum-sort-revamp branch October 24, 2019 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

5 participants