Extend pattern matching to Array and ArraySlice

context: ancient discussion

here’s a really common pattern:

if  let item:Item = items.first,
    items.index(after: items.startIndex) == items.endIndex
{
    ...
}

what if we could special-case Array and ArraySlice to support

if  case [let item] = items
{
    ...
}

?

1 Like

An alternative is:

extension Collection {
    var onlyElement: Element? {
        if let element = self.first,
            self.index(after: self.startIndex) == self.endIndex {
            element
        } else {
            nil
        }
    }
}
if let item = items.onlyElement {
    …
}

I think that reads better, in that specific case. It's more explicit and straightforward.

That said, for the situations where count is more than one, I see merit in pattern matching support, or destructing a la tuples. e.g.:

if let (first, second) = items {
    …
}

P.S. I had to check the Collection documentation to confirm that count is not guaranteed to be O(1). Re. your method for efficiently checking if the collection has one element. Which reiterates the need for of a contains(atLeast: Int) method for Collection.

2 Likes

sure. but you would have to duplicate that extension in every module that requires it, to avoid running afoul the “don’t (publicly and generically) extend types you don’t own” principle.

I was implying this should be added to the Swift stdlib.

But granted if we get destructing or somesuch instead, that suffices as a functional superset.

2 Likes

IIRC it goes "don’t extend types you don’t own with protocols you don't own". The first part alone is fine as it is pretty much equivalent to having a standalone function:

func myFunction<C: Collection>(_ collection: C) -> C.Element? {
}
5 Likes

Notably the complexities of first, startIndex, endIndex and self.index(after:) are not specified. :thinking:

i think it’s reasonable to assume those are all expected to be implemented in O(1).

“dont (...) extend types you don’t own” isn’t an axiom, it’s a consequence of applying the what if everyone did this? test.

if two people had the same global function idea, you can still disambiguate by qualified reference. the same is not true for colliding extensions.

1 Like

C# has a pair of functions like this via LINQ: in Swift they would be consuming func single() throws -> Element and consuming func singleOrDefault() -> Element?

2 Likes

F# can match lists (or generally any iterable collection as far as I am aware) with 2 cases:

  • exact number of items (e.g. [] if matching with an empty list or [first; second] if matching with exactly two items in the collection, etc.)
  • at least one (e.g head::tail and the tail can be an empty collection)

This approach is especially useful when used in recursive functions.

edit: I made an error in the last case. Now fixed and refactored.

2 Likes

I’m not very aware of this principle. I personally have Collection.only defined in a package I own and I use it all the time. I can imagine some possible arguments for the principle you mention, but can you tell me a bit about it? Do you have any link to discussion about it?

what if you start a second package, and that package also has a Collection.only extension? then anyone (including future you) who wants to depend on modules from both packages simultaneously will be unable to use either helper method.

Firstly, plan A would be to not define Collection.only in any other package - anywhere I need Collection.only I'll depend on the one package in which it is defined.

But worst comes to worst, the situation you describe is solvable:

Package C wants to depend on package A and package B, both of which define Collection.only. I can create a module named disambiguation-Collection-only inside of the C package and only import either A or B, then disambiguate using the following code:

import A

extension Collection {
    public var onlyElement: Element? {
        self.only
    }
}

and then import disambiguation-Collection-only in the rest of the modules of my package where it's needed.

Could it work like so:

// file1.swift
import A
collection.only // ✅ uses A.collection.only

// file2.swift
import B
collection.only // ✅ uses B.collection.only

// file3.swift
import A
import B
collection.only // 🛑 ambiguous use of collection "only" (defined in both A and B)

// file4.swift
// no import A or B
collection.only // 🛑 collection "only" is defined in more than one module, specify the one you want
1 Like

Oh, right - I’m so used to @_exported imports that I forgot that a separate module for the purpose of disambiguation wouldn’t even be necessary, a separate file would suffice.

I mean I'm not sure if it works like this or not today, it just feels it would be the best way to solve the issue raised by @taylorswift. Does it actually work like this today?

Just tested it to be sure - indeed, package C can depend on both A and B, and the below compiles successfully:

// Package C
// File1.swift
import A
import B

func demo (c: some Collection<Int>) {
    let test = c.onlyElement
}
// Package C
// File2.swift
import A
extension Collection {
    var onlyElement: Element? {
        self.only
    }
}
1 Like

Which is a problem @taylorswift is talking about, no? Which onlyElement is used here? :thinking: I'd prefer to have a compilation error in this case. Ditto when there is no explicit "import A" or "import B".

If "only" here is always A's and never B's - that's good.

I’d love to see this, but it would need to work with future destructuring and pattern matching of structured types (class, struct, dict). The syntax proposed previously resembled closure capture lists.