Closure return value seemingly affecting execution of unrelated expressions

Hi all,

I'm running in to a really strange issue using filtering on a collection. It seems that its code execution within a closure is dependent on the return value of the closure.

I am writing a script to iterate through a directory of image files which are named frame-0.png, frame-1.png, up to frame-139.png. I am trying to remove the .DS_Store file, then sort the remaining strings the number in the file name rather than alphabetically.

To remove .DS_Store. I added .drop { $0.hasPrefix(".") } before the sort operation. I noticed that the sort function was still operating on a collection containing .DS_Store. I did a sanity check in a Playground and .drop { $0.hasPrefix(".") } worked as expected on a smaller array.

Here is the function:

func getInputFilePaths(_ directory: String) -> [String]? {
    guard let directoryURL = URL(string: directory),
          let fileNames = try? FileManager.default.contentsOfDirectory(atPath: directory) else {
        print("Failed to find contents for directory \(directory)")
        return nil
    }
    
    return fileNames
        .drop { $0.hasPrefix(".") }
        .sorted { lhs, rhs in
            guard let leftFileNumStr = lhs.split(separator: ".").first?.split(separator: "-").last,
               let rightFileNumStr = rhs.split(separator: ".").first?.split(separator: "-").last,
               let leftFileNum = Int(leftFileNumStr),
               let rightFileNum = Int(rightFileNumStr) else {
                print("invalid fileName: \"\(lhs)\" or \"\(rhs)\". Expected format: [name]-[integer].[extension]")
                exit(1)
            }
            return leftFileNum < rightFileNum
        }
        .map { directoryURL.appendingPathComponent($0).absoluteString }
}

I rewrote the drop closure in order to step through it, placing a breakpoint within an if block. For whatever reason, the breakpoint was not being hit, even though I knew .DS_Store was in the array.

.drop {
             if $0.hasPrefix(".") {
                 return true // breakpoint
             }
             return false 
        }

Then I tried flipping the return value at the bottom. In this case, the breakpoint WAS hit. I am not sure why a separate expression in the closure would affect the return value of hasPrefix(".")

.drop {
             if $0.hasPrefix(".") {
                 return true // breakpoint
             }
             return true 
        }

I am perplexed by this, and am able to reproduce it consistently. Here is a GitHub repo with the entire code. I'm not sure what's causing the issue. I could find alternate ways to remove the unwanted string, but the behavior I am experiencing seems worth raising. It smells like something deeper than user error, so I decided to bring it to the Swift forums rather than Apple Developer forums.

2 Likes

drop skips the initial consecutive elements. You want filter.

2 Likes

Your issue is coming down to the order of the files. drop(while:) will drop the first k elements that satisfy the predicate, but then once an element doesn't satisfy the predicate it will return the rest. It isn't a drop(all:). You want to filter out the files you don't want.

let files = [".DS_Store", "file-1.png", "file-2.png"]
let files2 = ["file-1.png", ".DS_Store", "file-2.png"]

print(files.drop(while: { $0.hasPrefix(".") })) // ["file-1.png", "file-2.png"]
print(files2.drop(while: { $0.hasPrefix(".") })) // ["file-1.png", ".DS_Store", "file-2.png"]

print(files2.filter({ !$0.hasPrefix(".") })) // ["file-1.png", "file-2.png"]
2 Likes

I see, thank you both! I misunderstood the documentation and thought it was simply a sort of inverted filter()

2 Likes

It would be more obvious if the method was called dropFirst(while:), besides we could have had "dropLast(while:)".

1 Like