Skip to content
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

Add support for named alternate constructors. #387

Merged
merged 1 commit into from
Feb 18, 2021
Merged

Conversation

rfk
Copy link
Collaborator

@rfk rfk commented Feb 17, 2021

This is an alternative approach to factory methods, based on feedback from #384. It also fixes #37.

It's a fairly common pattern for object-oriented interfaces to have
static methods that act as named "factories" for producing object
instances in ways that differ from the default constructor.
This commit adds basic support for such named alternate constructors:

  • In the UDL, declare additional constructors with a [Name=Value]
    attribute to tell the tool what name should be exposed under.
  • In the Rust code, implement a corresponding method on the struct
    that acts as a constructor (no self parameter, Self return type).
  • In the foreign-language code, expect a static method on the
    resulting class with name matching the one provided in Rust.

We continue to reserve the name "new" to refer to the default constructor.
A followup could consider annotating it with an additional attribute
like [DefaultConstructor] in order to let the default constructor have
a different name on the Rust side.

@rfk rfk requested review from jhugman and mhammond February 17, 2021 06:45
@rfk rfk force-pushed the alternate-constructors branch 2 times, most recently from d727bbe to 8a0f0b6 Compare February 17, 2021 06:50
Copy link
Contributor

@jhugman jhugman left a comment

Choose a reason for hiding this comment

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

I'm not really qualified to comment on the WebIDL or python, but the Rust, Kotlin and Swift parts are looking very nice. Thank you.

There are a few nits, but I won't hold up proceedings to get this moving. r+

fun {{ cons.name()|fn_name_kt }}({% call kt::arg_list_decl(cons) %}): {{ obj.name()|class_name_kt }} =
{{ obj.name()|class_name_kt }}({% call kt::to_ffi_call(cons) %})
Copy link
Contributor

Choose a reason for hiding this comment

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

Lovely!

Comment on lines 21 to 23
public init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} {
self.handle = {% call swift::to_ffi_call(cons) %}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I'd suggest make everything go through a common initializer.

Suggested change
public init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} {
self.handle = {% call swift::to_ffi_call(cons) %}
}
public convenience init({% call swift::arg_list_decl(cons) -%}) {% call swift::throws(cons) %} {
self.init(fromRawHandle: {% call swift::to_ffi_call(cons) %})
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice, thanks. Also, my word Swift is a conceptually-dense language, TIL about "conveience initializers" vs "designated initializers"...

Comment on lines 33 to 35
static func lift(_ handle: UInt64) throws -> {{ obj.name()|class_name_swift }} {
{{ obj.name()|class_name_swift }}(fromRawHandle: handle)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, good catch, I think this is leftover from the previous factoring.

"Attribute identity Identifier not supported: {:?}",
identity.lhs_identifier.0
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice.

Comment on lines +368 to +384
Some(id) => {
let name = id.0.to_string();
if name == "new" {
bail!("the method name \"new\" is reserved for the default constructor");
}
name
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice.

}

#[test]
fn test_the_name_new_is_reserved_for_constructors() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Whilst we're crossing the 👀 and dotting the 🍵 testing-wise, what happens if we have two constructors called new?

            namespace test{};
            interface Testing {
                constructor();
                [Name=new]
                constructor();
            };

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK, I'll fess up...I actually started writing this exact test and then deleted it when I realized that it was likely to behave in a dumb manner (allowing duplicate names in the ComponentInterface and then giving inscrutable compile errors later on in the generated code). I'll take a look at making this error out more cleanly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(Surprise! Making it error out on duplicate names was actually quite straightforward and I should have just done this work the first time around 😅. Updated.)

}
{% endmatch %}
{% endfor %}

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: whitespace.

@rfk rfk force-pushed the alternate-constructors branch from 8a0f0b6 to 5aed68d Compare February 17, 2021 23:30
It's a fairly common pattern for object-oriented interfaces to have
static methods that act as named "factories" for producing object
instances in ways that differ from the default constructor.
This commit adds basic support for such named alternate constructors:

* In the UDL, declare additional constructors with a [Name=Value]
  attribute to tell the tool what name should be exposed under.
* In the Rust code, implement a corresponding method on the struct
  that acts as a constructor (no `self` parameter, `Self` return type).
* In the foreign-language code, expect a static method on the
  resulting class with name matching the one provided in Rust.

We continue to reserve the name "new" to refer to the default constructor.
A followup could consider annotating it with an additional attribute
like [DefaultConstructor] in order to let the default constructor have
a different name on the Rust side.
@rfk rfk force-pushed the alternate-constructors branch from 5aed68d to 623d32f Compare February 18, 2021 00:30
Copy link
Member

@mhammond mhammond left a comment

Choose a reason for hiding this comment

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

This is much clearer!

@rfk rfk merged commit 4d61601 into main Feb 18, 2021
@rfk rfk deleted the alternate-constructors branch February 18, 2021 00:55
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.

Static and/or multiple constructors support
3 participants