In my codebase, I have a type which I use to wrap and interface with dynamic objects coming from another programming language (not directly relevant, but in this case, JavaScript). This type offers ways of accessing both properties and methods on the underlying value; simplified significantly, it looks something like this:
struct Value {
func invoke(_ method: String, arguments: [Any]) -> Value { ... }
subscript(_ property: String) -> Value? {
get { ... }
set { ... }
}
}
At the moment, using Value
looks somewhat like this:
let v: Value = ...
let result = v.invoke("doAThing", arguments: [foo])
print(result["bar"])
Workable, and explicit, but a bit unwieldy. To make access a bit more fluent, I was hoping to transition this over to using @dynamicMemberLookup
:
let v: Value = ...
let result = v.doAThing(foo)
print(result.bar)
As presented above, easy enough to do! The wrinkle: in practice, these methods wrap multiple layers of abstraction, and so for debugging purposes, the type threads through #function
, #fileID
, and #line
to capture information about callsites at the point of use:
struct Value {
func invoke(
_ method: String,
arguments: [Any],
function: StaticString = #function,
file: StaticString = #fileID,
line: UInt = #line
) -> Value { ... }
subscript(
_ property: String,
function: StaticString = #function,
file: StaticString = #fileID,
line: UInt = #line
) -> Value? {
get { ... }
set { ... }
}
}
Because these arguments have default values, actual callsites are unaffected:
let v: Value = ...
let result = v.invoke("doAThing", arguments: [foo])
print(result["bar"])
But, this presents an issue: subscript(dynamicMember:)
is currently only allowed to have two forms:
subscript(dynamicMember: {Writable}KeyPath<...>)
, andsubscript(dynamicMember: T)
(whereT
conforms toExpressibleByStringLiteral
)
These forms are checked strictly, so adding function:file:line:
to the subscript causes it to no longer validate, even with default argument values.
This is easy enough to work around for methods, because they'd need to be represented by a wrapper value which implements callAsFunction
anyway:
@dynamicMemberLookup
struct Value {
struct Method {
// Instead of capturing `#function` et. al. in
// subscript(dynamicMember:), we can capture them here
func callAsFunction(
function: StaticString = #function,
file: StaticString = #fileID,
line: UInt = #line,
_ arguments: Any?...
) -> Value { ... }
}
subscript(dynamicMember method: String) -> Method { ... }
}
let v: Value = ...
let result = v.doAThing(foo)
// equivalent to v[dynamicMember: "doAThing"].callAsFunction(function: ..., foo)
(Yes, it's technically possible for the dynamic method to be looked up on a different line from the call so I'm not necessarily capturing the exact info I'm hoping for, but in practice, this will never happen.)
Properties, though, are more annoying, because it means that I'd have to wrap them up in a value with get()
and set()
methods:
@dynamicMemberLookup
struct Value {
struct Property {
// Instead of capturing `#function` et. al. in
// subscript(dynamicMember:), we can capture them here
func get(
function: StaticString = #function,
file: StaticString = #fileID,
line: UInt = #line
) -> Value? { ... }
func set(
_ value: Value?,
function: StaticString = #function,
file: StaticString = #fileID,
line: UInt = #line
) { ... }
}
subscript(dynamicMember property: String) -> Property { ... }
}
let v: Value = ...
let result = v.doAThing(foo)
print(result.bar.get())
// equivalent to result[dynamicMember: "bar"].get(function: ...)
Far from unworkable, but a little unfortunate.
So, I have two questions:
- Would it be feasible to allow
subscript(dynamicMember:)
to match with additional arguments, so long as they have default values? Are there any obvious drawbacks I'm not thinking of? I know this is fairly niche, but I'm not opposed to pitching and implementing - (Less relevant to this category, but:) Is there another scheme I can use to capture
#function
& co. without having to resort to aget()
andset()
? Likely not, but I figure I'm not the first person to have run into this