Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Access.filter/1 #6634

Merged
merged 8 commits into from
Oct 5, 2017
Merged

Implement Access.filter/1 #6634

merged 8 commits into from
Oct 5, 2017

Conversation

zachdaniel
Copy link
Contributor

@zachdaniel zachdaniel commented Oct 4, 2017

Implements Access.find/1 and Access.filter/1. Examples and documentation provided in the code (aligned with examples and documentation for Access.at/1.

@zachdaniel zachdaniel changed the title added find and where Added Access.find/1 and Access.where/1 Oct 4, 2017
@zachdaniel zachdaniel changed the title Added Access.find/1 and Access.where/1 Implement Access.find/1 and Access.where/1 Oct 4, 2017
@zachdaniel zachdaniel changed the title Implement Access.find/1 and Access.where/1 Implement Access.find/1 and Access.filter/1 Oct 4, 2017
@crestenstclair
Copy link

This looks great, and extremely useful.

@zachdaniel
Copy link
Contributor Author

I checked on PR #6364 and saw that it restructures documentation, so I haven't touched the main documentation, only the documentation specific to the functions I'm adding.

@@ -700,4 +700,158 @@ defmodule Access do
defp get_and_update_at([], _index, _next, updates) do
{nil, :lists.reverse(updates)}
end

@doc ~S"""
Returns a function that accesses all elements of a list that match the provided predicate.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole docs should be two spaces.


## Examples

iex> list = [%{name: "john", salary: 10}, %{name: "mary", salary: 20}, %{name: "francine", salary: 30}]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably do it with a list of two to simplify examples.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up going with a list of 3 items to show order preservation, and specifically with filter to show how order is preserved on both sides. I don't mind changing it though, if you feel strongly about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could also write a non-example test to confirm that, and use the simpler example?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could also write a non-example test to confirm that, and use the simpler example?

Perfect.

iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
** (RuntimeError) Access.filter/1 expected a list, got: %{}
"""
def filter(func) when is_function(func, 1) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to not check the arity because that leads to a better error later on the invocation site.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll change now.

@josevalim
Copy link
Member

Thank you @zachdaniel! I like filter a lot but i am not convinced on find since find can be seen as a equivalent to filter on all cases where the finding criteria is unique. So I would like to merge filter and we can wait until folks request something like find. Wdyt?

@zachdaniel
Copy link
Contributor Author

@josevalim I agree. I really use Access.filter/1 often, and I added Access.find/1 just in addition while I was at it. It could also be easily simulated w/ get_in(list, [Access.filter(pred), Access.at(0)])

@zachdaniel
Copy link
Contributor Author

Although on large lists, it would perform much worse. Either way, I'm on board to remove find

raise "Access.filter/1 expected a list, got: #{inspect data}"
end

defp get_and_update_where([head | rest], func, next, updates, gets) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's rename where to filter here and below then please? thanks!

@zachdaniel
Copy link
Contributor Author

Sounds good. I'll make the following changes when I get home tonight:

  • Rename get_and_update_where to get_and_update_filter
  • Simplify examples and add more complex supporting tests.

@zachdaniel
Copy link
Contributor Author

@josevalim The above items are complete.

@zachdaniel zachdaniel changed the title Implement Access.find/1 and Access.filter/1 Implement Access.filter/1 Oct 5, 2017
@test_list [1, 2, 3, 4, 5, 6]

test "filters in get_in" do
assert(get_in(@test_list, [Access.filter(&(&1 > 3))]) == [4, 5, 6])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please remove the parens around assert on this test and the ones below?


test "chains with other access functions" do
mixed_map_and_list = %{foo: Enum.map(@test_list, &(%{value: &1}))}
assert(get_in(mixed_map_and_list, [:foo, Access.filter(&(&1 <= 3)), :value]) == [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is a bit confusing. Maybe it would be better to filter based on is_tuple/1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure exactly what you mean. None of the elements of the list are tuples. get_and_update_in returns a tuples of the get value and update value though. I think using the @test_list module attribute is just less clear,and maybe it would be clearer with an explicit test list in each test.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your filter is comparing a map with <= 3. If you want to reject everything, it would be better to have a filter based on the data structure.

Copy link
Member

@ericmj ericmj Oct 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, is_tuple was wrong, is_map is probably better. You do &(&1 <= 3) where &1 is a map which doesn't really make sense. Is %{value: 1} <= 3 the comparison you wanted to make? It would be better if you changed it to a more natural comparison.

Copy link
Contributor Author

@zachdaniel zachdaniel Oct 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sorry it was supposed to be &1.value <= 3 I'm pretty sure the test should be failing with that mistake though.

Copy link
Member

@ericmj ericmj Oct 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't fail because all terms are comparable, even across different types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's just wrong all around. Shouldn't be asserting an empty list. I'll fix now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I said it should fail I was thinking that I had the right side of the comparison right [1, 2, 3]

@zachdaniel
Copy link
Contributor Author

@josevalim @ericmj Those two issues are addressed. I didn't simplify the test really, but just fixed it to use &1.value

@josevalim josevalim merged commit 3dca96f into elixir-lang:master Oct 5, 2017
@josevalim
Copy link
Member

❤️ 💚 💙 💛 💜

@albertored
Copy link
Contributor

albertored commented Aug 1, 2018

@josevalim @zachdaniel (sorry for explicit mention but I don't know if you receive notification for closed PR)

I really would like to have also Access.find/1. Not only for performance reason but also because it can not be replaced with [Access.filter(pred), Access.at(0)], see for example

iex> data = %{a: [%{id: 1, age: 4}, %{id: 2, age: 5}]}

iex> get_in(data, [:a, Access.find(fn el -> el.id == 1 end)])
%{age: 4, id: 1}

iex> get_in(data, [:a, Access.filter(fn el -> el.id == 1 end)])
[%{age: 4, id: 1}]

 iex> get_in(data, [:a, Access.filter(fn el -> el.id == 1 end), Access.at(0)])
** (RuntimeError) Access.at/1 expected a list, got: %{age: 4, id: 1}

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.

None yet

5 participants