Thoughts and opinions about designing write capabilities for the reflection API


(Dimitri Racordon) #1

Hello evolutionists,

As a domain specific languages designer, reflection is an essential feature for me, as it allows for embedded DSLs to easily handle user-defined types. Unfortunately I think Swift is quite behind regarding that point, in particular due to its inability to set properties discovered from a mirror (without resorting to the black magic of unsafe pointers). Before attempting to write a more formal proposal, I'd like to collect thoughts and opinions on the best way to extend Swift's reflection API so that it would support write capabilities.

There are many points to address, but I have 2 open questions in particular I’d like to focus on:

1. What would be the best way to provide a setter?
2. Should we care about the constantness of the properties (and/or their owning subjects)?

Currently, one is able to introspect a particular subject that way:

struct Device {
    enum Kind {
        case phone, tablet
    }

    let kind: Kind
    var modelName: String
}

var iphone7 = Device(kind: .phone, modelName: "iPhone 7")

var mirror = Mirror(reflecting: iphone7)
for child in mirror.children {
    print("\(child.label!): \(child.value)")
}

// Prints "kind: phone"
// Prints "modelName: iPhone 7"

However, it is not possible to set the value of a property using reflection. `Mirror.Child` is a mere alias to `(String?, Any)`, and the mirror itself doesn't have any method that would achieve that.

First question: what would be the best way to provide a setter?

One solution would be to get inspiration from other reflection APIs (like that of Java for instance) and to modify the type of `Mirror.Child` so that it'd be a struct that has knowledge of its owning object. Then it could have a `set` method that would change the value of the property it reflects (note that for the time being I ignore the fact that `kind` is a let constant):

// Rough idea of how the type alias would be replaced ...
struct MirrorChild {
    label: String?
    value: Any
    mutating func set(_ newValue: Any) throws { /* ... */ }
}

if var kindProperty = mirror.children.first() {
    try prop.set(value: Device.Kind.tablet)
}

Note that in the following example, I made `set` a throwing function because the type of the argument is it given may not agree with that of the property. This is because I kept `MirrorChild` as close as possible to the original type alias. However, if it was a struct, it might be clever to use a generic:

struct MirrorChild<T> {
    label: String?
    value: T
    mutating func set(_ newValue: T) { /* ... */ }
}

This approach might be problematic when the owning object is a value type (like in my example) for the child instance to have a “reference” on its owning subject. I guess this could be handled by some internal black pointer magic, or we could push the `set` function in the mirror type. Then the target child would be specified using its label or its index (in cases the label doesn’t exists, e.g. in collections):

mirror.children["kind”]?.value = Device.Kind.tablet

Unfortunately, if I think this approach would work for almost all kind of subject, it wouldn't for enumerations. First, as far as I know, it is currently not possible to know which is the case of a reflected enumeration. Cases with associated values however are reflected by a mirror whose children contain a child labeled with the name of the case, and whose values are the associated values:

enum Nat {
    case zero
    indirect case succ(Nat)
}

if let child = Mirror(reflecting: Nat.succ(Nat.zero)).children.first {
    print(child)
}

// Prints "(Optional("succ"), Nat.zero)"

Second, implementing a `set` method for mirror children wouldn't allow us to change the case of an enum, but only its associated values. One idea would be to sightly change how enum cases are represented, in a fashion closer to how dictionary entries are represented:

if let child = Mirror(reflecting: Nat.zero).children.first {
    print(child)
}
// Prints "(nil, ("zero", nil)"

if let child = Mirror(reflecting: Nat.succ(Nat.zero)).children.first {
    print(child)
}
// Prints "(nil, ("succ", (Nat.zero)))"

The label would be replaced by the nil optional, and the value would be a tuple whose first element is the case name, and second element an optional tuple containing the associated values (i.e. `(String, Any?)`).

A more elaborated solution would involve adding a kind of `PropertyDescription` type, that would allow to get/set the value of a property on a given instance. I think this approach would require more changes in the API reflection, which is why I‘d prefer the first I proposed.

Second question: should we care about the constantness of the properties?

In the example I gave above, I didn't care about the fact that `kind` is a let property of `Device`. As far as I know, it is currently not possible to know if a given child of a mirror is a let or var property. It doesn't really matter for read-only reflection, but might for write reflection.

As originally a Python programmer, I wouldn't have much problem saying that it's the programmer's responsibility to ensure she/he doesn't break invariants when using reflection (all Python programmers are `__dict__` dirty hackers in their heart). However, I guess this position may very well not be shared, and may also very well not be applicable in a statically optimized language. Unfortunately, my knowledge of Swift's compiler isn't sufficient to know the slightest about the implications of such design choice.

The same question could be asked for subjects that are let constants. Would `iphone7` be a let constant in my example, would it make sense to allow *any* of its properties to be modified via reflection?

Thanks a lot if you made it that far!
I’m looking forward to your reactions about this.

Best regards,
Dimitri Racordon