SE-0289 (review #2): Result Builders

My suggestion was strictly syntactic. Nobody asked to make builders type attributes. Please just make them look like type attributes, unless this harms the language.

8 Likes

In other words, your request is to always place parameter attributes after the colon to erase the syntactic distinction between type and declaration attributes. This also puts the attributes in a different place than for other kinds of declarations, where they are always at the beginning.

  • Doug

Personally I don't think many or even most Swift developers see those two contexts as similar, so the desire to keep them symmetrical may not be as valuable as you think. The additional confusion caused by @autoclosure and @escaping only makes that distinction less clear. There are things that are only valid on parameter types, some only valid on property types. So while this may be a clear and obvious distinction to a language designer, but probably isn't to most Swift developers.

Along those lines, if the proper term for something like @autoclosure is "type attribute", what would a result builder be?

2 Likes

If users aren't able to see the distinction right now, then wouldn't it be even more important to make the two kinds of attributes visually distinguishable? Otherwise, how would users ever understand why the attributes behave differently?

Do they behave differently though, from the user's perspective? Both grant special powers to the parameter value they're attached to. And frankly, if we want to distinguish between the two types of attributes, perhaps they shouldn't look alike? If the only difference is their relative position, I'm not going to be surprised when there's confusion around their roles.

In any case, I'm not arguing one way or the other here, really, I just think it should be given more thought.

1 Like

Hi Doug. When I read your detailed rephrasing of my question written in layman's terms, I think that such a change can not be introduced into SE-0289. We need another pitch and proposal.

If I were to sum up the desired outcome (still in layman's terms), it is that

func f(@foo identifier: @bar Type)

would become deprecated in favor of:

func f(identifier: @foo @bar Type)
// OR (unless this creates a problem)
func f(identifier: @bar @foo Type)

The reasoning behind such a change being that the two distinct locations are grounded on the improper level. The "different things should look different" rule does not apply, because the distinction between those different things are 1. unknown to most Swift developers and 2. useless for most Swift developers.

The second point is key to my reasoning. By learning about those different "things", the developer only learns to avoid compiler errors. But this does not help him write better code in any way. That's just disposable knowledge for most developers.

On top of that, identifiers are globally unique between all those sets of different things. If I say "apple stone cherry water", you have no problem splitting organic from inorganic objects, because words are not ambiguous. By analogy, the compiler would have no problem distinguishing type attributes from parameter attributes in a @ViewBuilder @autoclosure @escaping soup.

In the end:

  1. There is an ergonomic problem in having two distinct syntactic locations for two kinds of objects that are unknown from many developers.
  2. There is no (exhibited) problem when those objects are all grouped in the same syntactic location
  3. Assuming some people agree, we need a migration plan (deprecation + fixits in Swift 6, maybe).
  4. This does not fit in SE-0289 anyway.

Thanks for reading :slight_smile:

4 Likes

If we accept these premises, the your conclusions would be reasonable. I think the second point on which you base your reasoning is inaccurate, though.

Consider the examples given above: whether an annotation is part of the type goes to whether something can be an implementation of another or a more specific override of another, whether something can be an overload of another, and/or whether adding or removing an annotation can break the interface contract.

These are issues that most developers will have to deal with at some point or another.

Moreover, it has been complained of that overload resolution in Swift is not fully documented and already very confusing; adding to that by annotating the type with things that aren’t part of the type would only exacerbate this problem.

1 Like

Yes, this is a valid counter-argument. Both points of view end up in hurting, sometimes, some people. The interesting question is which one is the worse.

Now that I understand the reach of my initial suggestion, which is Swift 6, I kind of feel we'll end up on the statu quo. On such topic, establishing a proof of actual harm (which is necessary for introducing a breaking change) is near impossible. I have only two testimonies on "my" side so far, above. Proving, on top of that, that the change hurts less than the situation you describe, is also near impossible.

To choose is to forsake. I don't see how such a choice can be made.

Maybe we can stop this sub-subject, now that it has been established that it is irrelevant to SE-0289?

What would be in scope for this proposal though, or at least some qualify of implementation improvements related to it, is a dedicated diagnostic and fix-it to move any misplaced annotations.

4 Likes

Yes. That would be lovely.

I've having a very, very hard time understanding why you are okay with parameter declarations being different from everything else in the language. Function/result builder attributes can go on various things, and all of them are before the name of the thing:

@ViewBuilder var body: some View { ... }
@ViewBuilder func makeMyView() -> some View { ... }
func f<Content: View>(@ViewBuilder content: () -> View) { }

And attributes don't hey do not affect the type of that thing. We wouldn't move, say, a calling convention before var, would we?

@convention(c) var callback: (Int) -> Void

That would mean that the type of callback would be scattered into multiple places. Similarly, we wouldn't move @IBOutlet after the name, would we?

weak var instruction: @IBOutlet UILabel?

@IBOutlet doesn't affect the type, and then you wouldn't be able to directly type alias the type:

typealias InstructionType = @IBOutlet UILabel?  // error: not a type attribute

Parameters differ from let declarations only in that they don't have a keyword introducing them.

Doug

7 Likes

Thanks for enlarging the context, Doug. Indeed my suggestion missed a lot of other uses of such attributes. I was wrong! Now, maybe you'll agree that @xwu right above has the best answer to my initial complaint, which is not as wrong as my suggested fix:

If a user makes the mistake of writing func f(identifier: @foo Type) when the correct location is func f(@foo identifier: Type), a fixit would be welcome, instead of the current unknown attribute error which gives no clue.

I'm confident that even if you have a very hard time following an erroneous trend of thought, you are able to understand that users make mistakes, miss language subtleties, may be lazy, or even stupid, and that unknown attribute is not the best way to teach them how to avoid such compiler error. I tend to think, as a few people above do, that @ViewBuilder is one of those attributes that can easily be misinterpreted and misused. There exists a mental model where @ViewBuilder decorates the type instead of the parameter. It is wrong, granted. But it takes time to understand how and why it is wrong. There lies the opportunity for mistakes, and a good reason for a fixit.

6 Likes

Sure, the diagnostic could be better. Please file a bug at bugs.swift.org and we'll get to it.

Doug

4 Likes

Done: [SR-13722] Fixit for misplaced parameter attributes · Issue #56119 · apple/swift · GitHub

3 Likes

As an uneducated Swift user (which I am) I would probably try with this placement at first. Property wrappers are applied to variables, so I would intuitively expect them to be on the left of the parameter name in this case instead of having them on the left of the argument label (which usually is a preposition unrelated to the parameter).

@Lazy private var a: Int = 0
// 'a' is @Lazy, a variable and private

@available(iOS 14, *)
public let b: String = ""
// 'b' is available in iOS 14+, it is a constant and public

@propertyWrapper struct Wrapper { ... }
// 'Wrapper' is a @propertyWrapper and a struct
func footer<V: View>(@ViewBuilder from footer: () -> V) -> some View { ... }

In this case footer is a @ViewBuilder, but the parameter label from doesn't describe the parameter. It's a "gap" in the reading flow if we may say.

func footer<V: View>(from @ViewBuilder footer: () -> V) -> some View { ... }

It's a subtle difference, but placing the annotation between the argument label and the parameter name feels more natural while reading the code.

2 Likes

Let us not break the argument label and the parameter name. Many times they are one and the same. Now we'd need to answer some serious question about which one to omit when that's the case:

// Do we do this?
func footer(@ViewBuilder x: () -> View)

// Or this?
func footer(x @ViewBuilder: () -> View)
1 Like

I get your point, but the variable used in the function body is the one having as its name the parameter name. I think that the annotation should be placed near the parameter name for that reason. Argument labels are just decorative and are not used in the function body. They are related to the parameter but not in the same way var, private or @Lazy are, in my opinion.

As for your example, the first case would be the right one, since @ViewBuilder is attached to x.

func footer(@ViewBuilder x: () -> View)

Pitch #2: Extend Property Wrappers to Function and Closure Parameters - #23 by xAlien95 may be a more appropriate thread for this side discussion. I quoted my post there too.

1 Like

I'm actually warming up to the name "builder", even if I don't think that's what it really does. But it seems handy to talk about a @someWrapper or @someBuilder and everyone just knows what kind of thing it is, partly because of the standard @ViewBuilder.

If we renamed it to e.g. @statementVisitor there would be no obvious link to the @ViewBuilder as there is now. @ViewBuilder is building up a View by visiting statements. You could very well do other things by visiting statements than creating builders, so naming it a "result builder" is kind of lying I think, but it's probably the easiest to grok for newcomers.

The deadline for review passed long ago, but as afaik no one presented my opinion yet…

First of all, even if naming issues seem to be ridiculous, I don't think that is the case:
When a feature is easy to understand, that is a very big plus - and a name that is easy to grasp and descriptive helps a lot.
That said, I don't think SE-0289 is easy to understand, and no name for it is really great*. However, there is already a well established name that has been around for a long time, and I don't think "result builder" is so much better that it warrants a change.

*I like @statementTransformer, but I guess deep down in the discussion, this has already been discarded

3 Likes

The review concluded on October 1, 2020. The proposal has been accepted.

6 Likes