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
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution