Skip to content

Add KeyPathIterable protocol and synthesis. #21557

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
merged 3 commits into from
Dec 28, 2018

Conversation

dan-zheng
Copy link
Contributor

The KeyPathIterable protocol represents types whose values provide
custom key paths to properties or elements.

For types that conform to KeyPathIterable, the compiler can synthesize
a default implementation of allKeyPaths based on the type's stored
properties.


KeyPathIterable enables generic machine learning optimizers and will
be used to replace ParameterGroup.

The ParameterGroup protocol requirements are not sufficiently general
for many use cases: it is limited to a single parameter type and does not
support joint iteration over parameters for multiple ParameterGroup
instances.

KeyPathIterable solves these problems. In addition, it is a generally
useful language feature that can model both static stored properties
and custom dynamic properties.


KeyPathIterable.swift currently lives in stdlib/public/core to enable
future usage in AutoDiff tests without dependency on TensorFlow module.


KeyPathIterable is designed by @rxwei and myself.

KeyPathIterable is inspired by:

Here is an extended design for KeyPathIterable that separates functionality into two protocols: StoredPropertyIterable and CustomKeyPathIterable.

  • StoredPropertyIterable models the static layout of structs and provides a static collection of key paths to all stored properties.
  • CustomKeyPathIterable can model both static and dynamic structures, including collection types like Array and Dictionary.

@dan-zheng dan-zheng added the tensorflow This is for "tensorflow" branch PRs. label Dec 28, 2018
@dan-zheng dan-zheng requested review from rxwei and marcrasi December 28, 2018 09:42
public extension KeyPathIterable {
/// An array of all custom key paths of this value and any custom key paths
/// nested within each of what this value's key paths refers to.
var recursivelyAllKeyPaths: [PartialKeyPath<Self>] {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The naming of recursivelyAllKeyPaths and the doc comment have room for improvement.

Choose a reason for hiding this comment

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

nestedKeyPaths?

Copy link
Contributor Author

@dan-zheng dan-zheng Apr 26, 2019

Choose a reason for hiding this comment

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

Thanks for the suggestion! I feel nestedKeyPaths is not an ideal name for this property because it also contains top-level allKeyPaths. nestedKeyPaths sounds like it doesn't contain top-level properties (or at least leaves it ambiguous).


We considered nestedKeyPaths as not just a different name, but a different design option (where allNestedKeyPaths refers to recursivelyAllKeyPaths excluding top-level allKeyPaths, and recursivelyAllKeyPaths would be defined as allKeyPaths + allNestedKeyPaths).

We decided not to implement that option though, since we couldn't think of many use cases for "just recursively all nested non-top-level key paths". There seem to be more use cases for "recursively all key paths, including top-level key paths".

The `KeyPathIterable` protocol represents types whose values provide
custom key paths to properties or elements.

For types that conform to `KeyPathIterable`, the compiler can synthesize
a default implementation of `allKeyPaths` based on the type's stored
properties.

---

`KeyPathIterable` enables generic machine learning optimizers and will
be used to replace `ParameterGroup`.

The `ParameterGroup` protocol requirements are not sufficiently general
for many use cases: it is limited to a single parameter type and does not
support joint iteration over parameters for multiple `ParameterGroup`
instances.

`KeyPathIterable` solves these problems. In addition, it is a generally
useful language feature that can model both static stored properties
and custom dynamic properties.
Copy link
Contributor

@rxwei rxwei left a comment

Choose a reason for hiding this comment

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

Great!

UnresolvedDotExpr(nominalTypeExpr, SourceLoc(), member->getFullName(),
DeclNameLoc(), /*Implicit*/ true);
auto *keyPathExpr =
new (C) KeyPathExpr(SourceLoc(), dotExpr, nullptr, /*Implicit*/ true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Woo! This is so simple.

- Remove `@inlinable` from `KeyPathIterable` extension methods.
- Enable `KeyPathIterable` synthesis for empty structs, add test.
- Add comment by @rxwei about supporting synthesis for classes in the future.
Mark synthesized `allKeyPaths` declaration with `@inlinable`.
Verified that `@inlinable` is correctly added in SILGen:

```
struct Parameters : KeyPathIterable {
  @sil_stored var w: Float { get set }
  @sil_stored var b: Float { get set }
  init(w: Float, b: Float)
  typealias AllKeyPaths = [PartialKeyPath<Parameters>]
  @inlinable var allKeyPaths: [PartialKeyPath<Parameters>] { get }
}
```
@rxwei
Copy link
Contributor

rxwei commented Dec 28, 2018

@swift-ci please test tensorflow

@dan-zheng
Copy link
Contributor Author

Note: the synthesis condition for KeyPathIterable could be tightened to: "synthesize requirements only if no requirements have been user customized".

This would prevent errors like the following:

// error.swift
struct Test : KeyPathIterable {
  var x: Float
  // Question:
  //   What if I customize `AllKeyPaths` to be sth that conforms to
  //   `Collection` but not `ExpressibleByArrayLiteral`?
  //   And I let the compiler synthesize `allKeyPaths`?
  // Answer:
  //    The compiler doesn't crash, but produces an error because
  //    the synthesized body of `allKeyPaths` assumes that `AllKeyPaths`
  //    conforms to `ExpressibleByArrayLiteral`.
  typealias AllKeyPaths = UnsafeBufferPointer<PartialKeyPath<Test>>
}
error.swift:1:8: error: type 'Test' does not conform to protocol 'KeyPathIterable'
struct Test : KeyPathIterable {
       ^
error.swift:1:8: error: protocol 'KeyPathIterable' is broken; cannot derive conformance for type 'Test'
struct Test : KeyPathIterable {
       ^

Errors like this aren't unique to KeyPathIterable though, CaseIterable has a similar issue.
Since the compiler doesn't crash, I think this error can be handled in a follow-up PR.

@rxwei
Copy link
Contributor

rxwei commented Dec 28, 2018

Great. Thanks for the detailed note!

@rxwei rxwei merged commit 247bb4b into swiftlang:tensorflow Dec 28, 2018

public extension KeyPathIterable {
var _allKeyPathsTypeErased: [AnyKeyPath] {
return allKeyPaths.map { $0 as AnyKeyPath }
Copy link
Member

Choose a reason for hiding this comment

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

Since key paths are classes, you can cast the array for these two instead of mapping:

return allKeyPaths as [AnyKeyPath]

(This is really cool functionality!)

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not really directly convertible. I'm getting this error:

/Users/rxwei/Development/Swift/swift-tf-source/swift/stdlib/public/core/KeyPathIterable.swift:57:24: error: 'Self.AllKeyPaths' is not convertible to '[AnyKeyPath]'; did you mean to use 'as!' to force downcast?
    return allKeyPaths as [AnyKeyPath]
           ~~~~~~~~~~~~^~~~~~~~~~~~~~~
                       as!

Copy link
Contributor

Choose a reason for hiding this comment

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

If that works on master, then it's probably because the changes haven't been merged. The tensorflow branch is a pretty old (2018-11-24).

Copy link
Member

Choose a reason for hiding this comment

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

Ah, you’re right — I’d missed that allKeyPaths isn’t an array here, it’s an instance of the associated type. Carry on!

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah right. I forgot that too :)

extension Dictionary : KeyPathIterable {
public typealias AllKeyPaths = [PartialKeyPath<Dictionary>]
public var allKeyPaths: [PartialKeyPath<Dictionary>] {
return keys.map { \Dictionary[$0]! }
Copy link
Member

Choose a reason for hiding this comment

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

Seems like this (and really all collections) should follow the array example of giving access via indices, yielding key paths to key-value pairs here. If you make Dictionary.Values key-path iterable, too, a user could write myDictionary.values.allKeyPaths to get this result.

Copy link
Contributor

@rxwei rxwei Apr 21, 2019

Choose a reason for hiding this comment

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

Good point. Key paths using subscript and Dictionary.Index currently cause a crasher on our branch. This may have been fixed on master. We'll make sure to update these once we merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
tensorflow This is for "tensorflow" branch PRs.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants