Custom Attributes

It seems quite clear that Swift will get more powerful runtime reflection APIs in the future, probably post ABI-stabilization. But before that happens, I'd like to find out if Custom Attributes, a feature I love from C# and think would do wonders for Swift, could be added later, or if the current ABI doesn't have *space" for it.

Very quickly, such a feature would allow users to define their own attributes (with metadata) and attach them to types/members. Once we get reflection APIs, we could then query them at runtime. It has many uses. For example, C# uses them for their serialisation API. Swift could use them to provide an alternative method for customising Codable properties:

member attribute codable {
    let key: String?
    let dateFormatter: DateFormatter?
    let dataFormatter: DataFormatter?

    init(key: String? = nil) {
        self.key = key
        self.dateFormatter = nil
        self.dataFormatter = nil
    }
}

extension codable where Self == Date {
    init(key: String? = nil, formatter: DateFormatter) {
        self.init(key: key)
        self.dateFormatter = formatter
    }
}

extension codable where Self == Data {
    init(key: String? = nil, formatter: DataFormatter) {
        self.init(key: key)
        self.dataFormatter = formatter
    }
}

struct Person: Codable {
    @codable(key: "first_name")
    let firstName: String

    @codable(key: "date_of_birth", formatter: .dayFormatter)
    let dateOfBirth: Date

    @codable(key: "finger_print", formatter: .secureFormatter)
    let fingerPrint: Data
}

// With imaginative runtime APIs

let person: Person = ...
let personReflection = Reflection(person)
for property in personReflection.properties {
    for attribute in property.attributes {
        if let codable = attribute as? @codable {
            // ...
        }
    }
}
15 Likes

I am very much in support of adding a proper attributes DSL to Swift. :ok_hand:t2:

Syntax

Syntactically I would prefer attributes to look something like

@(codable: (key: "date_of_birth", formatter: .dayFormatter))

instead of

@codable(key: "date_of_birth", formatter: .dayFormatter)

Attributes would borrow the argument-part's syntax of Swift's func-calls: …(bar: baz, blee: (qux: 42)) and thus could be arbitrarily nested.

Examples

Availability Checking

I don't think I've ever managed to get non-trivial availability checks right on first try.

Turning Swift's custom-feature #available

if #available(iOS 8.0, *) { … }

into an ordinary conditional compilation via attributes

@(if: (target: (os: "iOS", version: (greaterThanOrEqualTo: 8.1))))
class Foo {
    // implementation that can use iOS 8.0+ APIs
}

would include the attributed item if the pattern matches.

More Target Checks

System and their versions are not the only things one might want to conditionally compile for:

  • @(if: (target: (arch: "x86_64"))) (only include on x86 64bit targets)
  • @(if: (target: (family: "unix"))) (only include on unix-flavored targets)
  • @(if: (target: (env: "musl"))) (only include on targets using musl instead of libc)
  • @(if: (target: (endian: "little"))) (only include on little-endian targets)
  • @(if: (target: (pointerWidth: 32))) (only include on 32bit targets)
  • @(if: (target: (vendor: "apple"))) (only include on apple targets)
  • @(if: (scheme: "test")) (only include on test executions/builds)
  • @(if: ())

SIMD

Or take SIMD as another possible use of a composition of nested attributes:

@(if: (target_feature: "avx"))
func foo() {
    // implementation that can use `avx`
}

@(if: (not: (target_feature: "avx")))
func foo() {
    // a fallback implementation
}

Opinionated Examples

Being a strong proponent of explicit syntax I personally would love to see Swift take the next step and migrate lots of its current implicit specialized behaviors into attributes and by that open then up for extension to third-parties.

Synthesized Conformance

As such moving the current auto-conformance of protocols

class Foo: Equatable, Hashable { … }

to an explicit "derive" via attributes:

@(derive: (Equatable, Hashable))
class Foo { … }

Would make it clear that Equatable and Hashable are auto-derived, and not just to be found elsewhere in the code-base, when looking at a type declaration.

Conditional Synthesized Conformance

Unlike the current syntax this would also allow for expressing feature-gated conditional derivation/conformance without having to add any custom-syntax/features besides generalized attributes:

@(if: (feature: "some-feature"), then: (derive: (Equatable, Hashable)))
class Foo { … }

Enum allCases

Swift 4.2 addes support for auto-implementation of static let allCased: [Self] on enums.

This could easily be made opt-in and explicit via attributes:

protocol AllCases {
    static let allCased: [Self] { get }
}

@(derive: AllCases)
enum Foo {
    case Bar, Baz, Blee
}

All of this with a single generalized, yet familiar syntax.

But "why …", you wonder, "… why would we want to move all these things into attributes and make them explicit?". Because it also moves these things out of the realm of language items into ordinary userland. It makes the language leaner and yet more extensible and uniform. It makes userland extensibility a first-class citizen in Swift.


If you're thinking "wait, this looks a bit like Rust's attributes", then you're absolutely right!


Next step: Give compiler plugins a way to programmatically register custom attributes allowing for building stuff like this as a third-party.

Attributes could open so many doors for Swift:

2 Likes

Note: While I'm quite aware that @hartbit was specifically targeting the use-case of runtime reflection, I think that those could be handled as "just" one use of compile-time attributes as described above. I'd consider @hartbit's proposed reflection attributes a strict syntactical subset of the latter.

Without commenting on the general feature pitched by David, I have to say I don't think it's a good idea to move CaseIterable (I think that's what you've meant instead of AllCases) into an attribute. Leaving this feature as a protocols enables us to use it with conditional conformances which allows us to build for instance an OptionSet type BitSet<Case> which would map the enumerated collection from the enum Case into Self while also conform to CaseIterable.

enum Foo : Int, CaseIterable {
  case a
  case b
}

struct BitSet<Case> : OptionSet
  where
  Case : RawRepresentable,
  Case.RawValue : FixedWidthInteger {
  let rawValue: Case.RawValue
}

extension BitSet : ExpressibleByArrayLiteral {
  init(arrayLiteral elements: Case...) {
    self.rawValue = elements.lazy
      .map { 1 << $0.rawValue }
      .reduce(0, |)
  }
}

extension BitSet : CaseIterable where Case : CaseIterable {
  typealias AllCases = [BitSet]
  static var allCases: AllCases {
    return Case.allCases.map {
      BitSet(rawValue: $0.rawValue)
    }
  }
}

let set: BitSet<Foo> = [.a]
let setCases = BitSet<Foo>.allCases
let fooCases = setCases.compactMap {
  Foo(rawValue: $0.rawValue)
}

I'm not "moving CaseIterable (yes, that's what I meant instead of AllCases :smile:) into an attribute".
I'm just making explicit that it's being synthesized via derive. Making synthesization explicit via attributes rather than implicit via a conformance declaration is a mere difference in syntax. Everything else stays the same. By exposing a (derive: …) attribute that one could then later hook into (once we have proper compiler plugins) I merely lower the synthesization down to userland making it available to everybody, not just a small set of core-team-blessed protocols.

1 Like

The hope is to make synthesis user-accessible. Right now, we’re focusing on things that are either higher-priority or lower-cost. Compiler-provided synthesis exists in the meantime to help with the most painful problems.

There is virtually no chance we will change the syntax of attributes, though; we declared source stability in Swift 4, and changes that would break existing syntax now need strong justification. The bar for breaking the syntax of something so common would be so high, the only reason I can even imagine would be some kind of serious legal problem, like a patent.

2 Likes

I'd also be interested to know whether something like @hartbit's pitch is something that could be added after the ABI has been stabilized. Can anyone who would know confirm? @Douglas_Gregor @Joe_Groff

A bit off-topic, but I have a project with a lot of OptionSet structs and your code snippet looks so much better! Thanks!

1 Like

Yes, the proposal for custom attributes looks like it could be added to the language at any time.

Doug

5 Likes

Great. Thanks, Doug.