Declaration-Like Argument Blocks

Actually, I can answer my own question:

var x: T = &y

is sugar for

var x: T { get { y } set(v) { y = v } }

That seems like it’s a straightforward way to make “property aliases”.

I’m failing to see the usage of this as DSLs and I personally don’t think it solves the complains people have with trailing closures. (I also think that those complains are overrated ^^)
But what I’m most concerned about is how a calling site looks like a class declaration when is unrelated.
I’m probably missing the point here so I’m, it’s just that reading the post strikes me as something Would confuse me a lot if it was in the language.

2 Likes

The point is precisely that it looks like a class/struct declaration, the idea being that those declarations aren’t all that different from call sites anyway. You could view them as instances of DLABs with special compiler support.

Part of what I want to get as feedback here is “is this how people think about Swift programs?” (That declarations are just special-cased call sites and so making other call sites look more like them increases uniformity and clarity.) Your reaction is valuable because it tells me that that’s not how you think—in which case, I’m interested in what your mental model for classes and structs actually is.

2 Likes

Thanks for the kind words. I have to admit I have 0 experience with languages that support anything like this, which I guess is why it was so surprising and mind-bending to me at first.

in which case, I’m interested in what your mental model for classes and structs actually is .

It will be hard for me to articulate but I guess it boils down to declaration side vs. usage side. When I'm calling a function I'm using some construct that already exists and it just expect that I pass some data to it. When I'm declaring a class I'm specifying a template for new constructs. In my brain those are very different things, but again is probably just because the baggage that I have.

I would keep an eye on this thread with an open mind ^^

1 Like

Out of all of this, the one thing I like is the where keyword being used to denote something different about the following "closure". If SE-0279 had been proposed with that keyword, I would've accepted it.

I also find something compelling in here. Especially in the where keyword.

For me the matching of member names to argument names is a little too magical, but I think you could leverage the where keyword without that. What if a where keyword and its associated anonymous struct just added declaratively to whatever line or closure it followed?

Suppose you wanted to calculate π via its continued fraction. It's not an especially efficient way to go about it, but let's say you do. You have access to a function that takes the generating functions of the continue fraction coefficients as arguments, but using this function for π makes for a long line and my eyes start to have to work hard to match parens and brackets:

let π = continued_fraction(b0: 3,
                            a: { pow(Double(2 * $0 - 1),2) }, 
                            b: { _ in 6 })

If we had access to a where keyword we could pull out the generating functions, ai and bi:

let π = continued_fraction(b0: 3, a: ai, b: bi) where {
    func ai(i: Int) -> Double { pow(Double(2 * i - 1),2) }
    func bi(i: Int) -> Double { 6 }
}

If the type checker knew the signature of ai and bi inside the where block you could potentially save some more typing as well.

Conversely, if you were doing something simpler like the square root of two you might not bother:

let sqrt2 = continued_fraction(b0: 1, a: { _ in 1 }, b: { _ in 2 })

I also like that it would let you pull out a lengthy closure that is not the last closure. Consider the continued fraction approximation of the regularized incomplete beta function:

let cf = continued_fraction(b0: 0, a: ai, b: { _ in 1 })
where {
    func ai(i: Int) -> Double {
        let mInt = (i - 1) / 2
        let m = Double(mInt)
        switch (i - 1) {
        case 0:
            return 1
        case 2 * mInt + 1:
            return -(a + m) * (a + b + m) / (a + 2 * m) / (a + 2 * m + 1) * x
        case _:
            return m * (b - m) / (a + 2 * m - 1) / (a + 2 * m) * x
        }
    }
}

This feels like something I'd use often. I realize that nothing prevents me from declaring ai(i:) before I call the continued fraction function right now, but in practice I do not and I think I would use this option if available. If in addition we got lazy variables in the where block that would potentially be a nice win for quantities that only needed to be computed on a subset of code paths.

Thanks for the bringing up this direction!

1 Like

The main difference between your code samples is formatting style, not the where keyword. If you want to compare apples to apples, you should use the same style for both samples

Style 1 (first argument on the same line as opening bracket, rest of the arguments aligned to the first argument, closing bracket on the same line as the last argument):

let π = continued_fraction(b0: 3,
                            a: { pow(Double(2 * $0 - 1),2) }, 
                            b: { _ in 6 })

let π = continued_fraction(b0: 3, a: ai, b: bi) where { func ai(i: Int) -> Double { pow(Double(2 * i - 1),2) };
                                                        func bi(i: Int) -> Double { 6 }}

Style 2 (all arguments on their own lines, closing bracket on their own line):

let π = continued_fraction(
    b0: 3,
    a: { pow(Double(2 * $0 - 1),2) }, 
    b: { _ in 6 }
)

let π = continued_fraction(b0: 3, a: ai, b: bi) where {
    func ai(i: Int) -> Double { pow(Double(2 * i - 1),2) }
    func bi(i: Int) -> Double { 6 }
}

If you don't use formatting style that disadvantages the normal calling style, the line length is much shorter, not longer than the proposed syntax, and matching : is just as easy

2 Likes

@cukr, I agree that my comparison was flawed by inconsistent formatting. Thank you for demonstrating a proper apples to apples comparison.

I think that it's in the realm of possibility that the type checker could know enough about ai and bi that we could write the even more similar

let π = continued_fraction(
    b0: 3,
    a: { pow(Double(2 * $0 - 1),2) }, 
    b: { _ in 6 }
)

let π = continued_fraction(b0: 3, a: ai, b: bi) where {
    let ai = { pow(Double(2 * $0 - 1),2) }
    let bi = { _ in 6 }
}

In that comparison I personally find it easier to match brackets than parentheses and colons, but that could just be what my eyes are used to scanning for.

Do you find the use case any more compelling when there's a single longer closure somewhere in the middle of the function call? Like in the incomplete beta example:

let cf = continued_fraction(
    b0: 0, 
    a: { i in
        let mInt = (i - 1) / 2
        let m = Double(mInt)
        switch (i - 1) {
        case 0:
            return 1
        case 2 * mInt + 1:
            return -(a + m) * (a + b + m) / (a + 2 * m) / (a + 2 * m + 1) * x
        case _:
            return m * (b - m) / (a + 2 * m - 1) / (a + 2 * m) * x
        }
    }, 
    b: { _ in 1 }
)

vs:

let cf = continued_fraction(b0: 0, a: ai, b: { _ in 1 })
where {
    let ai = { i in
        let mInt = (i - 1) / 2
        let m = Double(mInt)
        switch (i - 1) {
        case 0:
            return 1
        case 2 * mInt + 1:
            return -(a + m) * (a + b + m) / (a + 2 * m) / (a + 2 * m + 1) * x
        case _:
            return m * (b - m) / (a + 2 * m - 1) / (a + 2 * m) * x
        }
    }
}

I find that reading the first way makes me worry about whether I'm still within the function call and where it might end. It's only a difference in readability not in functionality, though, and those are subjective.

To the extent that it does help I think where could help after any kind of statement, not just after function calls—and potentially after function / closure blocks, too.

When I have a long thing that I want to pass somewhere, I make it a real function with a meaningful name (unless it's a completion handler. My completion handlers are looong, but it's okay because they are trailing closures anyway)

func widgetReticulationsPerSpline(_ i: Int) -> Int {
    let mInt = (i - 1) / 2
    let m = Double(mInt)
    switch i - 1 {
    case 0:
        return 1
    case 2 * mInt + 1:
        return -(a + m) * (a + b + m) / (a + 2 * m) / (a + 2 * m + 1) * x
    case _:
        return m * (b - m) / (a + 2 * m - 1) / (a + 2 * m) * x
    }
}
let cf = continued_fraction(
    b0: 0,
    a: widgetReticulationsPerSpline,
    b: { _ in 1 }
)

...which, now I realize is basically the same thing as your DLABs. Just that instead of writing

XXX
callMethod(yyy)

yours is

callMethod(yyy) where {
  XXX
}

If you are optimizing for worrying about wheter or not you are still within the function call, then you're going in a wrong direction, because where block look like they are part of the call for me, even though they really aren't

Interesting proposal, I guess I don’t have many use cases for such a complex function, so take this with a grain of salt. To me it seems like encapsulating this differently would probably help readability and maintainability.

In terms of the language design here, it reminds me a lot of how Kotlin offers in-place subclassing and method overriding with a similar syntax. In Kotlin, it is IMO a welcome addition to a language that is trying to improve on the verbosity of Java and its deep inheritance hierarchies. In Swift I wouldn’t be happy with it because I’ve learnt to structure my APIs in a way that doesn’t require it.

1 Like