Skip to content

Add handwritten convenience initializers #291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

kimdv
Copy link
Contributor

@kimdv kimdv commented Jun 14, 2021

And we try again @ahoppen 😄

I have added some handwritten convenience initializers.
Is there more you could think of, that would make it easier?

Copy link
Member

@ahoppen ahoppen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. This is looking great!

@ahoppen
Copy link
Member

ahoppen commented Jun 15, 2021

@swift-ci Please test

@ahoppen ahoppen merged commit 574288c into swiftlang:main Jun 22, 2021
@kimdv kimdv deleted the kimdv/add-handwritten-convenience-initializers branch June 22, 2021 08:00
@kimdv
Copy link
Contributor Author

kimdv commented Jun 22, 2021

@ahoppen thanks!
Is there more we need to add in SwiftSyntaxBuilder?

Any good ideas? 😁

@ahoppen
Copy link
Member

ahoppen commented Jun 22, 2021

So, I have finally managed to play around with this and tried to create a struct with a few variables as members. What I think would have been nice would have been something like this:

Struct("MyStruct") {
  Variable(.let, name: "myFirstVar", type: "Int")
  Variable(.var, name: "myOtherVar", type: "String")
}

(disclaimer: It doesn’t have to be exactly this syntax, it’s just what I would have loved but I really wouldn’t mind if the syntax looked a little different).

Instead, I had to write this:

 1 | StructDecl(identifier: "TestStruct", members: MemberDeclBlock(membersBuilder: {
 2 |     MemberDeclListItem(decl:
 3 |       VariableDecl(letOrVarKeyword: Tokens.let) {
 4 |         PatternBinding(pattern: IdentifierPattern(identifier: SyntaxFactory.makeIdentifier("myFirstVar")),
 5 |                        typeAnnotation: TypeAnnotation(type: SimpleTypeIdentifier("Int")))
 6 |       }
 7 |     )
 8 |     MemberDeclListItem(decl:
 9 |       VariableDecl(letOrVarKeyword: Tokens.let) {
10 |         PatternBinding(pattern: IdentifierPattern(identifier: SyntaxFactory.makeIdentifier("myOtherVar")),
11 |                        typeAnnotation: TypeAnnotation(type: SimpleTypeIdentifier("String")))
12 |       }
13 |     )
14 |   })
15 | )

So, here’s my wish list by line number since you were asking ;-)

1a. The types should strip the Decl and other suffixes from their names. StructDecl should become Struct

1b. A MemberDeclBlock should be directly creatable from a MemberDeclListBuilder result builder. This would probably take some hardcoding of MemberDeclBlock in the convenience initializers where the initialization of a MemberDeclBlock with a MemberDeclListBuilder is another convenience added (besides the current ones of: syntax collection through result builder, tokens by string literals and defaulting of tokens)

1c. I should be able to write the membersBuilder as a trailing closure to MemberDeclBlock, so that I don’t have to write the membersBuilder argument label. I suggest that we move all result builder parameters in the convenience initializers to the end of the initializer, so that trailing closure syntax can be used.

2a. I shouldn’t need to wrap the VariableDecl in a MemberDeclListItem for the result builder. I suggest that we add a protocol

protocol ExpressibleAsMemberDeclListItem {
  func createMemberDeclListItem() -> MemberDeclListItem
}

and that MemberDeclListBuilder takes arguments of that type. Then add conformances

extension MemberDeclListItem: ExpressibleAsMemberDeclListItem {
  func createMemberDeclListItem() -> MemberDeclListItem { return self }
}
extension DeclBuildable: ExpressibleAsMemberDeclListItem {
  func createMemberDeclListItem() -> MemberDeclListItem { return MemberDeclListItem(decl: self) }
}

The same approach would probably also work for all other result builders and would give us the flexibility to avoid these boilderplate conversions for other types (e.g. CodeBlock) as well

3a. I think a manually written convenience initializer to create variables would be a good idea. The current syntax is just gross.

3b. If the public enum Tokens would be an extension TokenSyntax, I could write Tokens.let using leading dot syntax as .let

4a. TokenSyntax should be ExpressibleByStringLiteral. That way we wouldn’t need SyntaxFactory.makeIdentifier("myFirstVar"). It might also remove the need for convenience of passing in a String for a TokenSyntax in the convinience initializers because TokenSyntax can now just be expressed by a string literal.

If any of these don’t make sense to you, please ask!

In any case, thanks again so much for working on this! 🙏

@kimdv
Copy link
Contributor Author

kimdv commented Jun 22, 2021

1a. The types should strip the Decl and other suffixes from their names. StructDecl should become Struct

I have tried to generate without the Decl, Expr.

We get some clashes with types like ArrayExpr -> Array.
I have tried to add some back ticks, but it didn't help.

Any suggestions @ahoppen ?

1c. I should be able to write the membersBuilder as a trailing closure to MemberDeclBlock, so that I don’t have to write the membersBuilder argument label. I suggest that we move all result builder parameters in the convenience initializers to the end of the initializer, so that trailing closure syntax can be used.

I have tried to generate with all result builder at the end.
example:

extension VariableDecl {
  public init(
    letOrVarKeyword: TokenSyntax,
    @AttributeListBuilder attributesBuilder: () -> AttributeList? = { nil },
    @ModifierListBuilder modifiersBuilder: () -> ModifierList? = { nil },
    @PatternBindingListBuilder bindingsBuilder: () -> PatternBindingList = { .empty }
  ) {
    self.init(
      attributes: attributesBuilder(),
      modifiers: modifiersBuilder(),
      letOrVarKeyword: letOrVarKeyword,
      bindings: bindingsBuilder()
    )
  }
}

But test case

let buildable = VariableDecl(letOrVarKeyword: Tokens.var) {
        PatternBinding(pattern: IdentifierPattern(identifier: SyntaxFactory.makeIdentifier("number")),
                       typeAnnotation: TypeAnnotation(type: SimpleTypeIdentifier("Int")),
                       initializer: InitializerClause(value: IntegerLiteralExpr(digits: Tokens.integerLiteral("123"))))
    }

Gives a warning Backward matching of the unlabeled trailing closure is deprecated; label the argument with 'bindingsBuilder' to suppress this warning

We could add a _ before each parameter name, but I'm not sure I like that, specially when there are multiple result builders in an init.
Then it will look like this

let buildable = VariableDecl(letOrVarKeyword: Tokens.let, {
        PatternBinding(pattern: IdentifierPattern(identifier: SyntaxFactory.makeIdentifier("color")),
                       typeAnnotation: TypeAnnotation(type: SimpleTypeIdentifier("UIColor")))
    })

4a. TokenSyntax should be ExpressibleByStringLiteral. That way we wouldn’t need SyntaxFactory.makeIdentifier("myFirstVar"). It might also remove the need for convenience of passing in a String for a TokenSyntax in the convinience initializers because TokenSyntax can now just be expressed by a string literal.

I'm not sure on this?
Do you think about something like:

extension TokenSyntax: ExpressibleByStringLiteral {
  public init(stringLiteral value: Int) {
    self.init(value)
  }
}

@ahoppen
Copy link
Member

ahoppen commented Jun 23, 2021

1a. The types should strip the Decl and other suffixes from their names. StructDecl should become Struct

I have tried to generate without the Decl, Expr.

We get some clashes with types like ArrayExpr -> Array.
I have tried to add some back ticks, but it didn't help.

Any suggestions @ahoppen ?

Ah, I didn’t consider that. In that case maybe it’s best to just leave the suffixes there for the moment.

1c. I should be able to write the membersBuilder as a trailing closure to MemberDeclBlock, so that I don’t have to write the membersBuilder argument label. I suggest that we move all result builder parameters in the convenience initializers to the end of the initializer, so that trailing closure syntax can be used.

I have tried to generate with all result builder at the end.
example:

extension VariableDecl {
  public init(
    letOrVarKeyword: TokenSyntax,
    @AttributeListBuilder attributesBuilder: () -> AttributeList? = { nil },
    @ModifierListBuilder modifiersBuilder: () -> ModifierList? = { nil },
    @PatternBindingListBuilder bindingsBuilder: () -> PatternBindingList = { .empty }
  ) {
    self.init(
      attributes: attributesBuilder(),
      modifiers: modifiersBuilder(),
      letOrVarKeyword: letOrVarKeyword,
      bindings: bindingsBuilder()
    )
  }
}

But test case

let buildable = VariableDecl(letOrVarKeyword: Tokens.var) {
        PatternBinding(pattern: IdentifierPattern(identifier: SyntaxFactory.makeIdentifier("number")),
                       typeAnnotation: TypeAnnotation(type: SimpleTypeIdentifier("Int")),
                       initializer: InitializerClause(value: IntegerLiteralExpr(digits: Tokens.integerLiteral("123"))))
    }

Gives a warning Backward matching of the unlabeled trailing closure is deprecated; label the argument with 'bindingsBuilder' to suppress this warning

We could add a _ before each parameter name, but I'm not sure I like that, specially when there are multiple result builders in an init.
Then it will look like this

let buildable = VariableDecl(letOrVarKeyword: Tokens.let, {
        PatternBinding(pattern: IdentifierPattern(identifier: SyntaxFactory.makeIdentifier("color")),
                       typeAnnotation: TypeAnnotation(type: SimpleTypeIdentifier("UIColor")))
    })

Hmm. That’s a bit unfortunate. Still, I think it would be beneficial to have the result builders at the end because:
i) it will allow us to use trailing closure syntax if there is only one result builder (like for MemberDeclBlock)
ii) it will allow us to use multiple trailing closure syntax if all closures are being passed.
iii) Having a labelled closure at the end, at least in my opinion, looks at least a little bit like a trailing closure.

4a. TokenSyntax should be ExpressibleByStringLiteral. That way we wouldn’t need SyntaxFactory.makeIdentifier("myFirstVar"). It might also remove the need for convenience of passing in a String for a TokenSyntax in the convinience initializers because TokenSyntax can now just be expressed by a string literal.

I'm not sure on this?
Do you think about something like:

extension TokenSyntax: ExpressibleByStringLiteral {
  public init(stringLiteral value: Int) {
    self.init(value)
  }
}

That’s what I was thinking. Why do you have doubts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants