Can I make certain functions available within a closure arg?

I've been using Kotlin lately. It has a feature that allows you define functions that take closure arguments that have a specifically defined scope. For example:

 buildSqlTable(...) {
    column("name", .string)
    primaryKey(...)
 }

In that example, the trailing closure arg to buildSqlTable as a scope in which the functions column and primaryKey are defined. Those functions are not in the outer scope.

I know Swift doesn't have exactly this feature, but is there a way to achieve a similar thing? In other words, can you define column and primaryKey in a way that they do not pollute the global scope, but are available in the body of the trailing closure argument? Perhaps using the new function builders features?

Hello, your answer is in your question. Swift does not allow to switch scopes.

Result builders (formerly function builders) do not change this: SwiftUI works because import SwiftUI puts Text, Image etc in the global scope.


(Personal opinion on the topic follows)

Because they live in the global scope, SwiftUI types become "reserved": Text or Image are not only defined inside the body of view builders, and you can't use those identifiers for other purposes. If we were to design a result builder for SQL tables, we'd have to wonder if it is desirable to "lock" the Column and PrimaryKey types for table definition. For example:

import SwiftSQL

buildSqlTable(...) {
    // In the table builder scope
    Column("name", .string)
    PrimaryKey(...)
}

// Out of the table builder scope
Column(...)     // the same Column
PrimaryKey(...) // the same PrimaryKey

An SQL DSL usually has two facets, schema definition (CREATE ...), and query generation (SELECT ..., INSERT...). They share some concepts: "table", "column", "primary key", etc. It is not always true that a single type such as Column can fulfill the needs of both facets. Given that the schema definition API is usually much less used, in an application, than the query generation API, is it desirable to lock Column for schema definition, making it unavailable for query generation? Your call.

Faced with the same question, I decided that the schema definition DSL of GRDB would not hinder the query generation api, and would not use the same types. It uses dedicated types named like ColumnDefinition. Those types are public, but they would not look good in user's code. So it eventually looks like:

try db.create(table: "player") { t in
    t.autoIncrementedPrimaryKey("id")
    t.column("name", .text).notNull()
    t.column("score", .text).notNull().defaults(to: 0)
    t.column("teamID", .integer)
        .notNull()
        .indexed()
        .references("team", onDelete: .cascade)
}

The t. prefix is a very small visual pollution. Some will prefer $0. That's the best I could come up with in the current state of Swift.

2 Likes

Another inspiration is the Swift Package Manager API.

Okay, thank you for the explanation / confirmation. I wonder if Swift will eventually end up with some kind of scope control feature. It seems super useful in Kotlin. I'm using it a lot in Jetpack Compose too (the Android equivalent of SwiftUI).

Me too. I also think it could greatly enhance some APIs. We need a good pitch first :wink:

You can also use result builders along with leading dot syntax to achieve something very close to the scope control you're looking for:

struct SpecialMembers {
    static func doSomething(_ arg: Int) -> Self { return SpecialMembers() }
}

@resultBuilder
struct Builder {
    static func buildExpression<T>(_ t: T) -> T { t }
    static func buildExpression(_ expression: SpecialMembers) -> SpecialMembers { return expression }
    static func buildBlock(_ exprs: Any...) -> Void {}
}

func takesBuilder(@Builder _: () -> Void) {}

func doSomethingElse() {}

takesBuilder {
    .doSomething(0)
    doSomethingElse()
}

FWIW, I think I actually prefer this approach to something like implicitly assigned scopes. The leading dot is at least a small indication that we are doing something weird with that member.

3 Likes

Wow, thanks for the technique!

There are caveats to this approach, such as the fact that you can only use .doSomething(...) at the top level, so it's definitely not an equivalent solution for all use cases. But, as I mentioned, I think some of these restrictions actually assist with clarity.

ETA: also, since you’re not operating within an instance scope you’ll also obviously have to follow typical result builder patterns and “collect” all your intermediate results into a final result with your static buildBlock or buildFinalResult methods.

1 Like