Pitch: Scoped Functions

SwiftUI is built specifically in that way because the order in which modifiers are applied matters: setting a view's frame and then adding a border is different than adding a border first and then setting the frame. Scoped functions would unlikely change that.

5 Likes

SwiftUI is only one use-case for builders, so I think we do understand why SwiftUI is implemented that way, but it is currently the ONLY way to use Builders. You have to return something on every line, thus the only way to modify an object is through functions that either return self or return a new object.

What I mean is that I have built many SwiftUI like libraries that build interfaces but have functions that only return exactly self just so I can use 'declarative' syntax --- which is annoying. I just want to modify a property, and we have great syntax for that e.g. something.property = thing. We shouldn't need to write functions which only purpose is to set a property and return self just to use declarative syntax. To me, this proposal is way more desired than Builders, but regardless is an amazing compliment to Builders -- it allows us to mix the best of both worlds while keeping succinct syntax. It might also lead to solving some of the pitfalls of Builders especially when it comes to control flow -- for which Builders are not well equipped today.

I give this proposal 1000000+ points.

There was a very similar pitch back in 2019 Receiver Closures, which was also inspired by another Kotlin feature: Function literals with receiver
And I do hope that either one of these pitches will be considered and eventually implemented.

Perhaps the main point to address here is this:

My 2 cents here. Many (real-world) concepts and domains can be elegantly abstracted with a DSL. Having worked with Kotlin I've contributed to some DSLs that were really pleasant to use, which also could be understood by programmers and non-programmes alike, because of the familiarity with the domain.

This feature would be a niche, as it is in Kotlin, however it would allow library authors to provide very very convenient and elegant APIs.

Instead of the usual HTML example, I would like to show the API I'd like to provide for SwiftkubeModel for building Kubernetes API objects programmatically:

This just a simple example, check out this k8s-kotlin-dsl repository for a complete K8s DSL built with Kotlin using function receivers:

let nginx = deployment {
  metadata {
    name = "nginx"
    labels = ["app": "nginx"]
  }
  spec {
    replicas = 2
    selector {
      matchLabels(["app": "nginx"])
    }
    template {
      spec {
        containers {
          conrainer("nginx") {
            image = "nginx:latest"
          }
        }
      }
    }
  }
}

it is readable, type-safe, auto-completable, and above all it is familiar for everyone working with K8s.

4 Likes

Insouciant pitch: Why not use |. as the sigil?

// The build(_:configure:) function accepts a closure 
// scoped on its first argument:
let label = build(UILabel()) {
    |.text = "Hello scoped functions!"
    |.font = .preferredFont(forTextStyle: .body)
}

// The `filter(_:)` and `order(_:)` functions accept an 
// autoclosure scoped on its receiver type.
// `Player` collaborates, and has defined two
// `Player.score` and `Player.team` static constants.
let players = try Player
    .filter(|.team == "Red")
    .order(|.score.desc)
    .limit(10)
    .fetchAll(from: database)

// A mix and match between result builders and
// scoped functions. Each closure is scoped on an
// object that defines the relevant leading-dot apis 
// inside the body.
let html = HTML {
    |.head { |.title("Hello scoped functions!") }
    |.body { ... }
}

// A revisit of SE-0299: the argument of toggleStyle(_:) is
// an autoclosure scoped on an ad-hoc enum type:
Toggle("Wi-Fi", isOn: $isWiFiEnabled)
  .toggleStyle(|.switch)

@Jumhyn mentioned its hybrid nature, it made me think of a sigil "between" \. and ..

I agree that using a different sigil would be the best option. I'm not sold on the alternate sigil proposed ', though.

I wish that • was easy to type on Windows keyboards, I would have liked that to be the sigil that we used instead. A period like sigil feels right.

It's mostly already possible with some result builder trickery, but it doesn't look like an ergonomic solution.

build(UILabel()) {
    \.text <- "Hello scoped functions!"
    \.font <- .preferredFont(forTextStyle: .body)
}

// implementation is below

@resultBuilder
enum Builder<T> {
    static func buildBlock(_ items: T...) -> [T] { items }
    static func buildExpression(_ expression: T) -> T { expression }
}

struct Modification<T> {
    let perform: (inout T) -> Void
}

func build<T>(_ initialValue: T, @Builder<Modification<T>> modifications: () -> [Modification<T>]) -> T {
    modifications().reduce(into: initialValue, annotate)
}

func annotate<T>(value: inout T, with modification: Modification<T>) {
    modification.perform(&value)
}

infix operator <-

extension WritableKeyPath {
    static func <- (lhs: WritableKeyPath<Root, Value>, rhs: Value) -> Modification<Root> {
        Modification {
            $0[keyPath: lhs] = rhs
        }
    }
}

I continue to believe that this would be the clearest way to express scoped functions:

with(UILabel()) {
    text = "Hello scoped functions!"
    font = .preferredFont(forTextStyle: .body)
}
1 Like

Agreed that "without prefix" looks elegant.
And the point would be how variable shadowing should be treated I think, like mentioned in the first quote.

While those are simple and elegant, however, I wonder how those would be translated:

Something like into this?:

Galaxy.filter(isEmpty)
Galaxy.filter(!isEmpty)
Galaxy.filter(name == "Milky Way")
Galaxy.filter(radius >= 50000 && starCount >= 1.0e11)

  • With leading .
  • Direct access to property inside @scoped (no prefix)

In contrast to above two ideas, I believe it will be very hard to convince people to accept custom prefix that doesn't exist in Swift stdlib today, if this pitch becomes an actual proposal and went under review.

@gwendal.roue
Not sure if this will do something for the pich improvement though, found something interesting:

github.com/makeupstudio/swift-declarative-configuration
gist.github.com/maximkrouk/eede7171952e044492c1fa57291bcf94

Those basically allows you to do below:

struct Test {
    var title: String = ""
    var description: String = ""
    var date: Date = .init()
    var action: () -> Void = {}
}

func makeSecretTest() -> Test {
    Builder(Test())
        .title( "Secret test")
        .description("Nobody should know")
        .action { print("secret action") }
        .build()
}

let label = UILabel().builder
    .frame.size.width(100)
    .backgroundColor(.red)
    .textColor(.white)
    .text("Hello")
    .build()

This is interesting approach, though...
I support and encourage this Scoped pitch again as none of workarounds that can be built on top of what Swift have today is far from it :sweat_smile:

I had the same itch making predicates for database API, so I totally agree with de motivation.

Another exemple that comes to mind is a package manifest.

I think the leading dot would be unfortunate. I would lean toward no leading character at all.