isContained(in array:) extension of Equatable


(Harmen Warringa) #1

Consider the following code which checks if a variable x is equal to at least one of several values x1, x2, x3:

if (x == x1) || (x == x2) || (x == x3) {
// doSomething
}

Would it not be convenient if one could write:

if x.isContained(in: [x1, x2, x3]) {
// doSomething
}

Here the function isContained is defined as an extension of Equatable in the following way:

public extension Equatable {
    
    public func isContained(in array: [Self]) -> Bool {
        return array.contains(self)
    }
}

Especially when one would like to check if a nested variable is equal to at least one out of several values this extension saves a lot of code writing. Compare:

if (someClass.someStruct.someVariable == x1) || (someClass.someStruct.someVariable == x2) || (someClass.someStruct.someVariable == x3) { 
   // doSomething
}

To:

if someClass.someStruct.someVariable.isContained(in: [x1, x2, x3]) {
   // doSomething
}

Checking whether a variable equals at least one out of several values happens often and therefore it would be nice to make that as convenient as possible. The advantage of adding this small extension to the Swift language is I think that it will promote uniformity and discoverability.

What do you think?


(David Catmull) #2

What is the advantage of that over if [x1, x2, x3].contains(···) {}?


(Harmen Warringa) #3

Good question.

In my opinion x.isContained(in: [x1, x2, x3]) is clearer than if [x1, x2, x3].contains(x).

Also an advantage is that you can use the type inference.

So consider

enum SomeThing {
   case a
   case b
   case c
   case d
   case e
}

Then assume x has type Something.
If I now want to check if x is one of several cases I can write

if x.isContained(in: [.a, .b, .c]) {

}

With your suggestion you would end up with something that is slightly more complicated which will look like this:

if [SomeThing.a, SomeThing.b, SomeThing.c].contains(x) {
  // doSomething
}

(Adrian Zubarev) #4

If x is of the same type it’s inferred in the array literal and you don‘t need the explicit type there.

[.a, .b, .c].contains(x)

Is just fine.


(Harmen Warringa) #5

You are correct that compiles.

But while typing [.a, .b, .c] the editor does not know yet which type .a, .b and .c belong. So then you do not get the suggestions from the type inference.

If contrast with x.isContained(in: array) the editor knows that the elements of the array should be of the same type as x and can show you suggestions. This makes it easier to write the code in this order. You will probably also make fewer mistakes by being able to use the suggestions.


(Rod Brown) #6

I think there’s a clear anti pattern in this proposal - you’re requiring equatable to know about arrays. It seems inappropriate to mix these two types together like this. Equatable is a base protocol that knows nothing but about equatability - how is it relevant to it to know about arrays?

While I see the slight difficulty on type inference you’re mentioning, I think it’s far, far outweighed by the very concerning anti pattern here. Collection containment is a concern of the collection containing it, not the item being (potentially) contained. It’s backwards.


(Harmen Warringa) #7

Dear Rod. Thank you for your comments.

If you dislike the array part, what about a solution using a variable number of arguments:

public extension Equatable {
   public func isAtLeastOne(from variables: Self...) -> Bool {
      return variables.contains(self)
  }
}

So then you can do:

if x.isAtLeastOne(from: x1, x2, x3) { }

(Pierre Houston) #8

I'd like to see this:

if x in [x1, x2, x3] {
    // doSomething
}

(Happy Human Pointer) #9

Everyone has great ideas, but they are more on the sugar side.

@Harmen:

As @David_Catmull, @DevAndArtist, and @Rod_Brown said, the type inference flaw is not really A big issue. Equatable does not need to know about arrays: it should always be dependent only on == and nothing else. Would you like Equatable to not work if there is no Array?

@PierreH-GlanceTech

Swift is meant to be clean and readable but not to go overboard. The syntax at first seems confusing to a beginner, as there is slight confusion on what the code may mean. Is it really too much to do [x1, x2, x3] ~= x?
If you really wish to make a serious proposal involving this discussion, start a thread, and if approved I'm sure @John_McCall would be happy to help you.


(Harmen Warringa) #10

Dear Happy Human,

I agree with you that this proposal is only about syntatic sugar. But since it happens quite often that you have to check if a variable is one out of several options I thought it might be useful to make this somewhat simpler.

Of course I do not want to Equatable not to work if there is no Array. Only the proposed extension should then not work. But this seems a hypothetical situation to me since we know Array's exist.


(Happy Human Pointer) #11

@Harmen

I like how you address me as Happy Human as if that is my real name. You can just call me @SafelySwift

But this seems a hypothetical situation to me since we know Array 's exist.

Thats not the point. We do not want a "on its own" protocol to depend on anything other than its needs. All Equatable needs is a == function, and is not related to arrays in any way. If you look at the WWDC 2018 video "Embracing Algorithms", Crusty keeps berating the speaker as he makes his extension more generic and dependent on the bare minimum to make the function work. Does Equatable need Array to work? No! Then Equatable should be able to work without Array (logic).

On a side note, If you want to use it, just add a simple extension to Equatable in your project. It is not that difficult, and in my opinion not worth the addition to the standard library.


(Rod Brown) #12

Hi Harmen,

The concern regarding array is not so much whether they do co-exist, but what an appropriate separation of concerns. By requiring types to know about each other, things can become a tangled mess of dependencies. By simplifying types to appropriate extensions, you create clean, understandable requirements.

I find your variable parameter solution an interesting idea, and would be a better solution than designing around array.

That said, I’m not sure the value is so great that it would be a good fit for the standard library. You can definitely use this in an extension yourself, thought!

I think if you need to match multiple cases, you could always use a switch, or an if block with ors?

switch x {
     case .a, .b, .c:
           // do something here
     default:
           break
}

(Harmen Warringa) #13

Dear @SafelySwift,

Thank you for your explanation. I understand your concern and I agree that it makes sense. What about my second idea, using variable arguments described above? Then there is no dependency on array anymore.

if x.isAtLeastOne(from: x1, x2, x3, ....) { }

As you say I can easily add this extension to my projects myself, I already use them for a long time. I just thought it was nice little thing to share. The advantage of adding it to the standard library is that it promotes uniformity and allows for discoverability. I think it might be convenient to the Swift users to be able to simplify the common statement:

if (x==x1) || (x==x2) || (x==x3) || .... { }


(Harmen Warringa) #14

Hi Rod,

Indeed using a switch you can also achieve what I want, but I think that
if x.isAtLeastOne(from: x1, x2, x3) { } is clearer and more convenient to use.


(Adrian Zubarev) #15

Well the bar for new API surface in the stdlib is vey high now, so let us rephrase the question:

What new algorithms does your method allow us to express?


(Harmen Warringa) #16

For example the method can be useful for filtering a collection with respect to nested variables.

Let us assume that that SomeStruct has the variable x and that myArray is an array of SomeStruct. Then if we want to filter myArray based whether the value of x is x1 or x2 or x3 we can do:

let filtered = myArray.filter { $0.x.isAtLeastOne(from: x1, x2, x3) }

We can also use this method to check if all elements of a collection have the desired values of x.

let isUseful = myArray.allSatisfy { $0.x.isAtLeastOne(from: x1, x2, x3) }

(Adrian Zubarev) #17

Here is my answer.

let filtered = myArray.filter { 
  [x1, x2, x3].contains($0.x)
}

let isUseful = myArray.allSatisfy { 
  [x1, x2, x3].contains($0.x) 
}

How is your solution something new? You also have to type more then I just showed you.

Your new method is only flipping the sides and limiting the scalability of the well established contains method. You only permit an array while in reality you could also have dictionaries or even sets. For that reason alone I‘m -1 on the pitch, since I personally think it would be a regression rather than an improvement.


(Harmen Warringa) #18

Ok that works as well, but in my suggestion the type inference can help you while typing as explained above. The naming isAtLeastOne(from: is only a suggestion and might be improved.


(Adrian Zubarev) #19

Well then I don't understand the issue. As a daily Swift developer I know the existence of contains and I know that it's available on a collection type which implies that the compared value will be written on the rhs. That said it's logically that the editor cannot do much analysis about the collection type you create from a literal if you want to omit the explicit type until it can infer the Element type backwards from one of the collection members such as contains. In that sense I don't think it's the language's responsibility to improve editors in that way by only flipping lhs with rhs of an expression nor is it the editors duty to reason about every type at the time you type it, because it's logically constrained.


To be truly fair with you, I'm not trying to sabotage the success of this or any other thread, it's just my honest opinion I share with you.


(Pierpaolo Frasa) #20

From a purely linguistic point of view, if x.isContained(in: y) or if x in y is more natural to me than y.contains(x), if x is currently the topic (i.e. the variable I'm interested in). For example, one might express business logic in terms of "if this user belongs to the set of privileged users" for which if user.isContained(in: privilegedUsers) seems a closer translation than if privilegedUsers.contains(user).

That said, programming languages are not natural languages and if it's too prohibitive to add natural-language-likeness or it would make the computational model significantly worse (natural languages are wonderfully messy as opposed to programming languages), that might not be worth it.