Compile Time Key Path Checking in Swift

Apply KeyPath in map() in simplest form

Why doesn't it work? Seems like it should.

You are right. Both of your examples should compile because

  • SE-0227 Identity Keypath has been implemented in Swift 5, and
  • SE-0249 Key Path Expressions as Functions has been implemented in Swift 5.2.

In fact the latter proposal explicitly mentions an example similar to yours. However, that does not compile (as of Xcode 11.7 and Xcode 12 beta):

[1, nil, 3, nil, 5].compactMap(\.self)
// error: generic parameter 'ElementOfResult' could not be inferred

That is a bug. It has already been reported as

  • SR-12897 - ".self" not working with keypath-as-function - Examples in Key Path Expressions as Functions proposal don't compile.

The bug report mentions a custom extension as a possible workaround:

extension Optional { var mySelf: Self { self } }

[1, nil, 3, nil, 5].compactMap(\.mySelf)

Swift: .contains(where:) using key-path expression

Yes, you simply need to pass the key-path to contains(where:) without a closure, same as you do with filter.

let hasTruth = structs.contains(where: \.bool)

Getting string from Swift 4 new key path syntax?

For Objective-C properties on Objective-C classes, you can use the _kvcKeyPathString property to get it.

However, Swift key paths may not have String equivalents. It is a stated objective of Swift key paths that they do not require field names to be included in the executable. It's possible that a key path could be represented as a sequence of offsets of fields to get, or closures to call on an object.

Of course, this directly conflicts with your own objective of avoiding to declare properties @objc. I believe that there is no built-in facility to do what you want to do.

Compile time check for valid file references in Xcode

AutoComplete for [UIImage imageNamed:] by Kent Sutherland.

This provides code completion support within Xcode - a brilliant piece of code. This is working for me in Xcode 4.6: Sample Image

Currently this project does not have support for strings other than imageNamed:. To support those, I will try to write a compile time script. Or maybe I will become bold and try to extend Mr. Sutherland's spectacular work.

Can the non-string property name passed to #keyPath() be saved independently?

It doesn't look like it is possible.


Here's the compiler's code to parse a key path expression:

///   expr-keypath:
/// '#keyPath' '(' unqualified-name ('.' unqualified-name) * ')'
///
ParserResult<Expr> Parser::parseExprKeyPath() {
// Consume '#keyPath'.
SourceLoc keywordLoc = consumeToken(tok::pound_keyPath);

// Parse the leading '('.
if (!Tok.is(tok::l_paren)) {
diagnose(Tok, diag::expr_keypath_expected_lparen);
return makeParserError();
}
SourceLoc lParenLoc = consumeToken(tok::l_paren);

// Handle code completion.
SmallVector<Identifier, 4> names;
SmallVector<SourceLoc, 4> nameLocs;
auto handleCodeCompletion = [&](bool hasDot) -> ParserResult<Expr> {
ObjCKeyPathExpr *expr = nullptr;
if (!names.empty()) {
expr = ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names,
nameLocs, Tok.getLoc());
}

if (CodeCompletion)
CodeCompletion->completeExprKeyPath(expr, hasDot);

// Eat the code completion token because we handled it.
consumeToken(tok::code_complete);
return makeParserCodeCompletionResult(expr);
};

// Parse the sequence of unqualified-names.
ParserStatus status;
while (true) {
// Handle code completion.
if (Tok.is(tok::code_complete))
return handleCodeCompletion(!names.empty());

// Parse the next name.
DeclNameLoc nameLoc;
bool afterDot = !names.empty();
auto name = parseUnqualifiedDeclName(
afterDot, nameLoc,
diag::expr_keypath_expected_property_or_type);
if (!name) {
status.setIsParseError();
break;
}

// Cannot use compound names here.
if (name.isCompoundName()) {
diagnose(nameLoc.getBaseNameLoc(), diag::expr_keypath_compound_name,
name)
.fixItReplace(nameLoc.getSourceRange(), name.getBaseName().str());
}

// Record the name we parsed.
names.push_back(name.getBaseName());
nameLocs.push_back(nameLoc.getBaseNameLoc());

// Handle code completion.
if (Tok.is(tok::code_complete))
return handleCodeCompletion(false);

// Parse the next period to continue the path.
if (consumeIf(tok::period))
continue;

break;
}

// Parse the closing ')'.
SourceLoc rParenLoc;
if (status.isError()) {
skipUntilDeclStmtRBrace(tok::r_paren);
if (Tok.is(tok::r_paren))
rParenLoc = consumeToken();
else
rParenLoc = PreviousLoc;
} else {
parseMatchingToken(tok::r_paren, rParenLoc,
diag::expr_keypath_expected_rparen, lParenLoc);
}

// If we cannot build a useful expression, just return an error
// expression.
if (names.empty() || status.isError()) {
return makeParserResult<Expr>(
new (Context) ErrorExpr(SourceRange(keywordLoc, rParenLoc)));
}

// We're done: create the key-path expression.
return makeParserResult<Expr>(
ObjCKeyPathExpr::create(Context, keywordLoc, lParenLoc, names,
nameLocs, rParenLoc));
}

This code first creates a list of period-separated names inside the parentheses, and then it attempts to parse them as an expression. It accepts an expression and not data of any Swift type; it accepts code, not data.

Advantage of Key-Value Coding in Swift 4

Two key advantages are:

  1. Keys are validated: Xcode will warn you, at compile time, if you try to use a key that is not valid.

  2. Results are strongly typed: With the following, the compiler will know that the resulting name is a String:

    let name = student[keyPath: nameKey]

    This eliminates the need to do any casting.

The result is that it's much easier to write safe code.

map(keyPath) where keyPath is a variable

Array.map expects a closure with signature (Element) throws -> T.

In Swift 5.2, key paths were allowed to be passed in as functions/closures (here's an evolution proposal), but only as literals (at least, according to the proposal, it says "for now", so perhaps this restriction would be lifted).

To overcome this, you can create an extension on Sequence that accepts a key path:

extension Sequence {
func map<T>(_ keyPath: KeyPath<Element, T>) -> [T] {
return map { $0[keyPath: keyPath] }
}
}

(credit to: https://www.swiftbysundell.com/articles/the-power-of-key-paths-in-swift/)

Then you can do what you wanted:

let keyPath = \(Int, Int).0
arr.map(keyPath)


Related Topics



Leave a reply



Submit