Swift Predicates 'disappear' in collection function parameter

I've created a modern version of NSPredicateEditor that lets me construct complex searches of model objects. Each row becomes a Predicate and I then pass an array of these Predicates to a function that translates them into SQL++ database WHERE clauses.

BUT, the array of predicates that I pass magically vanishes. Here's the signature of the function I'm calling, which is generic on the type of model object being fetched:

public func liveResults<T: Couchable>(_ objectType: T.Type, matching junctionType: PredicateJunctionType, ofPredicates predicates: [Predicate<T>], sortedBy sorter: (any SortComparator<T>)? = nil) -> LiveResults<T>?

But when I call this method and inspect in the debugger, here's what the predicates value is: [Foundation.Predicate<Pack{τ_0_0}>]

At the site where I call this function, I can see a well-formed array of Foundation.Predicate. I have absolutely no idea what τ_0_0 is. There are no errors; the function runs just as if it had an empty array for predicates.

Can anyone point me in a direction to debug this? If I pass a single predicate from the array, that makes it through just fine.

Why Not Just Combine The Predicates?

Because the compiler falls over. With more than just a handful of the buildExpressions functions, the compiler can't type-check in reasonable time.

The only workaround I've currently found is to:

  1. Limit the number of rows my PredicateEditor can have to, say, 10.

  2. Use the ol' SwiftUI approach where we have 10 different versions of the function that currently takes an [Predicate<Foo>]. Each version has 1 to 10 different predicate parameters.

This works but it's also objectively awful.

Passing inside View works

Here's the View that contains my PredicateEditor:

struct MasterDatabaseSearchPane: View
{
    @Environment(AppController.self) private var appController: AppController
    @Binding var viewModel: MasterDatabaseWindowViewModel

    @State private var beginValidation: Bool = false
        
    
    var body: some View
    {
        VStack
        {
            PredicateEditor<MasterCue>(beginValidation: $beginValidation, searchAction: { predicates in
                print("Start Search called with \(predicates.count) predicates: \(predicates)")
                viewModel.liveCues = appController.modelController.mainContext?.liveResults(MasterCue.self, matching: .conjunction, ofPredicates: predicates, sortedBy: viewModel.cuesSortOrder.first ?? KeyPathComparator(\MasterCue.creationDate, order: .reverse))
            })
            
            Button("Search Master Cues") {
                beginValidation = true
            }
        }
        .padding()
    }
}

PredicateEditor listens to beginValidation and, when it becomes true, the editor walks each row, turns that row's conditions into a Predicate<MasterCue> and then calls the searchAction closure with that array. The array of Predicates makes it to the closure just fine. But the ensuing call out to the method on modelController.mainContext happens and in that method I get the weird [Foundation.Predicate<Pack{τ_0_0}>] for the predicates parameter value.

Everything here is bound to the mainActor. I'm on Xcode 16.3 in Swift 6 language mode targeting macOS 14.6+

Not an expert on the matter, but τ_0_0 looks to me like the compiler is leaking implementation details here.

This post appears to clarify what the zeroes represent (and that τ_0_0 represents the top-most generic parameter).

2 Likes

Thanks! That post was illuminating.

Unfortunately, I haven't been able to find the problem with Predicate. I was considering dropping the generics and writing a version that worked with just MasterCue (the type of model object I'm searching with predicates), but that would be a lot of surgery at this point and I'm not sure it would resolve whatever this is.

The Workaround

Instead, I realized I don't actually need to pass the array of Predicate<MasterCue> out of the View. All I do with them is convert them to a SQL++ WHERE clause and while I'd like to do that inside of my database abstraction layer because that's better encapsulation, I can just do it in the View itself.

So it ends up as:

PredicateEditor<MasterCue>(beginValidation: $beginValidation, searchAction: { predicates in
    
    var clauses: [String] = []
    for predicate: Predicate<MasterCue> in predicates
    {
        guard let clause: String = predicate.couchbaseQuery() else {
            // Show alert that the search conditions could not be transformed to a SQL++ query
        }
        clauses.append(clause)
    }

    // Call appController.modelController.mainContext method with `clauses` instead of `predicates`.    
})

Predicate has been quite a battle to use. Were I to do this again from scratch, I'd probably sacrifice the type-safe KeyPaths and use NSPredicate instead.

1 Like