elm-community / list-extra

Convenience functions for working with List.
http://package.elm-lang.org/packages/elm-community/list-extra/latest
MIT License
135 stars 59 forks source link

Proposal for `contains` function #35

Open stil4m opened 7 years ago

stil4m commented 7 years ago

Often I'll find myself doing someList |> List.filter (flip List.member myList). I assume that other people have this same construct to filter a list.

Would it be nice to have a function in this package that makes this code cleaner. Such as:

contains : List a -> a -> Bool
contains =
    flip List.member

Then the resulting code would be nicer and more concise:

import List.Extra as List

someList |> List.filter (List.contains myList)

I can make a PR on this if my suggestion some kind of approval.

witoldsz commented 7 years ago

I think that having two distinct functions doing literally the same but named after two synonyms (member and contains) could be a little bit confusing.

Maybe all we need is just a new function to suit your original problem:

{-| Creates a list composed of elements common to both provided lists. 
-}
intersection : List a -> List a -> List a

It would not hurt if there was also an:

intersectionBy: ((a, a) -> Bool) -> List a -> List a -> List a`
EverybodyKurts commented 7 years ago

Is there still a want/need for this? I could write this :).

prikhi commented 7 years ago

I think it makes sense for intersection/intersectionBy to exist.

No opinion on if intersectionBy should be ((a, a) -> Bool) -> List a -> List a -> List a, or if it should match the other "by" functions: (a -> comparable) -> List a -> List a -> List a

witoldsz commented 7 years ago

I would vote for: intersectionBy: (a -> a -> Bool) -> List a -> List a -> List a is better than: intersectionBy: ((a, a) -> Bool) -> List a -> List a -> List a

The latter seems unfriendly to function composition.

What would (a -> comparable) mean? I think, we either include an element or exclude it.

witoldsz commented 7 years ago

Maybe better name for: intersectionBy would be an intersectionWith?

I am looking at the RamdaJS http://ramdajs.com/docs/ library. All that functions and their names were carefully though out, in the past there were many incompatible changes adjusting to new functions and as a result of discussions on a mailing list, so maybe we could use their achievements?

P.S. There are other useful list functions there, maybe list-extra could implement some of them as well, like difference and differenceWith?

witoldsz commented 7 years ago

Actually I think it should be (a -> b -> Bool) -> List a -> List b -> List a , because there is no need for both lists to be of same type (but they still can).

Chadtech commented 7 years ago

I like intersectBy, but one problem I think with the name, is that it doesnt have anything to do with intersections any more. @witoldsz is right in pointing out that they two lists dont need to be the same type, but then, if they arent the same type, then they are necessarily not the same, and there cant be any intersection between them. Even if they are the same type, if you are using a a -> a -> Bool to determine if they will be present in the output List a, it doesnt follow that it will be on the basis of them being the same.

It seems like we've abstracted from @stil4m 's original use case (which I think has been fruitful). I've been in his exact use case, and I made myself a helper function called contains typed List a -> List a -> Bool.

What about this proposal?

intersection : List a -> List a -> List a

intersection [ 1, 2 ] [ 2, 3 ] == [ 2 ]

doIntersect : List a -> List a -> Bool
doIntersect list0 list1 =
    not <| List.isEmpty (intersection list0 list)

doIntersect [ 1, 2 ] [ 2, 3 ] == True
pzp1997 commented 7 years ago

Keep in mind that there already is a function for getting the intersection of two sets, namely Set.intersect. Does it make sense to duplicate this functionality for lists? In my opinion, it would just encourage people to use the wrong type of collection.

Another question is how would one even define intersection for lists. In terms of handling duplicate values, should intersection [ 1, 1 ] [ 1, 1 ] equal [ 1 ] or [ 1, 1 ]? Should a particular order for the return list be guaranteed, for example intersection [ 1, 2 ] [ 2, 1 ] could equal either [ 1, 2 ] or [ 2, 1 ]?

Chadtech commented 7 years ago

I am tempted to disagree with what you said about sets. I would think that one might want to preserve the order of a list, and get it's intersection with another list. But then that does force your question about what order the output would have. The order has to be based on either the first or second parameter. Since its based on one or the other, its more like a filter that uses one list on the other; not a true intersection.

So anyway, good points, I agree. That brings us back to the original problem, which I am not sure we've fully formed.

stil4m commented 7 years ago

I would think that one might want to preserve the order of a list, and get it's intersection with another list

@Chadtech That is exactly what the original ticket was for.

Chadtech commented 7 years ago

Right. I'm with ya.

I still feel good about my proposal, if we just change the names (since they arent true intersections). How about only and hasOnly? So..

only: List a -> List a -> List a
only list0 list1 =
    List.filter ((flip List.member) list1) list0

hasOnly : List a -> List a -> Bool
hasOnly list0 list1 =
    not <| List.isEmpty (only list0 list)
witoldsz commented 7 years ago

@pzp1997

Another question is how would one even define intersection for lists. In terms of handling duplicate values, should intersection [ 1, 1 ] [ 1, 1 ] equal [ 1 ] or [ 1, 1 ]?

Valid point! Intersection combines lists into a set. The function could be implemented, but it is out of scope here, as this is not what OP wanted.

About the only function… It takes list0, filters it leaving members of list1. Am I correct? If so, list0 should be the last argument, it's not a coincidence it's last argument of List.filter. So, it's basically a special variation on filter, so maybe the name should be something like filterMembers, filterEquals or the like? What do you think? My proposal is would be:

filterMembers members list =
List.filter ((flip List.member) members) list

or maybe:

filterMembers members list =
list |>
    List.filter (\item -> List.member item members)

About hasOnly: is there a way to stop at first not equal item?

pzp1997 commented 7 years ago

@Chadtech I like your proposal. My only thought (pun intended) is to switch the order of the lists passed to only to make partial application and composition easier. I think it is more common that you would have a bunch of lists that you want to limit to the same subset of values than that you want to limit a single list to multiple subsets of values (not sure if that makes sense in words, feel free to ask me for an example if it is not clear). Here's my proposed version of only.

only : List a -> List a -> List a
only list =
    List.filter <| (flip List.member) list

Regarding hasOnly, I am not sure if I correctly understand the purpose of the function. I was under the impression that it would determine if a list contains only a given subset of values and no other values. If that is the case, then I think your implementation might be slightly flawed. For example, hasOnlyChad [1, 3] [3] == True, yet the values in the first list are clearly not a subset of the values in the second list. Your implementation seems to determine if the first list contains any values from the second list. This also seems useful in some cases and could be what you intended, but I would say that hasOnly is not the most intuitive name for this behavior. Instead, I propose the following implementation for hasOnly.

hasOnly : List a -> List a -> Bool
hasOnly list =
    List.foldl (\x acc -> acc && List.member x list) True

EDIT Just read @witoldsz's comment and he seems to agree with me about the order of the arguments to only.

Chadtech commented 7 years ago

Parameter order

Okay I see what you both mean about the parameter order. Yeah, it should be flipped.

name

How about filterMember, in the singular? "Members" threw me for a loop, because it sounds like its referring to the members of the list. But if we refer to the function member it makes sense, because thats the basis of the fil. That said member is sa bad name imo, but I would rather be consistent with it.

hasOnly

In response to @witoldsz, Yeah I am sure there is a more efficient implementation.

Anyway @pzp1997, the purpose I had in mind was just to say if a list has any of the members from a different list. So, member tells you if the list has a certain element. hasOnly would tell if you it has any member from a given list. Maybe it needs a better name. Since we are leaning towards referencing member, a name like anyMember would work.

witoldsz commented 7 years ago

@pzp1997

only : List a -> List a -> List a
only list =
List.filter <| (flip List.member) list

I would suggest renaming the list to members, because it represents a list of members we are going to check against, the list to filter is to be provided.

@Chadtech

How about filterMember, in the singular?

I used plural, because we are checking against a list of members, not a single member.

pzp1997 commented 7 years ago

@Chadtech if I now understand anyMember correctly, it sounds it like it should be implemented as

anyMember list =
    List.any <| (flip List.member) list

This also made me realize that my version of hasOnly a.k.a. allMember should be implemented like this.

allMember list =
    List.all <| (flip List.member) list

And given the likely implementation of filterMember, it looks like we've got a pattern here.

I'm beginning to think that maybe the right idea is what was suggested in the original proposal. If we create an alias for flip List.member then people will be able to easily create predicates for all these functions and more. If we instead choose to make Member variants for filter, any, and all, what would be stopping us from doing the same for List.partition, List.Extra.takeWhile, List.Extra.dropWhile, List.Extra.filterNot, etc. for which there exist convincing cases for inclusion as well?

Sorry to turn around and change my opinion so suddenly. I have been thinking about this for a while, yet only realized this today. My "vote" is in favor of adding a function that aliases flip List.member perhaps called memberOf. I think that the name is different enough from List.member that people won't get confused. As a trivial example, this would look like

List.filter (List.Extra.memberOf [1, 4, 9, 16]) [4, 2, 1, 9, 9, 3] == [4,1,9,9]