Allow dynamic keyword on non-objc properties

This pitch is the result of a question "Dynamic member lookup on specific typed properties".

The swift compiler should tranlate this

class Token {
	dynamic var expiresAfter : Double?
	...
}

into this

class Token {
	var expiresAfter : Double? {
		get { return self[dynamicMember: #function] }
		set { self[dynamicMember: #function] = newValue }
	}
	...
}

The current alternatives are:

  1. write this boilerplate code for each and every property
  2. define the properties in a seperate file, then generate the boilerplate code
  3. use @dynamicMemberLookup

The final alternative is actually the worst because it throws the door open to code such as:

@dynamicMemberLookup
class Token {
	...
}

var token = Token()
token.expiresAfter = "Hello World"

Note that we know about expiresAfter at compile time but we're not allowed to tell the compiler about the type even if we know it.

@NSManaged

Trevor Squires's article Why Swift is the most satisfying language for using Core Data showed how to use Core Data in Swift for a more satisfying experience

However the best he could come up with given Swift was this:

class Token : NSManagedObject {
    var expiresAfter: Double? {
        get {
            let member = #function
            willAccessValue(forKey: member)
            defer { didAccessValue(forKey: member) }
            let number = primitiveValue(forKey: member) as? NSNumber
            return number?.doubleValue
        }
        set {
            let member = #function
            willChangeValue(forKey: member)
            defer { didChangeValue(forKey: member) }
            let number = newValue.map({NSNumber(value: $0)})
            setPrimitiveValue(number, forKey: member)
        }
    }
}

(Note that I've modified Trevor's example to replace @NSManaged with the functionally equivalent primitiveValue(forKey:){%.language=swift})

With Swift only dynamic one could write this instead:

class Token: ManagedObject {
    dynamic var expiresAfter: Double?
}

with the subscript(dynamicMember:) being moved to a base class

class ManagedObject : NSManagedObject {
    subscript(dynamicMember member: String) -> Double? {
        get {
            willAccessValue(forKey: member)
            defer { didAccessValue(forKey: member) }
            let number = primitiveValue(forKey: member) as? NSNumber
            return number?.doubleValue
        }
        set(newValue) {
            willChangeValue(forKey: member)
            defer { didChangeValue(forKey: member) }
            let number = newValue.map({NSNumber(value: $0)})
            setPrimitiveValue(number, forKey: member)
        }
    }
}
2 Likes

I'm curious about ManagedObject use cases. Is it a problem if dynamic properties are limited to a single type (the return type of subscript(dynamicMember:))?

class Token {
    dynamic var expiresAfter: Double? { ... }
    // What if I want dynamic properties of a different type?
    dynamic var issuer: String? { ... }

    subscript(dynamicMember member: String) -> Double? { ... }
}

I think that's exactly correct. My goal is to be able to define the type for a dynamic property and disallow anything else at compile time.

The example you give fails to compile:

class Token: ManagedObject {
    var expiresAfter: Double? {
        get { return self[dynamicMember: #function] }
        set { self[dynamicMember: #function] = newValue }
    }
    
    var issuer: String? {
        get { return self[dynamicMember: #function] } // 🛑 Cannot subscript a value of type 'Token'
        set { self[dynamicMember: #function] = newValue } // 🛑 Cannot assign value of type 'String?' to type 'Double?'
    }
}

I think this is correct, although with a different error as it's generated code. Something like this maybe?

'dynamic' var 'issuer' requires 'Token' to have method 'subscript(dynamicMember:) -> String?'

Well, correct me if I'm wrong but the dynamic on an @objc property has a very specific meaning i.e. that it produces KVO notifications for access to this property. (Only annotating with @objc doesn't suffice)

I would expect any property that is annotated as dynamic without being @objc to produce some future Swift-only observations to fire. I think that this was actually discussed some time ago (I think with @jrose? ).

Therefore I am more or less against using dynamic in the context that is suggested here. (Keeping my fingers crossed that Swift will one day have something similar to KVO but native :thinking:)

But then again, maybe I'm completely wrong here…

No, dynamic does not mean "KVO". dynamic means "the implementation might be different at run time from what's seen here". Automatic KVO works by replacing the implementation of a setter, which is why dynamic is necessary, but it's not the only use case.

dynamic on a non-@objc property or method in Swift would mean there's some way to replace the implementation of this property or method at run time, presumably exposed through the standard library.

9 Likes

Thank you for the clarification!

So I guess the mythical "Property Behaviors" would fit into this concept in the future? This would also allow for something like the OP's idea to be defined I guess.

Yesterday I tried whether I can use dynamic properties with String-based enums (does not work). If the following code would work a lot of boilerplate could be avoided for a exact set of functions known at compile-time:

class Token {
    
    enum DoubleGroup: String { case expiresAfter, ... }
    enum StringGroup: String { case issuer, ... }
    
    subscript(dynamicMember member: DoubleGroup) -> Double? { ... }
    subscript(dynamicMember member: StringGroup) -> String? { ... }
}

Of course the type checker should be able to disambiguate the following code:

let exp = token.expiresAfter // -> Double?
let iss = token.issuer // -> String?

As an example of what dynamic could mean and how it could be used in Swift without @objc see the proposal on dynamic function replacement just posted here: Dynamic method replacement

@dan-zheng can you point me to where I could take this pitch to progress it? (if you're implementing stuff in swift then you must be skilled in the arts of pitch progression :slight_smile:)

Sure!

If you want to elevate your pitch into a formal proposal, please submit a PR to Swift Evolution. You should follow this proposal template. The full Swift Evolution process is documented here.


I might suggest asking for more feedback from the community first, though.

Personally, I'd like to see a proposed solution for multiple dynamic var properties of different types. It could be as simple as defining multiple subscript(dynamicMember:) methods (with result types matching the properties' types), similar to @jazzbox's post above.

Thanks for your help.

wrt to multiple types, does the current proposal not address that? Are you referring to something different than referenced here: Allow dynamic keyword on non-objc properties - #3 by jjrscott

Your linked post is a bit confusing to me: it seems that your code example fails to compile?
Could you please provide a clear, working example of differently typed dynamic var properties?

Ah, my apologies. I trying to show that multiple types are already handled by Swift. As it's a straight code swap the actual Swift (a little ways along the pipeline remains unchanged).

Here's an example that will compile and run:

class ManagedObject {
    
    var values = [String:Any]()
    
    subscript(dynamicMember member: String) -> Double? {
        get {
            return values[member] as? Double
        }
        set(newValue) {
            values[member] = newValue
        }
    }
    
    subscript(dynamicMember member: String) -> String? {
        get {
            return values[member] as? String
        }
        set(newValue) {
            values[member] = newValue
        }
    }
}

class Token: ManagedObject {
    var expiresAfter: Double? {
        get { return self[dynamicMember: #function] }
        set { self[dynamicMember: #function] = newValue }
    }
    
    var issuer: String? {
        get { return self[dynamicMember: #function] }
        set { self[dynamicMember: #function] = newValue }
    }
}

let token = Token()

token.issuer = "bob"
print("issuer = \(token.issuer  ?? "<unset>")")

If the pitch was implemented, Token would look like this:

class Token: ManagedObject {
    dynamic var expiresAfter: Double?
    dynamic var issuer: String?
}

Rememeber, the suggestion is almost a simple regex (in preprocessor terms). Very early on the compiler replaces this pattern

dynamic var !!!memberName!!! : !!!memberType!!!

with

var !!!memberName!!! : !!!memberType!!! {
        get { return self[dynamicMember: #function] }
        set { self[dynamicMember: #function] = newValue }
    }

The type/generics system will then take care of the rest. My earlier response described how the compiler would correctly fail, but that we could supply a better error than the current ones (which would anyway be hidden away in generated code).

Am I on the right lines of understand your question and/or answering it?

Yes, that answers it!

One thing that gives me pause is that the requirements for dynamic var and @dynamicMemberLookup are pretty ad-hoc (as opposed to protocol requirements, which are easier to understand and provide good fix-its). I'd like to move away from this direction in general, unless there's a compelling reason otherwise.

I agree that the error for @dynamicMemberLookup is a bit unhelpful:

@dynamicMemberLookup attribute requires 'Thing' to have a 'subscript(dynamicMember:)' member with a string index

In fact, that's one of the (two) reasons I started down this road; I think dynamic var is an improvement in two ways:

Improved error handling

which contains all the material needed for a great fix-it / stubs vargetset Swift Computed Variable Get and Set Declaration:

subscript(dynamicMember member: String) -> !!!memberType!!! {
    get {
        <#statements#>
    }
    set {
        <#variable name#> = newValue
    }
}

Member definition returns home

In Swift, we're all more used to member types being defined in the class definition rather than the call site. dynamic allows dynamic value lookup while keeping the type in the expected location. In fact, given that this works

class Token: ManagedObject  {
    var expiresAfter: Double? {
        get { return self[dynamicMember: #function] }
        set { self[dynamicMember: #function] = newValue }
    }
}

extension Token {
    var issuer: String? {
        get { return self[dynamicMember: #function] }
        set { self[dynamicMember: #function] = newValue }
    }
}

the dynamic var equivalent would also work:

class Token: ManagedObject  {
    dynamic var expiresAfter: Double?
}

extension Token {
    dynamic var issuer: String?
}

I think dynamic var may be superior to @dynamicMemberLookup even if you explicitly want to do something like this:

@dynamicMemberLookup
class Coin : ManagedObject { }

let coin = Coin()
coin.expiresAfter = 5000
coin.expiresAfter = "one week"

as this works

class Coin : ManagedObject {
    dynamic var expiresAfter: Any?
}

let coin = Coin()
coin.expiresAfter = 5000
coin.expiresAfter = "one week"

without issue.

I feel like I've missed something major :slightly_frowning_face:

I think that property behaviors (currently shelved, but should come back someday) are really the best answer to this. Here is the old draft proposal.

3 Likes

Thanks for pointing me in the direction of behaviours. Reading the proposal, I couldn't help wonder how it would be written in light of Swift 4.2 dynamic member lookup. Allowing «objectName».«variableName» to be mapped to «objectName»[dynamicMember: "«variableName»"] enables the programmer to emulate pretty much everything that behaviours would have enabled.

To attempt something that is likely too ambitious I've generalized the dynamic var mapping out to behaviours.

class «className» {
	«behaviorName» var «variableName» : «variableType»
}

maps to

class «className» {
	var «variableName» : «variableType» {
		get { return self[«behaviorName»Member: #function] }
		set { self[«behaviorName»Member: #function] = newValue }
	}
}

with a requirement for a method with the signature

subscript(«behaviorName»Member member: String) -> «variableType»

If not available then the following error would be produced

'«behaviorName»' var '«variableName»' requires '«className»' to have method 'subscript(«behaviorName»Member:) -> «variableType»'

which would include a fix-it to create the stub

subscript(«behaviorName»Member member: String) -> «variableType» {
	get {
		<#statements#>
	}
	set(newValue) {
		<#variable name#> = newValue
	}
}

I'm not sure if that's better or worse :worried:

Dynamic member looking and property behaviors are orthogonal things, and I don't think they really relate. Property behaviors (IMO, which should be renamed to property macros) are a modifier that you place on an explicit named property declaration. DML is about implicit properties that don't have declarations.

2 Likes

I'm actually after creating a much need type-safe replacement for @NSManaged and was diverted somewhat by the elegant solution provided for implicit properties by @dynamicMethodLookup.

dynamic var is an attempt to build on that by allowing explicitly named property declarations to use the same machinery. What I produced, as you kindly pointed out, what I'm proposing looks closer to property behaviours (or property macros). However, I still think dynamic var neatly uses existing functionality to enable code of greater clarity.

What do you think of the premise itself viewed through the lens of macros (or standing by itself)?

Would it be acceptable to change the name to managed var with subscript(managedMember:) or is there a general issue with mapping defined properties to a common backing method?