How unique/useful is this twist on contains(), is there a better way to do this?

I defined an onlyIfNotIn function for this one use case I had where I wanted to use contains() more naturally and in a chained expression:

let foos = things.compactMap({ $0.foo?.onlyIfNotIn([a,b,c]) })

And then went ahead to complete the extension with the 4 functions: ifIn, ifnotIn, onlyIfIn, onlyIfNotIn. To be honest, they're based on an isIn implementation I saw in this swift forums post, before which I was having trouble getting it right.

More examples:

if n.isIn(1..<10) { print(n) }
let namesOfAllowed = elements.compactMap({$0.onlyIfIn(allowed)?.name})

Implemented like this:

public extension Equatable {
  func isIn<S: Sequence>(_ s: S) -> Bool where S.Element == Self {
    return s.contains(self)
  }
  func isNotIn<S: Sequence>(_ s: S) -> Bool where S.Element == Self {
    return !s.contains(self)
  }
  func onlyIfIn<S: Sequence>(_ s: S) -> Self? where S.Element == Self {
    return s.contains(self) ? self : nil
  }
  func onlyIfNotIn<S: Sequence>(_ s: S) -> Self? where S.Element == Self {
    return s.contains(self) ? nil : self
  }
}

I omitted for brevity overloads taking variadic Self... in place of sequences, and ones that permit the sequence elements/variadics to be optionals.

See https://gist.github.com/jpmhouston/72eb8e6681b7cc2499ca1c350b5f0c09

Questions:

Is there another way to skin this cat[1], something already commonly used? Rather, if this is unique and useful should it be pitched for the standard library? Do variants using async sequence make sense?


  1. no cats were harmed during this coding exercise ↩︎

I think that it’s mostly considered as confusing naming with "not" part in it. onlyIfNotIn puts too much cognitive load compared to !contains(…)

To change this from taking an allowed set to a forbidden set:

let namesOfAllowed = elements.compactMap({$0.onlyIfIn(allowed)?.name})
let namesOfAllowed2 = elements.compactMap({$0.onlyIfNotIn(forbidden)?.name})

unless I'm out to lunch there's no place to simply insert a negation (a !), but instead would need to make the expression much more complex.

(i had this example wrong in the original post. will correct there too)

So I don't think there's any other way than to have a negated variant of the function. If they were named includedIn and excludedFrom would that an improvement?

let namesOfAllowed = elements.compactMap({$0.includedIn(allowed)?.name})
let namesOfAllowed2 = elements.compactMap({$0.excludedFrom(forbidden)?.name})

// indeed isIncludedIn can be negated, so there's no need for isExcludedFrom()
if !n.isIncludedIn(1..<10) { print(n) }

You can express this in a different way

let namesOfAllowed2 = elements
    .filter { !$0.contains(forbidden) }
    .map { $0.name }

And in case that is a large array — add .lazy.

I think the word “unless” is more understandable:

let foos = things.compactMap { $0.foo?.unlessIn([a, b, c]) }

However, I’d probably formulate it like this:

let foos: Array = things.lazy
    .compactMap { $0.foo }
    .filter { ![a, b, c].contains($0) }

The explicit Array type declaration forces Swift to choose the filter overload that returns an Array instead of a lazy sequence.

2 Likes

Yes of course, .filter { !forbidden.contains($0) } was the other way I was looking for.

Thanks @mayoff, unlessIn is indeed a lot better :man_facepalming:. Although I may now be more fond of isIncludedIn / includedIn / excludedFrom. Ok, worth keeping to myself (and my gist) but that's about it.

Revisiting this to put my gist to bed at a time when I'm not operating on no sleep, I still see that a reversed contains is eminently desirable if testing only one element in a chain of expressions, ie. not being able to rewrite sequence.compactMap that uses $0.excludedFrom… as sequence.filter

Say something like:

bananas.first?.excludedFrom(forbidden)?.name ?? "forbidden banana"

With only a contained function on the sequence and no chainable contained-by function on the element (what I'm now calling includedIn / excludedFrom) then that has to be written like this maybe:

bananas.first.flatMap({ forbidden.contains($0) ? nil:$0 })?.name ?? "forbidden banana"

Or am I out to lunch about this too?

Has such a inverse-contains function already been pitched and rejected? If no, does anyone want to help me pitch this addition to the the standard library?