How to use raw identifiers in Swift

⋅ 5 min read ⋅ Swift Swift Evolution Testing

Table of Contents

When we write a test with swift-testing, we usually end up naming it twice.

@Test("square returns x * x")
func squareIsXTimesX() {
#expect(square(4) == 4 * 4)
}

Once in the @Test macro, and once in the function name. The second one is a camelCase paraphrase of the first.

SE-0451: Raw identifiers, introduced in Swift 6.2, lets us write it once.

@Test func `square returns x * x`() {
#expect(square(4) == 4 * 4)
}

What is a raw identifier

We already use backticks in Swift to borrow a keyword as a name.

enum Element {
case `class`
case `protocol`
}

Swift 6.2 extends that same syntax to names that aren't just keywords, but characters the language wouldn't otherwise allow in a name at all.

The proposal gives these two uses different names:

  • An escaped identifier is what we have today, a keyword in backticks. The characters inside are still ordinary identifier characters.
  • A raw identifier contains characters that are not allowed in an identifier, such as a space or a leading digit.

In both cases the backticks are only delimiters. They are not part of the name.

That last point matters more than it looks. func `with a space`() declares a function actually named with a space, so that is the name we see in the debugger, in crash logs, and in index data. It isn't a display string layered on top of a generated symbol.

This is the argument for using a raw identifier over the @Test("...") description. The description only reaches the test report. Everything below the testing framework, including the compiler, the linker, and backtraces, sees the function name. Naming the function once means every tool agrees on what the test is called.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Names that are naturally numbers

Some things are best named with a number, and Swift's rules say an identifier can't start with a digit.

Take a design system where colors come in numbered intensities. Today we have to bend the name to fit.

// A leading underscore usually signals "don't touch this"
enum ColorVariant {
case _50
case _100
}

// Repetitive
enum ColorVariant {
case variant50
case variant100
}

With a raw identifier, the case can simply be the number.

enum ColorVariant {
case `50`
case `100`
case `200`
}

let color = Color(hue: .red, variant: .`100`)

The backticks are still ceremony, but it is shared ceremony. Every codebase writing a numeric name now writes it the same way, instead of each one inventing its own prefix. If you like collecting these, I have a few more in lesser known ways of using Swift enums.

Generated code and resource names

The same problem shows up whenever names come from outside Swift, and it is the case that will affect most people indirectly.

Code generators currently have to transform foreign names into Swift-safe ones, and those transformations aren't reversible. Two different source names can collide on the same Swift name, so generators grow rules to break the ties, and we have to learn the rules to know what to type.

Apple's own SF Symbols are the obvious example, with names like 1.circle. A generator can now map the name straight across.

extension UIImage {
static var `10.circle`: UIImage { ... }
}

The same applies to module names in very large projects. A build system that identifies a target by its path no longer has to flatten it.

import `myapp/extensions/widget/common/utils`

What can't go in a raw identifier

Almost any Unicode character is allowed. The exceptions are:

  • A backtick, since it ends the identifier.
  • A backslash, reserved for escape sequences Swift might add later.
  • A newline or carriage return, so an identifier is always on one line.
  • NUL, and the other non-printable ASCII control characters.

There are also two rules about what an identifier can't consist entirely of.

It can't be only whitespace. Leading, trailing, and internal spaces are all fine, so `two words` and ` padded ` both work, but a name made of nothing but spaces doesn't.

It also can't be only operator characters. This one is worth knowing because the error is confusing if you don't expect it.

func + (lhs: Int, rhs: Int) -> Int    // ok
func `+` (lhs: Int, rhs: Int) -> Int // error

let x = 1 + 2 // ok
let x = 1 `+` 2 // error

A name that merely contains an operator character is fine. `square returns x * x` is legal, because it isn't only operator characters. The restriction exists to keep the syntax free for a possible future meaning.

The property wrapper gotcha

If a property wrapper wraps a property named with a raw identifier, the _ and $ sigils go inside the backticks.

struct UsesWrapper {
@Wrapper var `with a space`: Int
}

let x = UsesWrapper()

print(x.`_with a space`) // correct
doSomethingWith(x.`$with a space`) // correct

print(x._`with a space`) // error
doSomethingWith(x.$`with a space`) // error

The reasoning is that the sigil is part of the name of the backing storage, not a prefix applied to it. The generated property really is called _with a space.

A related consequence: `$0` in backticks is an ordinary identifier, not the first closure argument. That has to be true, otherwise a property named `0` would have no way to spell its projected value.

When to reach for one

The test case is the clearest, because a test function name is not API. Nothing calls it but the framework, and its only job is to describe what the test checks. That's exactly the situation where a sentence beats a camelCase paraphrase of a sentence.

I would be more careful elsewhere. A raw identifier for a function other people call means every call site carries backticks, and that cost lands on readers rather than on the author. The numeric-enum and generated-code cases work because the alternative — _100, or a mangled name from a generator — is worse for the reader too.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Summary

Raw identifiers extend Swift's existing backtick syntax to names containing spaces, digits, and punctuation.

The backticks are delimiters only, so the declared name is the real name and every tool reports it consistently. The main restrictions are that an identifier can't contain a backtick, backslash, or newline, and can't consist entirely of whitespace or entirely of operator characters.

The feature earns its place in tests, where the name exists purely to describe, and in generated code, where names come from somewhere that never agreed to Swift's rules in the first place.


Read more article about Swift, Swift Evolution, Testing, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
Private properties no longer break the memberwise initializer in Swift 6.4

SE-0502 changes the rules for the synthesized memberwise initializer so a private property with an initial value doesn't make the whole initializer inaccessible.

← Home