Pitch: Introduce custom attributes

Ah, you're right! That means the use of no-case enums could work for static attributes as they would work today. I have two remaining complaints:

  • Those fatalError()s will become a problem if we do get some kind of macro or metaprogramming system, where we want to actually do something with the instances but the result still must not go into the binary.

  • I feel like the use of no-case enums to ensure static-ness is really subtle, and I'd feel more comfortable with something like @staticAttribute that provides the guarantee that there is no per-use cost.

    Doug

3 Likes

These wouldn’t support static meta programs that want to work with the actual value though. That’s a very important eventual use case IMO.

+1

It is an important eventual use case, yes. But when we get there, I think we'll need some general way to declare that a type is macro-only, and we can use that mechanism for compile-time-only types with real data.

(On the other hand, we could just pick that keyword now and start using it for this, with plans to use it more generally in the future. e.g. @attribute(.struct, .class, .enum) compiletime struct AutoCoding { ... }.)

I'm afraid that laziness or lack of knowledge will cause many @attribute declarations to lack @compileTime even when it would be more appropriate. With @staticAttribute and @runtimeAttribute, users have to make a conscious choice at declaration time.

As an alternate syntax, what about lifting the attribute declaration to be a first-class type itself? Instead of

@runtimeAttribute(usage: [.struct, .class, .enum])
struct Versioned {
  let major: Int
  let minor: Int
}

You'd write

runtimeAttribute Versioned: struct, class, enum {
  let major: Int
  let minor: Int
}
  • The runtimeAttribute Versioned: struct, class, enum declaration mirrors the type constraint used for protocol FooProtocol: class.
  • I don't know if attributes need to use reference vs value semantics, or subclassing; this approach wouldn't work if so.
  • Similarly, it might be nice to declare an attribute as an enum, so you could use it ala @Platform.ios (kind of a contrived example but you get the idea).
  • I'm not sure how many types of attributes there might be; if there are quite a few, this might pollute the top-level namespace too much.
  • Would this be too complicated at the compiler level?

Thoughts?

For reference, adding new and unique of declarations has a fairly high cost in the compiler and conceptual cost in the language. The reason property delegates (and now custom attributes) are trending toward types is that it means we're inventing far less new syntax, keeping the overall language smaller.

Doug

9 Likes

Really excited about this!

I'm quite in favour of this syntax. It means we can write custom attributes with the same language features we're used to work with everyday. It's much easier to grasp the usage of @staticAttribute(usage: [.struct, .class, .enum]) than to learn a new syntax for declaring attributes.

As Swift doesn't allow (hopefully "just yet") for compile time execution, how would this work? Are these types gonna be exposed trough sourcekit/libsyntax and these kind of tools based on compiler output? I'm totally cool with that, still I wish that eventually we could just write some code in the staticAttribute itself that could run at compile time to generate that output.

Do you mean with this that propertyDelegate is gonna be a compiler known attribute like runtimeAttribute and staticAttribute OR that thanks to the custom attributes propertyDelegate will just become a custom attribute?


I've also seen in this thread posts mentioning Equatable and other protocols that the compiler automatically generates code for. Makes me wonder if with this custom attributes that generation could be moved out at the library level? And in any case, wouldn't we start causing a little discrepancy between protocols, automatic conformance with extensions and generated code?

For now, SourceKit/SwiftSyntax/libSyntax exposing them is the best we can do. Compile-time evaluation... some day.

The former: @propertyDelegate will be an attribute understood by the compiler. Each type tagged with @propertyDelegate will become a custom attribute.

We don't have a good way to express synthesis of protocol conformances in the language, yet: that would require some kind of (static or dynamic) reflection facility, which might want to query custom attributes (e.g., for coding key names, excluding some fields from Equatable / Hashable, etc.). I don't think we're boxing ourselves in with this design; we can always create other ways to define custom attributes if types don't cut it, but I really like the idea of types being the primary way because we understand them so well.

Doug

4 Likes

+1. This is such an elegant approach and should work well for anything I can think of. We don't need another language for custom attributes when the one we already have works perfectly well.

1 Like

I can not :+1: the idea of compile time and runtime custom attributes enough.

When I was a C# developer I used attributes built into the .NET framework all the time and often wrote my own custom attributes when I needed them for meta programming or plugin style behaviours.

I'm envious of my Android counterparts who get to use libraries like Retrofit for Restful services and frameworks like Dagger for Dependency Injection. All of these empowered by the JVM's support for developer defined attributes and type metadata.

For a custom attribute system to be truely useful however it needs to not just support annotating types, methods and properties but also method parameters and return types.

This is particularly important for tools like Retrofit (where you're mapping parameters to query or URL paths) and dependency injection (for constructor parameter injection).

The mechanism C# used for defining custom attributes is just for the developer to subclass System.Attribute. While we could define custom Swift syntax for defining attributes (like operators) I'm unsure why you would need to. Attribute parameters in C# are just effectively constructor params.

In C# controlling the applicability of a custom attribute is just defined by adding an attribute to the custom attribute type. Though a selection of built in Attribute subclasses in the stdlib Swift could do the same.

See: Creating Custom Attributes (C#) | Microsoft Learn

3 Likes

This is nice. There might also be a case for a version of this that takes a type to search for (for performance reasons). Alternatively I guess you could just use filter or compactMap and the like to make this more succinct.

reflect(DataRecord.self).runtimeAttributes.filter { $0 is Versioned }
1 Like

Hi @Oliver_Jones (and anyone else that may happen upon this thread): we've moved the conversation about this pitch into another thread for a second round of discussion and feedback. Feel free to join in!

2 Likes

I would greatly appreciate something like this, on the lines similar to what JetForMe alrwady documented

@Table(name: "User")
public struct Table {
@Column name: String
@Column ageInYears: Int
}

I would like it to do the following:

  • Be able to find all structs with annotation Table at run time
  • PropertyWrappers alleviate the need for a column considerably, but it may still be useful for separating out meta data from the value of the property
  • I would also like to use the annotation to track changes. For example, to do things like:

What was the name of the table in a prior schema version, so that it's easier to trigger updates

Currently I could use tools like Sourcery to achieve the same thing, but relying on code parsers is probably not the best long term solution

Yes, I always have the same feeling. Property wrapper is a good thing and partially we have property annotation but for the rest of the swift types we still don't have any tool for metaprogramming. Would be great to see in swift 6

2 Likes